Display loading icon until host details are saved (#1376)

This commit is contained in:
Kyle Knight 2017-03-09 07:50:53 -08:00 committed by Jason Meller
parent 84ffd1d5a3
commit b23ab83336
8 changed files with 73 additions and 7 deletions

View File

@ -1,3 +1,5 @@
* Show loading spinner while newly added Host Details are saved
* Show a generic computer icon when when referring to hosts with an unknown platform instead of the text "All"
* Kolide will now warn on startup if there are database migrations not yet completed.

View File

@ -4,6 +4,7 @@ import Button from 'components/buttons/Button';
import hostInterface from 'interfaces/host';
import Icon from 'components/icons/Icon';
import PlatformIcon from 'components/icons/PlatformIcon';
import CircleLoader from 'components/loaders/Circle';
import { humanMemory, humanUptime } from './helpers';
const baseClass = 'host-details';
@ -29,7 +30,7 @@ const ActionButton = ({ host, onDestroyHost, onQueryHost }) => {
);
};
const HostDetails = ({ host, onDestroyHost, onQueryHost }) => {
const HostDetails = ({ host, onDestroyHost, onQueryHost, isLoading }) => {
const {
host_cpu: hostCpu,
host_mac: hostMac,
@ -46,16 +47,17 @@ const HostDetails = ({ host, onDestroyHost, onQueryHost }) => {
return (
<div className={`${baseClass} ${baseClass}--${status}`}>
<header className={`${baseClass}__header`}>
<span className={`${baseClass}__cta-host`}>
{!isLoading && <span className={`${baseClass}__cta-host`}>
<ActionButton host={host} onDestroyHost={onDestroyHost} onQueryHost={onQueryHost} />
</span>
</span>}
<p className={`${baseClass}__hostname`}>{hostname}</p>
<p className={`${baseClass}__hostname`}>{hostname || 'incoming host'}</p>
</header>
<ul className={`${baseClass}__details-list`}>
{isLoading && <div className={`${baseClass}__loader`}><CircleLoader /></div>}
{!isLoading && <ul className={`${baseClass}__details-list`}>
<li className={` ${baseClass}__detail ${baseClass}__detail--os`}>
{platform && <PlatformIcon name={platform} className={`${baseClass}__icon`} title="Operating System & Version" />}
<PlatformIcon name={platform} className={`${baseClass}__icon`} title="Operating System & Version" />
<span className={`${baseClass}__host-content`}>{osVersion || '--'}</span>
</li>
@ -88,7 +90,7 @@ const HostDetails = ({ host, onDestroyHost, onQueryHost }) => {
<Icon name="world" className={`${baseClass}__icon`} title="IP Address" />
<span className={`${baseClass}__host-content ${baseClass}__host-content--mono`}>{hostIpAddress || '--'}</span>
</li>
</ul>
</ul>}
</div>
);
};
@ -103,6 +105,7 @@ HostDetails.propTypes = {
host: hostInterface.isRequired,
onDestroyHost: PropTypes.func.isRequired,
onQueryHost: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
};
export default HostDetails;

View File

@ -1,6 +1,7 @@
import React from 'react';
import expect, { createSpy, restoreSpies } from 'expect';
import { mount } from 'enzyme';
import { noop } from 'lodash';
import { hostStub } from 'test/stubs';
import HostDetails from 'components/hosts/HostDetails';
@ -18,6 +19,7 @@ describe('HostDetails - component', () => {
host={offlineHost}
onDestroyHost={destroySpy}
onQueryHost={querySpy}
isLoading={false}
/>
);
const btn = offlineComponent.find('Button');
@ -40,6 +42,7 @@ describe('HostDetails - component', () => {
host={miaHost}
onDestroyHost={destroySpy}
onQueryHost={querySpy}
isLoading={false}
/>
);
const btn = miaComponent.find('Button');
@ -62,6 +65,7 @@ describe('HostDetails - component', () => {
host={onlineHost}
onDestroyHost={destroySpy}
onQueryHost={querySpy}
isLoading={false}
/>
);
const btn = onlineComponent.find('Button');
@ -73,5 +77,19 @@ describe('HostDetails - component', () => {
expect(destroySpy).toNotHaveBeenCalled();
expect(querySpy).toHaveBeenCalled();
});
it('renders a spinner while hosts details are loaded', () => {
const loadingComponent = mount(
<HostDetails
host={{ ...hostStub }}
onDestroyHost={noop}
onQueryHost={noop}
isLoading
/>
);
expect(loadingComponent.find('Circle').length).toEqual(1);
expect(loadingComponent.find('.host-details__details-list').length).toEqual(0);
});
});

View File

@ -3,6 +3,7 @@
@include flex-direction(column);
@include flex-grow(1);
@include flex-basis(250px);
justify-content: center;
background-color: $white;
border-radius: 3px;
box-shadow: 0 12px 17px 0 rgba(47, 47, 91, 0.07), 0 3px 8px 0 rgba(0, 0, 0, 0.08), 0 -2px 0 0 rgba(32, 36, 50, 0.04);
@ -84,6 +85,14 @@
padding: 8px 0;
}
&__loader {
flex-grow: 1;
align-self: center;
align-content: center;
display: flex;
align-items: center;
}
&__detail {
font-size: 15px;
line-height: 15px;

View File

@ -0,0 +1,11 @@
import React from 'react';
const baseClass = 'kolide-circle-loader';
const Circle = () => {
return (
<div className={baseClass} />
);
};
export default Circle;

View File

@ -0,0 +1,19 @@
$circle-slice: #48c586;
$circle-background: rgba($circle-slice, 0.2);
$circle-size: 60px;
.kolide-circle-loader {
@include size($circle-size);
border-radius: 50%;
border: #{$circle-size / 12} solid $circle-background;
border-left-color: $circle-slice;
transform: rotate(0deg);
animation: spin-circle 1s linear infinite;
}
@keyframes spin-circle {
100% {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1 @@
export default from './Circle';

View File

@ -522,12 +522,15 @@ export class ManageHostsPage extends Component {
if (display === 'Grid') {
return sortedHosts.map((host) => {
const isLoading = !host.hostname;
return (
<HostDetails
host={host}
key={`host-${host.id}-details`}
onDestroyHost={toggleDeleteHostModal}
onQueryHost={onQueryHost}
isLoading={isLoading}
/>
);
});