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:
noahtalerman 2020-11-30 10:23:58 -08:00 committed by GitHub
parent 57950a9645
commit 3953afb0b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 93 additions and 588 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -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';

View File

@ -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>
);

View File

@ -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);
});

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);
});
});

View File

@ -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;
}
}
}
}

View File

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

View File

@ -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';
}
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;

View File

@ -22,7 +22,7 @@
}
@include breakpoint(smalldesk) {
margin: 10px 5px 10px;
margin: 10px 5px;
}
}

View File

@ -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,

View File

@ -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' });

View File

@ -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;
}

View File

@ -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 };

View File

@ -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,

View File

@ -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({

View File

@ -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});
}
}
}

View File

@ -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);