mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
New styles and layout for hosts page. Remove grid view. (#73)
The goal of these changes is to update the main content (center column) of the /hosts page. What is included in these changes: - Removing the grid view for hosts. This required removing the actions, reducers, and props using to toggle the display between grid and table view. The toggle buttons in the UI are also removed. - Adding host_cpu, memory, and uptime columns to the table. This increases the table's width which is now horizontally scrollable. - Removing the HostDetails component used in the grid view. Moving the helpers.js file to HostTable. Adjusting JS tests to account for these changes. - Updating pagination styles.
This commit is contained in:
parent
57950a9645
commit
3953afb0b0
BIN
assets/images/no-matching-host-100x100@2x.png
Normal file
BIN
assets/images/no-matching-host-100x100@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
@ -4,7 +4,7 @@ import { noop } from 'lodash';
|
||||
import AceEditor from 'react-ace';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import hostHelpers from 'components/hosts/HostDetails/helpers';
|
||||
import hostHelpers from 'components/hosts/HostsTable/helpers';
|
||||
import Icon from 'components/icons/Icon';
|
||||
import PlatformIcon from 'components/icons/PlatformIcon';
|
||||
import targetInterface from 'interfaces/target';
|
||||
|
@ -4,9 +4,9 @@ import PropTypes from 'prop-types';
|
||||
import hostInterface from 'interfaces/host';
|
||||
import labelInterface from 'interfaces/label';
|
||||
import HostsTable from 'components/hosts/HostsTable';
|
||||
import HostDetails from 'components/hosts/HostDetails';
|
||||
import LonelyHost from 'components/hosts/LonelyHost';
|
||||
import Spinner from 'components/loaders/Spinner';
|
||||
import NoHostsImage from '../../../../assets/images/no-matching-host-100x100@2x.png';
|
||||
|
||||
const baseClass = 'host-container';
|
||||
|
||||
@ -15,7 +15,6 @@ class HostContainer extends Component {
|
||||
hosts: PropTypes.arrayOf(hostInterface),
|
||||
selectedLabel: labelInterface,
|
||||
loadingHosts: PropTypes.bool.isRequired,
|
||||
displayType: PropTypes.oneOf(['Grid', 'List']),
|
||||
toggleAddHostModal: PropTypes.func,
|
||||
toggleDeleteHostModal: PropTypes.func,
|
||||
onQueryHost: PropTypes.func,
|
||||
@ -28,40 +27,29 @@ class HostContainer extends Component {
|
||||
|
||||
return (
|
||||
<div className={`${baseClass} ${baseClass}--no-hosts`}>
|
||||
<h1>No matching hosts found.</h1>
|
||||
<h2>Where are the missing hosts?</h2>
|
||||
<ul>
|
||||
{isCustom && <li>Check your SQL query above to confirm there are no mistakes.</li>}
|
||||
<li>Check to confirm that your hosts are online.</li>
|
||||
<li>Confirm that your expected hosts have osqueryd installed and configured.</li>
|
||||
</ul>
|
||||
<div className={`${baseClass}--no-hosts__inner`}>
|
||||
<img src={NoHostsImage} alt="No Hosts" />
|
||||
<div>
|
||||
<h1>No matching hosts found.</h1>
|
||||
<h2>Where are the missing hosts?</h2>
|
||||
<ul>
|
||||
{isCustom && <li>Check your SQL query above to confirm there are no mistakes.</li>}
|
||||
<li>Check to confirm that your hosts are online.</li>
|
||||
<li>Confirm that your expected hosts have osqueryd installed and configured.</li>
|
||||
</ul>
|
||||
|
||||
<div className={`${baseClass}__no-hosts-contact`}>
|
||||
<p>Still having trouble?</p>
|
||||
<p><a href="https://github.com/fleetdm/fleet/issues">File a Github issue</a>.</p>
|
||||
<div className={`${baseClass}__no-hosts-contact`}>
|
||||
<p>Still having trouble?</p>
|
||||
<a href="https://github.com/fleetdm/fleet/issues">File a Github issue</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHosts = () => {
|
||||
const { displayType, hosts, toggleDeleteHostModal, onQueryHost } = this.props;
|
||||
|
||||
if (displayType === 'Grid') {
|
||||
return hosts.map((host) => {
|
||||
const isLoading = !host.hostname;
|
||||
|
||||
return (
|
||||
<HostDetails
|
||||
host={host}
|
||||
key={`host-${host.id}-details`}
|
||||
onDestroyHost={toggleDeleteHostModal}
|
||||
onQueryHost={onQueryHost}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
const { hosts, toggleDeleteHostModal, onQueryHost } = this.props;
|
||||
|
||||
return (
|
||||
<HostsTable
|
||||
@ -74,7 +62,7 @@ class HostContainer extends Component {
|
||||
|
||||
render () {
|
||||
const { renderHosts, renderNoHosts } = this;
|
||||
const { hosts, displayType, loadingHosts, selectedLabel, toggleAddHostModal } = this.props;
|
||||
const { hosts, loadingHosts, selectedLabel, toggleAddHostModal } = this.props;
|
||||
|
||||
if (loadingHosts) {
|
||||
return <Spinner />;
|
||||
@ -89,7 +77,7 @@ class HostContainer extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClass} ${baseClass}--${displayType.toLowerCase()}`}>
|
||||
<div className={`${baseClass}`}>
|
||||
{renderHosts()}
|
||||
</div>
|
||||
);
|
||||
|
@ -39,15 +39,9 @@ describe('HostsContainer - component', () => {
|
||||
expect(page.find('.host-container--no-hosts').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders hosts as HostDetails by default', () => {
|
||||
it('renders hosts as HostsTable', () => {
|
||||
const page = mount(<HostContainer {...props} />);
|
||||
|
||||
expect(page.find('HostDetails').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders hosts as HostsTable when the display is "List"', () => {
|
||||
const page = mount(<HostContainer {...props} displayType="List" />);
|
||||
|
||||
expect(page.find('HostsTable').length).toEqual(1);
|
||||
});
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
.host-container {
|
||||
|
||||
&--no-hosts {
|
||||
width: 440px;
|
||||
margin: 35px auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 35px;
|
||||
font-size: 15px;
|
||||
font-weight: $regular;
|
||||
line-height: 2;
|
||||
letter-spacing: normal;
|
||||
color: rgba(32, 37, 50, 0.66);
|
||||
border-top: 1px solid $ui-borders;
|
||||
|
||||
h1 {
|
||||
font-size: $large;
|
||||
@ -19,21 +22,40 @@
|
||||
h2 {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.5px;
|
||||
margin: 0 0 24px;
|
||||
line-height: 20px;
|
||||
color: $core-black;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
padding: 0;
|
||||
color: $core-black;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
|
||||
&::before {
|
||||
content: '•';
|
||||
color: $core-blue;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__inner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
img {
|
||||
margin-right: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__no-hosts-contact {
|
||||
text-align: left;
|
||||
margin-top: 30px;
|
||||
margin-top: 24px;
|
||||
|
||||
p {
|
||||
color: $core-black;
|
||||
@ -50,12 +72,4 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--grid {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
@ -1,178 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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, humanLastSeen } from './helpers';
|
||||
|
||||
const baseClass = 'host-details';
|
||||
|
||||
export const statuses = {
|
||||
online: 'Online',
|
||||
offline: 'Offline',
|
||||
};
|
||||
|
||||
const ActionButton = ({ host, onDestroyHost, onQueryHost }) => {
|
||||
if (host.status === 'online') {
|
||||
return (
|
||||
<Button
|
||||
onClick={onQueryHost(host)}
|
||||
variant="unstyled"
|
||||
title="Query this host"
|
||||
>
|
||||
<Icon name="query" className={`${baseClass}__cta-host-icon`} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={onDestroyHost(host)}
|
||||
variant="unstyled"
|
||||
title="Delete this host"
|
||||
>
|
||||
<Icon name="trash" className={`${baseClass}__cta-host-icon`} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const lastSeenTime = (status, seenTime) => {
|
||||
if (status !== 'online') {
|
||||
return `Last Seen: ${humanLastSeen(seenTime)} UTC`;
|
||||
}
|
||||
|
||||
return 'Online';
|
||||
};
|
||||
|
||||
const HostDetails = ({ host, onDestroyHost, onQueryHost, isLoading }) => {
|
||||
return (
|
||||
<div className={`${baseClass} ${baseClass}--${host.status}`}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
{!isLoading && (
|
||||
<span className={`${baseClass}__cta-host`}>
|
||||
<ActionButton
|
||||
host={host}
|
||||
onDestroyHost={onDestroyHost}
|
||||
onQueryHost={onQueryHost}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<p
|
||||
className={`${baseClass}__hostname`}
|
||||
title={lastSeenTime(host.status, host.seen_time)}
|
||||
>
|
||||
{host.hostname || 'incoming host'}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{isLoading && (
|
||||
<div className={`${baseClass}__loader`}>
|
||||
<CircleLoader />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && (
|
||||
<ul className={`${baseClass}__details-list`}>
|
||||
<li className={` ${baseClass}__detail ${baseClass}__detail--os`}>
|
||||
<PlatformIcon
|
||||
name={host.platform}
|
||||
className={`${baseClass}__icon`}
|
||||
title="Operating System & Version"
|
||||
/>
|
||||
<span className={`${baseClass}__host-content`}>
|
||||
{host.os_version || '--'}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li className={` ${baseClass}__detail ${baseClass}__detail--osquery`}>
|
||||
<Icon
|
||||
name="osquery"
|
||||
className={`${baseClass}__icon`}
|
||||
title="Osquery Version"
|
||||
/>
|
||||
<span className={`${baseClass}__host-content`}>
|
||||
{host.osquery_version || '--'}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li className={` ${baseClass}__detail ${baseClass}__detail--cpu`}>
|
||||
<Icon
|
||||
name="cpu"
|
||||
className={`${baseClass}__icon`}
|
||||
title="CPU Cores and Speed"
|
||||
/>
|
||||
<span className={`${baseClass}__host-content`}>
|
||||
{host.host_cpu || '--'}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li className={` ${baseClass}__detail ${baseClass}__detail--memory`}>
|
||||
<Icon
|
||||
name="memory"
|
||||
className={`${baseClass}__icon`}
|
||||
title="Memory / RAM"
|
||||
/>
|
||||
<span className={`${baseClass}__host-content`}>
|
||||
{humanMemory(host.memory) || '--'}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li className={` ${baseClass}__detail ${baseClass}__detail--uptime`}>
|
||||
<Icon
|
||||
name="uptime"
|
||||
className={`${baseClass}__icon`}
|
||||
title="Uptime"
|
||||
/>
|
||||
<span className={`${baseClass}__host-content`}>
|
||||
{humanUptime(host.uptime) || '--'}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li className={` ${baseClass}__detail ${baseClass}__detail--mac`}>
|
||||
<Icon
|
||||
name="mac"
|
||||
className={`${baseClass}__icon`}
|
||||
title="MAC Address"
|
||||
/>
|
||||
<span
|
||||
className={`${baseClass}__host-content ${baseClass}__host-content--mono`}
|
||||
>
|
||||
{host.primary_mac ? host.primary_mac.toUpperCase() : '--'}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li className={` ${baseClass}__detail ${baseClass}__detail--ip`}>
|
||||
<Icon
|
||||
name="world"
|
||||
className={`${baseClass}__icon`}
|
||||
title="IP Address"
|
||||
/>
|
||||
<span
|
||||
className={`${baseClass}__host-content ${baseClass}__host-content--mono`}
|
||||
>
|
||||
{host.primary_ip || '--'}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ActionButton.propTypes = {
|
||||
host: hostInterface.isRequired,
|
||||
onDestroyHost: PropTypes.func.isRequired,
|
||||
onQueryHost: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
HostDetails.propTypes = {
|
||||
host: hostInterface.isRequired,
|
||||
onDestroyHost: PropTypes.func.isRequired,
|
||||
onQueryHost: PropTypes.func.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default HostDetails;
|
@ -1,95 +0,0 @@
|
||||
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';
|
||||
|
||||
describe('HostDetails - component', () => {
|
||||
afterEach(restoreSpies);
|
||||
|
||||
it('calls the onDestroyHost prop when the action button is clicked on an offline host', () => {
|
||||
const destroySpy = createSpy();
|
||||
const querySpy = createSpy();
|
||||
const offlineHost = { ...hostStub, status: 'offline' };
|
||||
|
||||
const offlineComponent = mount(
|
||||
<HostDetails
|
||||
host={offlineHost}
|
||||
onDestroyHost={destroySpy}
|
||||
onQueryHost={querySpy}
|
||||
isLoading={false}
|
||||
/>,
|
||||
);
|
||||
const btn = offlineComponent.find('Button');
|
||||
|
||||
expect(btn.find('Icon').prop('name')).toEqual('trash');
|
||||
|
||||
btn.simulate('click');
|
||||
|
||||
expect(destroySpy).toHaveBeenCalled();
|
||||
expect(querySpy).toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls the onDestroyHost prop when the action button is clicked on a mia host', () => {
|
||||
const destroySpy = createSpy();
|
||||
const querySpy = createSpy();
|
||||
const miaHost = { ...hostStub, status: 'mia' };
|
||||
|
||||
const miaComponent = mount(
|
||||
<HostDetails
|
||||
host={miaHost}
|
||||
onDestroyHost={destroySpy}
|
||||
onQueryHost={querySpy}
|
||||
isLoading={false}
|
||||
/>,
|
||||
);
|
||||
const btn = miaComponent.find('Button');
|
||||
|
||||
expect(btn.find('Icon').prop('name')).toEqual('trash');
|
||||
|
||||
btn.simulate('click');
|
||||
|
||||
expect(destroySpy).toHaveBeenCalled();
|
||||
expect(querySpy).toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls the onQueryHost prop when the action button is clicked on an online host', () => {
|
||||
const destroySpy = createSpy();
|
||||
const querySpy = createSpy();
|
||||
const onlineHost = { ...hostStub, status: 'online' };
|
||||
|
||||
const onlineComponent = mount(
|
||||
<HostDetails
|
||||
host={onlineHost}
|
||||
onDestroyHost={destroySpy}
|
||||
onQueryHost={querySpy}
|
||||
isLoading={false}
|
||||
/>,
|
||||
);
|
||||
const btn = onlineComponent.find('Button');
|
||||
|
||||
expect(btn.find('Icon').prop('name')).toEqual('query');
|
||||
|
||||
btn.simulate('click');
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -1,148 +0,0 @@
|
||||
.host-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
flex-basis: 250px;
|
||||
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);
|
||||
border: solid 1px rgba(73, 143, 226, 0.14);
|
||||
box-sizing: border-box;
|
||||
margin: 30px 7px 0;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
max-width: 300px;
|
||||
|
||||
&--online {
|
||||
border-top: 6px solid $success;
|
||||
|
||||
.host-details__status,
|
||||
.host-details__hostname {
|
||||
color: $success;
|
||||
}
|
||||
}
|
||||
|
||||
&--offline {
|
||||
border-top: 6px solid $alert;
|
||||
|
||||
.host-details__status,
|
||||
.host-details__hostname {
|
||||
color: $alert;
|
||||
}
|
||||
}
|
||||
|
||||
&--mia {
|
||||
border-top: 6px solid $core-dark-blue-grey;
|
||||
|
||||
.host-details__status,
|
||||
.host-details__hostname {
|
||||
color: $core-dark-blue-grey;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
position: relative;
|
||||
border-bottom: solid 1px rgba(73, 143, 226, 0.16);
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
&__cta-host {
|
||||
@include position(absolute, 12px 5px null null);
|
||||
|
||||
span {
|
||||
display: block;
|
||||
color: $core-dark-blue-grey;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__cta-host-icon {
|
||||
color: $core-purple;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&__hostname {
|
||||
@include ellipsis(78%);
|
||||
font-size: 16px;
|
||||
line-height: 50px;
|
||||
font-weight: $bold;
|
||||
letter-spacing: -0.4px;
|
||||
text-align: center;
|
||||
clear: both;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__details-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
list-style: none;
|
||||
margin: 0 auto;
|
||||
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;
|
||||
color: #727083;
|
||||
padding: 0;
|
||||
margin: 5px 0 15px;
|
||||
text-align: center;
|
||||
|
||||
.kolidecon {
|
||||
color: $core-dark-blue-grey;
|
||||
font-size: 23px;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&--os {
|
||||
flex-basis: 49.5%;
|
||||
flex-grow: 1;
|
||||
border-right: solid 1px rgba(73, 143, 226, 0.16);
|
||||
}
|
||||
|
||||
&--osquery {
|
||||
flex-basis: 49.5%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&--cpu,
|
||||
&--memory,
|
||||
&--uptime {
|
||||
flex-basis: 36%;
|
||||
border-right: solid 1px rgba(73, 143, 226, 0.16);
|
||||
}
|
||||
|
||||
&--memory {
|
||||
flex-basis: 27%;
|
||||
}
|
||||
|
||||
&--uptime {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&--mac,
|
||||
&--ip {
|
||||
flex-basis: 100%;
|
||||
|
||||
.kolidecon {
|
||||
display: inline;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export default from './HostDetails';
|
@ -1,7 +1,7 @@
|
||||
.host-pagination {
|
||||
&__pager-wrap {
|
||||
flex-basis: 100%;
|
||||
margin: 50px 0 115px;
|
||||
margin: 16px 0 39px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@ -14,10 +14,11 @@
|
||||
.rc-pagination-prev,
|
||||
.rc-pagination-next,
|
||||
.rc-pagination-item {
|
||||
border: 1px solid $core-medium-blue-grey;
|
||||
font-size: 15px;
|
||||
border: 0;
|
||||
font-size: $x-small;
|
||||
margin-right: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
|
||||
a {
|
||||
color: $core-medium-blue-grey;
|
||||
@ -28,8 +29,7 @@
|
||||
.rc-pagination-prev {
|
||||
a {
|
||||
&::after {
|
||||
@extend %kolidecon;
|
||||
content: '\f006';
|
||||
content: 'Prev';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -37,8 +37,7 @@
|
||||
.rc-pagination-next {
|
||||
a {
|
||||
&::after {
|
||||
@extend %kolidecon;
|
||||
content: '\f008';
|
||||
content: 'Next';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,10 @@ import classnames from 'classnames';
|
||||
|
||||
import Button from 'components/buttons/Button';
|
||||
import Icon from 'components/icons/Icon';
|
||||
import PlatformIcon from 'components/icons/PlatformIcon';
|
||||
import hostInterface from 'interfaces/host';
|
||||
import iconClassForLabel from 'utilities/icon_class_for_label';
|
||||
|
||||
import { humanLastSeen } from '../HostDetails/helpers';
|
||||
import { humanMemory, humanUptime, humanLastSeen } from './helpers';
|
||||
|
||||
const baseClass = 'hosts-table';
|
||||
|
||||
@ -67,13 +66,13 @@ class HostsTable extends Component {
|
||||
<td className={statusClassName}>
|
||||
<Icon name={iconClassForLabel(host.status)} />
|
||||
</td>
|
||||
<td>
|
||||
<PlatformIcon name={host.platform} title={host.os_version} />{' '}
|
||||
{host.os_version}
|
||||
</td>
|
||||
<td>{host.os_version}</td>
|
||||
<td>{host.osquery_version}</td>
|
||||
<td>{host.primary_ip}</td>
|
||||
<td>{host.primary_mac}</td>
|
||||
<td>{host.host_cpu}</td>
|
||||
<td>{humanMemory(host.memory)}</td>
|
||||
<td>{humanUptime(host.uptime)}</td>
|
||||
<td>
|
||||
<ActionButton
|
||||
host={host}
|
||||
@ -100,6 +99,9 @@ class HostsTable extends Component {
|
||||
<th>Osquery</th>
|
||||
<th>IPv4</th>
|
||||
<th>Physical Address</th>
|
||||
<th>CPU</th>
|
||||
<th>Memory</th>
|
||||
<th>Uptime</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -3,14 +3,13 @@
|
||||
border: solid 1px $ui-borders;
|
||||
border-radius: 6px;
|
||||
margin-top: $pad-base;
|
||||
overflow: hidden;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
&__table {
|
||||
border-collapse: collapse;
|
||||
color: $core-black;
|
||||
font-size: $x-small;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
tr {
|
||||
@ -25,15 +24,18 @@
|
||||
background-color: $core-light-blue-grey;
|
||||
color: $core-black;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid $ui-borders;
|
||||
|
||||
th {
|
||||
padding: $pad-small $pad-xsmall;
|
||||
padding: 18px 27px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
td {
|
||||
padding: $pad-xsmall;
|
||||
padding: 12px 27px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
font-size: $small;
|
||||
font-weight: $bold;
|
||||
background-color: $core-medium-blue-grey;
|
||||
padding: 9px 75px;
|
||||
padding: 9px 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
|
@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
@include breakpoint(smalldesk) {
|
||||
margin: 10px 5px 10px;
|
||||
margin: 10px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import AceEditor from 'react-ace';
|
||||
import { connect } from 'react-redux';
|
||||
import { push } from 'react-router-redux';
|
||||
import { sortBy } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AddHostModal from 'components/hosts/AddHostModal';
|
||||
import Button from 'components/buttons/Button';
|
||||
@ -12,10 +11,8 @@ import configInterface from 'interfaces/config';
|
||||
import HostContainer from 'components/hosts/HostContainer';
|
||||
import HostPagination from 'components/hosts/HostPagination';
|
||||
import HostSidePanel from 'components/side_panels/HostSidePanel';
|
||||
import Icon from 'components/icons/Icon';
|
||||
import LabelForm from 'components/forms/LabelForm';
|
||||
import Modal from 'components/modals/Modal';
|
||||
import PlatformIcon from 'components/icons/PlatformIcon';
|
||||
import QuerySidePanel from 'components/side_panels/QuerySidePanel';
|
||||
import labelInterface from 'interfaces/label';
|
||||
import hostInterface from 'interfaces/host';
|
||||
@ -25,7 +22,6 @@ import enrollSecretInterface from 'interfaces/enroll_secret';
|
||||
import { selectOsqueryTable } from 'redux/nodes/components/QueryPages/actions';
|
||||
import {
|
||||
getStatusLabelCounts,
|
||||
setDisplay,
|
||||
setPagination,
|
||||
} from 'redux/nodes/components/ManageHostsPage/actions';
|
||||
import hostActions from 'redux/nodes/entities/hosts/actions';
|
||||
@ -34,8 +30,6 @@ import { renderFlash } from 'redux/nodes/notifications/actions';
|
||||
import entityGetter from 'redux/utilities/entityGetter';
|
||||
import PATHS from 'router/paths';
|
||||
import deepDifference from 'utilities/deep_difference';
|
||||
import iconClassForLabel from 'utilities/icon_class_for_label';
|
||||
import platformIconClass from 'utilities/platform_icon_class';
|
||||
import scrollToTop from 'utilities/scroll_to_top';
|
||||
import helpers from './helpers';
|
||||
|
||||
@ -46,7 +40,6 @@ export class ManageHostsPage extends PureComponent {
|
||||
static propTypes = {
|
||||
config: configInterface,
|
||||
dispatch: PropTypes.func,
|
||||
display: PropTypes.oneOf(['Grid', 'List']),
|
||||
hosts: PropTypes.arrayOf(hostInterface),
|
||||
isAddLabel: PropTypes.bool,
|
||||
labelErrors: PropTypes.shape({
|
||||
@ -65,7 +58,6 @@ export class ManageHostsPage extends PureComponent {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
display: 'Grid',
|
||||
page: 1,
|
||||
perPage: 100,
|
||||
loadingHosts: false,
|
||||
@ -201,16 +193,6 @@ export class ManageHostsPage extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
onToggleDisplay = (event) => {
|
||||
event.preventDefault();
|
||||
const { dispatch } = this.props;
|
||||
const value = event.currentTarget.dataset.value;
|
||||
|
||||
dispatch(setDisplay(value));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onDeleteLabel = () => {
|
||||
const { toggleDeleteLabelModal } = this;
|
||||
const { dispatch, selectedLabel } = this.props;
|
||||
@ -378,16 +360,6 @@ export class ManageHostsPage extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
renderIcon = () => {
|
||||
const { selectedLabel } = this.props;
|
||||
|
||||
if (platformIconClass(selectedLabel.display_text)) {
|
||||
return <PlatformIcon name={platformIconClass(selectedLabel.display_text)} title={platformIconClass(selectedLabel.display_text)} />;
|
||||
}
|
||||
|
||||
return <Icon name={iconClassForLabel(selectedLabel)} />;
|
||||
}
|
||||
|
||||
renderQuery = () => {
|
||||
const { selectedLabel } = this.props;
|
||||
const { slug, label_type: labelType, label_membership_type: membershipType, query } = selectedLabel;
|
||||
@ -422,51 +394,32 @@ export class ManageHostsPage extends PureComponent {
|
||||
}
|
||||
|
||||
renderHeader = () => {
|
||||
const { renderIcon, renderQuery, renderDeleteButton } = this;
|
||||
const { display, isAddLabel, selectedLabel, statusLabels } = this.props;
|
||||
const { renderQuery, renderDeleteButton } = this;
|
||||
const { isAddLabel, selectedLabel, statusLabels } = this.props;
|
||||
|
||||
if (!selectedLabel || isAddLabel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { count, description, display_text: displayText, statusLabelKey, type } = selectedLabel;
|
||||
const { onToggleDisplay } = this;
|
||||
|
||||
const hostCount = type === 'status' ? statusLabels[`${statusLabelKey}`] : count;
|
||||
const hostsTotalDisplay = hostCount === 1 ? '1 Host Total' : `${hostCount} Hosts Total`;
|
||||
const hostsTotalDisplay = hostCount === 1 ? '1 host' : `${hostCount} hosts`;
|
||||
const defaultDescription = 'No description available.';
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__header`}>
|
||||
{renderDeleteButton()}
|
||||
<h1 className={`${baseClass}__title`}>
|
||||
{renderIcon()}
|
||||
<span>{displayText}</span>
|
||||
</h1>
|
||||
{renderQuery()}
|
||||
<div className={`${baseClass}__description`}>
|
||||
<h2>Description</h2>
|
||||
<p>{description || <em>{defaultDescription}</em>}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__topper`}>
|
||||
<p className={`${baseClass}__host-count`}>{hostsTotalDisplay}</p>
|
||||
<a
|
||||
onClick={onToggleDisplay}
|
||||
className={classNames(`${baseClass}__toggle-view`, {
|
||||
[`${baseClass}__toggle-view--active`]: display === 'List',
|
||||
})}
|
||||
data-value="List"
|
||||
>{<svg viewBox="0 0 24 24" className="kolidecon"><path d="M9 4h11a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1zm0 7h11a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-1a1 1 0 0 1 1-1zm0 7h11a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-1a1 1 0 0 1 1-1zm-4.5 3a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0-7a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0-7a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z" fillRule="evenodd" /></svg>}
|
||||
</a>
|
||||
<a
|
||||
onClick={onToggleDisplay}
|
||||
className={classNames(`${baseClass}__toggle-view`, {
|
||||
[`${baseClass}__toggle-view--active`]: display === 'Grid',
|
||||
})}
|
||||
data-value="Grid"
|
||||
>{<svg viewBox="0 0 24 24" className="kolidecon"><path d="M5 15v4h4v-4H5zm-1-2h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1zm11 2v4h4v-4h-4zm-1-2h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1zm1-8v4h4V5h-4zm-1-2h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM5 5v4h4V5H5zM4 3h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z" fillRule="nonzero" /></svg>}
|
||||
</a>
|
||||
</div>
|
||||
{renderQuery()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -566,7 +519,6 @@ export class ManageHostsPage extends PureComponent {
|
||||
page,
|
||||
perPage,
|
||||
hosts,
|
||||
display,
|
||||
isAddLabel,
|
||||
loadingLabels,
|
||||
loadingHosts,
|
||||
@ -609,11 +561,10 @@ export class ManageHostsPage extends PureComponent {
|
||||
{!isAddLabel && !isEditLabel &&
|
||||
<div className={`${baseClass} body-wrap`}>
|
||||
{renderHeader()}
|
||||
<div className={`${baseClass}__list ${baseClass}__list--${display.toLowerCase()}`}>
|
||||
<div className={`${baseClass}__list`}>
|
||||
<HostContainer
|
||||
hosts={sortedHosts}
|
||||
selectedLabel={selectedLabel}
|
||||
displayType={display}
|
||||
loadingHosts={loadingHosts}
|
||||
toggleAddHostModal={toggleAddHostModal}
|
||||
toggleDeleteHostModal={toggleDeleteHostModal}
|
||||
@ -643,7 +594,7 @@ const mapStateToProps = (state, { location, params }) => {
|
||||
const activeLabelSlug = activeLabel || 'all-hosts';
|
||||
const selectedFilter = labelID ? `labels/${labelID}` : activeLabelSlug;
|
||||
|
||||
const { display, status_labels: statusLabels, page, perPage } = state.components.ManageHostsPage;
|
||||
const { status_labels: statusLabels, page, perPage } = state.components.ManageHostsPage;
|
||||
const { entities: hosts } = entityGetter(state).get('hosts');
|
||||
const labelEntities = entityGetter(state).get('labels');
|
||||
const { entities: labels } = labelEntities;
|
||||
@ -662,7 +613,6 @@ const mapStateToProps = (state, { location, params }) => {
|
||||
selectedFilter,
|
||||
page,
|
||||
perPage,
|
||||
display,
|
||||
hosts,
|
||||
isAddLabel,
|
||||
labelErrors,
|
||||
|
@ -95,14 +95,14 @@ describe('ManageHostsPage - component', () => {
|
||||
const oneHostLabel = { ...allHostsLabel, count: 1 };
|
||||
const page = mount(<ManageHostsPage {...props} selectedLabel={oneHostLabel} />);
|
||||
|
||||
expect(page.text()).toInclude('1 Host Total');
|
||||
expect(page.text()).toInclude('1 host');
|
||||
});
|
||||
|
||||
it('displays "#{count} Hosts Total" when there are more than 1 host', () => {
|
||||
const oneHostLabel = { ...allHostsLabel, count: 2 };
|
||||
const page = mount(<ManageHostsPage {...props} selectedLabel={oneHostLabel} />);
|
||||
|
||||
expect(page.text()).toInclude('2 Hosts Total');
|
||||
expect(page.text()).toInclude('2 hosts');
|
||||
});
|
||||
});
|
||||
|
||||
@ -267,7 +267,7 @@ describe('ManageHostsPage - component', () => {
|
||||
const ownProps = { location: {}, params: { active_label: 'all-hosts' } };
|
||||
const component = connectedComponent(ConnectedManageHostsPage, { props: ownProps, mockStore });
|
||||
const page = mount(component);
|
||||
const deleteBtn = page.find('HostDetails').last().find('Button');
|
||||
const deleteBtn = page.find('ActionButton').last().find('Button');
|
||||
|
||||
spyOn(hostActions, 'destroy').andReturn((dispatch) => {
|
||||
dispatch({ type: 'hosts_LOAD_REQUEST' });
|
||||
|
@ -1,5 +1,5 @@
|
||||
.manage-hosts {
|
||||
padding: $pad-base;
|
||||
padding: 50px 30px 0;
|
||||
min-height: 90vh;
|
||||
|
||||
&__header {
|
||||
@ -9,6 +9,8 @@
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: $large;
|
||||
|
||||
.kolidecon {
|
||||
color: $core-medium-blue-grey;
|
||||
margin-right: 15px;
|
||||
@ -45,7 +47,7 @@
|
||||
&__description {
|
||||
line-height: 1.54;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0 0 15px;
|
||||
margin: 0 0 16px;
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
@ -65,7 +67,6 @@
|
||||
&__topper {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
border-bottom: solid 1px $ui-medium-grey;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ import labelActions from 'redux/nodes/entities/labels/actions';
|
||||
export const GET_STATUS_LABEL_COUNTS_FAILURE = 'GET_STATUS_LABEL_COUNTS_FAILURE';
|
||||
export const GET_STATUS_LABEL_COUNTS_SUCCESS = 'GET_STATUS_LABEL_COUNTS_SUCCESS';
|
||||
export const LOAD_STATUS_LABEL_COUNTS = 'LOAD_STATUS_LABEL_COUNTS';
|
||||
export const SET_DISPLAY = 'SET_DISPLAY';
|
||||
export const SET_PAGINATION = 'SET_PAGINATION';
|
||||
|
||||
// Actions
|
||||
@ -80,13 +79,4 @@ export const setPagination = (page, perPage, selectedLabel) => (dispatch) => {
|
||||
Promise.all(promises).then(dispatch(setPaginationSuccess(page, perPage, selectedLabel)));
|
||||
};
|
||||
|
||||
export const setDisplay = (display) => {
|
||||
return {
|
||||
type: SET_DISPLAY,
|
||||
payload: {
|
||||
display,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default { getStatusLabelCounts, setDisplay, setPagination, silentGetStatusLabelCounts };
|
||||
export default { getStatusLabelCounts, setPagination, silentGetStatusLabelCounts };
|
||||
|
@ -2,12 +2,10 @@ import {
|
||||
GET_STATUS_LABEL_COUNTS_FAILURE,
|
||||
GET_STATUS_LABEL_COUNTS_SUCCESS,
|
||||
LOAD_STATUS_LABEL_COUNTS,
|
||||
SET_DISPLAY,
|
||||
SET_PAGINATION,
|
||||
} from './actions';
|
||||
|
||||
export const initialState = {
|
||||
display: 'Grid',
|
||||
page: 1,
|
||||
perPage: 100,
|
||||
status_labels: {
|
||||
@ -48,11 +46,6 @@ export default (state = initialState, { type, payload }) => {
|
||||
loading_counts: true,
|
||||
},
|
||||
};
|
||||
case SET_DISPLAY:
|
||||
return {
|
||||
...state,
|
||||
display: payload.display,
|
||||
};
|
||||
case SET_PAGINATION:
|
||||
return {
|
||||
...state,
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
getStatusLabelCountsFailure,
|
||||
getStatusLabelCountsSuccess,
|
||||
loadStatusLabelCounts,
|
||||
setDisplay,
|
||||
silentGetStatusLabelCounts,
|
||||
} from './actions';
|
||||
import reducer, { initialState } from './reducer';
|
||||
@ -19,15 +18,6 @@ describe('ManageHostsPage - reducer', () => {
|
||||
expect(reducer(undefined, { type: 'SOME_ACTION' })).toEqual(initialState);
|
||||
});
|
||||
|
||||
describe('#setDisplay', () => {
|
||||
it('sets the display in state', () => {
|
||||
expect(reducer(initialState, setDisplay('List'))).toEqual({
|
||||
...initialState,
|
||||
display: 'List',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getStatusLabelCounts', () => {
|
||||
it('sets the loading boolean', () => {
|
||||
expect(reducer(initialState, loadStatusLabelCounts)).toEqual({
|
||||
|
@ -71,14 +71,16 @@ a {
|
||||
.has-sidebar & {
|
||||
margin-right: 0;
|
||||
min-width: 610px;
|
||||
max-width: calc(100vw - #{$nav-width} - #{$pad-base} - #{$pad-base} - #{$sidepanel-width});
|
||||
|
||||
// 62px includes
|
||||
max-width: calc(100vw - #{$nav-width} - #{$pad-base} - #{$pad-base} - #{$pad-body} - #{$pad-body} - #{$pad-borders} - #{$sidepanel-width});
|
||||
|
||||
@at-root .core-wrapper--small & {
|
||||
max-width: calc(100vw - #{$nav-tablet-width} - #{$pad-base} - #{$pad-base} - #{$sidepanel-tablet-width});
|
||||
max-width: calc(100vw - #{$nav-tablet-width} - #{$pad-base} - #{$pad-base} - #{$pad-body} - #{$pad-body} - #{pad-borders} #{$sidepanel-tablet-width});
|
||||
}
|
||||
|
||||
@include breakpoint(smalldesk) {
|
||||
max-width: calc(100vw - #{$nav-tablet-width} - #{$pad-base} - #{$pad-base} - #{$sidepanel-tablet-width});
|
||||
max-width: calc(100vw - #{$nav-tablet-width} - #{$pad-base} - #{$pad-base} - #{$pad-body} - #{$pad-body} - #{pad-borders} - #{$sidepanel-tablet-width});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,3 +7,5 @@ $pad-large: px-to-rem(24);
|
||||
$pad-half: px-to-rem(9);
|
||||
$pad-none: 0;
|
||||
$pad-most: px-to-rem(40);
|
||||
$pad-body: px-to-rem(30);
|
||||
$pad-borders: px-to-rem(2);
|
||||
|
Loading…
Reference in New Issue
Block a user