Hosts Pagination (#1594)

This commit is contained in:
Kyle Knight 2017-11-07 11:54:56 -06:00 committed by Mike Arpaia
parent 0ad4caa95c
commit 78b831a6d2
16 changed files with 773 additions and 229 deletions

View File

@ -0,0 +1,99 @@
import React, { Component, PropTypes } from 'react';
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';
const baseClass = 'host-container';
class HostContainer extends Component {
static propTypes = {
hosts: PropTypes.arrayOf(hostInterface),
selectedLabel: labelInterface,
loadingHosts: PropTypes.bool.isRequired,
displayType: PropTypes.oneOf(['Grid', 'List']),
toggleAddHostModal: PropTypes.func,
toggleDeleteHostModal: PropTypes.func,
onQueryHost: PropTypes.func,
};
renderNoHosts = () => {
const { selectedLabel } = this.props;
const { type } = selectedLabel || '';
const isCustom = type === 'custom';
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-contact`}>
<p>Still having trouble? Want to talk to a human?</p>
<p>Contact Kolide Support:</p>
<p><a href="mailto:support@kolide.co">support@kolide.co</a></p>
</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}
/>
);
});
}
return (
<HostsTable
hosts={hosts}
onDestroyHost={toggleDeleteHostModal}
onQueryHost={onQueryHost}
/>
);
}
render () {
const { renderHosts, renderNoHosts } = this;
const { hosts, displayType, loadingHosts, selectedLabel, toggleAddHostModal } = this.props;
if (loadingHosts) {
return <Spinner />;
}
if (hosts.length === 0) {
if (selectedLabel && selectedLabel.type === 'all') {
return <LonelyHost onClick={toggleAddHostModal} />;
}
return renderNoHosts();
}
return (
<div className={`${baseClass} ${baseClass}--${displayType.toLowerCase()}`}>
{renderHosts()}
</div>
);
}
}
export default HostContainer;

View File

@ -0,0 +1,60 @@
import React from 'react';
import expect from 'expect';
import { noop } from 'lodash';
import { mount } from 'enzyme';
import { hostStub } from 'test/stubs';
import HostContainer from './HostContainer';
const allHostsLabel = { id: 1, display_text: 'All Hosts', slug: 'all-hosts', type: 'all', count: 22 };
const customLabel = { id: 6, display_text: 'Custom Label', slug: 'custom-label', type: 'custom', count: 3 };
describe('HostsContainer - component', () => {
const props = {
hosts: [hostStub],
selectedLabel: allHostsLabel,
loadingHosts: false,
displayType: 'Grid',
toggleAddHostModal: noop,
toggleDeleteHostModal: noop,
onQueryHost: noop,
};
it('renders Spinner while hosts are loading', () => {
const loadingProps = { ...props, loadingHosts: true };
const page = mount(<HostContainer {...loadingProps} hosts={[]} selectedLabel={allHostsLabel} />);
expect(page.find('Spinner').length).toEqual(1);
});
it('render LonelyHost if no hosts available', () => {
const page = mount(<HostContainer {...props} hosts={[]} selectedLabel={allHostsLabel} />);
expect(page.find('LonelyHost').length).toEqual(1);
});
it('renders message if no hosts available and not on All Hosts', () => {
const page = mount(<HostContainer {...props} hosts={[]} selectedLabel={customLabel} />);
expect(page.find('.host-container--no-hosts').length).toEqual(1);
});
it('renders hosts as HostDetails by default', () => {
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);
});
it('does not render sidebar if labels are loading', () => {
const loadingProps = { ...props, loadingLabels: true };
const page = mount(<HostContainer {...loadingProps} hosts={[]} selectedLabel={allHostsLabel} />);
expect(page.find('HostSidePanel').length).toEqual(0);
});
});

View File

@ -0,0 +1,49 @@
.host-container {
&--no-hosts {
width: 440px;
margin: 35px auto;
font-size: 15px;
font-weight: $normal;
line-height: 2;
letter-spacing: normal;
color: rgba(32, 37, 50, 0.66);
h1 {
font-size: 32px;
font-weight: $normal;
line-height: normal;
letter-spacing: normal;
color: #48c586;
}
h2 {
font-size: 16px;
font-weight: $bold;
line-height: 1.5;
letter-spacing: -0.5px;
color: rgba(32, 37, 50, 0.66);
}
ul {
margin: 0;
padding-left: 20px;
}
}
&__no-hosts-contact {
text-align: right;
margin-top: 30px;
p {
margin: 0;
}
}
&--grid {
@include display(flex);
@include justify-content(space-around);
@include flex-wrap(wrap);
@include align-content(center);
margin: 0 auto;
}
}

View File

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

View File

@ -0,0 +1,70 @@
import React, { PureComponent, PropTypes } from 'react';
import Pagination from 'rc-pagination';
import Select from 'react-select';
import 'rc-pagination/assets/index.css';
import enUs from 'rc-pagination/lib/locale/en_US';
const baseClass = 'host-pagination';
class HostPagination extends PureComponent {
static propTypes = {
allHostCount: PropTypes.number,
currentPage: PropTypes.number,
hostsPerPage: PropTypes.number,
onPaginationChange: PropTypes.func,
onPerPageChange: PropTypes.func,
};
render () {
const {
allHostCount,
currentPage,
hostsPerPage,
onPaginationChange,
onPerPageChange,
} = this.props;
const paginationSelectOpts = [
{ value: 20, label: '20' },
{ value: 100, label: '100' },
{ value: 500, label: '500' },
{ value: 1000, label: '1,000' },
];
const humanPage = currentPage + 1;
const startRange = (currentPage * hostsPerPage) + 1;
const endRange = Math.min(humanPage * hostsPerPage, allHostCount);
if (allHostCount === 0) {
return false;
}
return (
<div className={`${baseClass}__pager-wrap`}>
<Pagination
onChange={onPaginationChange}
current={humanPage}
total={allHostCount}
pageSize={hostsPerPage}
className={`${baseClass}__pagination`}
locale={enUs}
showLessItems
/>
<p className={`${baseClass}__pager-range`}>{`${startRange} - ${endRange} of ${allHostCount} hosts`}</p>
<div className={`${baseClass}__pager-count`}>
<Select
name="pager-host-count"
value={hostsPerPage}
options={paginationSelectOpts}
onChange={onPerPageChange}
className={`${baseClass}__count-select`}
clearable={false}
/> <span>Hosts per page</span>
</div>
</div>
);
}
}
export default HostPagination;

View File

@ -0,0 +1,132 @@
.host-pagination {
&__pager-wrap {
flex-basis: 100%;
margin: 50px 0 115px;
display: flex;
flex-wrap: wrap;
}
&__pagination {
margin: 0 25px 0 0;
padding: 0;
font-family: 'Oxygen', 'Helvetica Neue', 'Helvetica', 'Roboto', 'Arial', sans-serif;
.rc-pagination-prev,
.rc-pagination-next,
.rc-pagination-item {
border: 1px solid $success;
font-size: 15px;
box-sizing: border-box;
border-radius: 3px;
a {
color: $success;
font-weight: bold;
}
}
.rc-pagination-prev {
a {
&::after {
@extend %kolidecon;
content: '\f006';
}
}
}
.rc-pagination-next {
a {
&::after {
@extend %kolidecon;
content: '\f008';
}
}
}
.rc-pagination-item {
border: 0;
&:hover {
background-color: $success-light;
a {
color: $white;
}
}
}
.rc-pagination-jump-prev,
.rc-pagination-jump-next {
&::after {
color: $success;
}
&:hover {
&::after {
font-size: 22px;
line-height: 20px;
}
}
}
.rc-pagination-disabled {
opacity: 0.25;
}
.rc-pagination-item-active {
background-color: $success;
a {
color: $white;
}
}
}
&__pager-count {
flex-basis: 100%;
margin: 15px 0 0;
display: flex;
}
&__count-select {
width: 90px;
margin-right: 15px;
.Select-control {
border-color: $success;
border-radius: 3px;
height: 30px;
@at-root .Select.is-focused#{&} {
border-color: $success;
}
}
.Select-value {
box-shadow: none;
}
&.Select--single {
> .Select-control {
.Select-value {
line-height: 30px;
border: 0;
}
}
}
.Select-input {
height: 30px;
}
.Select-option {
&.is-focused {
background-color: $success;
}
}
}
&__pager-range {
margin: 0;
}
}

View File

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

View File

@ -4,8 +4,6 @@
border-radius: 3px; border-radius: 3px;
box-shadow: inset 0 0 8px 0 rgba(0, 0, 0, 0.12); box-shadow: inset 0 0 8px 0 rgba(0, 0, 0, 0.12);
margin-top: $pad-base; margin-top: $pad-base;
max-height: 85vh;
overflow: scroll;
} }
&__table { &__table {

View File

@ -2,38 +2,37 @@ import React, { Component, PropTypes } from 'react';
import AceEditor from 'react-ace'; import AceEditor from 'react-ace';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import { orderBy, sortBy } from 'lodash';
import { push } from 'react-router-redux'; import { push } from 'react-router-redux';
import { isEqual, orderBy, slice, sortBy } from 'lodash';
import deepDifference from 'utilities/deep_difference'; import Kolide from 'kolide';
import entityGetter from 'redux/utilities/entityGetter'; import AddHostModal from 'components/hosts/AddHostModal';
import Button from 'components/buttons/Button';
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 Rocker from 'components/buttons/Rocker';
import labelInterface from 'interfaces/label';
import hostInterface from 'interfaces/host';
import osqueryTableInterface from 'interfaces/osquery_table';
import statusLabelsInterface from 'interfaces/status_labels';
import { selectOsqueryTable } from 'redux/nodes/components/QueryPages/actions';
import { getStatusLabelCounts, setDisplay, silentGetStatusLabelCounts } from 'redux/nodes/components/ManageHostsPage/actions'; import { getStatusLabelCounts, setDisplay, silentGetStatusLabelCounts } from 'redux/nodes/components/ManageHostsPage/actions';
import helpers from 'pages/hosts/ManageHostsPage/helpers';
import hostActions from 'redux/nodes/entities/hosts/actions'; import hostActions from 'redux/nodes/entities/hosts/actions';
import labelActions from 'redux/nodes/entities/labels/actions'; import labelActions from 'redux/nodes/entities/labels/actions';
import labelInterface from 'interfaces/label';
import HostDetails from 'components/hosts/HostDetails';
import hostInterface from 'interfaces/host';
import HostSidePanel from 'components/side_panels/HostSidePanel';
import HostsTable from 'components/hosts/HostsTable';
import LabelForm from 'components/forms/LabelForm';
import LonelyHost from 'components/hosts/LonelyHost';
import AddHostModal from 'components/hosts/AddHostModal';
import Icon from 'components/icons/Icon';
import Spinner from 'components/loaders/Spinner';
import Kolide from 'kolide';
import PlatformIcon from 'components/icons/PlatformIcon';
import osqueryTableInterface from 'interfaces/osquery_table';
import paths from 'router/paths';
import QuerySidePanel from 'components/side_panels/QuerySidePanel';
import { renderFlash } from 'redux/nodes/notifications/actions'; import { renderFlash } from 'redux/nodes/notifications/actions';
import Rocker from 'components/buttons/Rocker'; import entityGetter from 'redux/utilities/entityGetter';
import Button from 'components/buttons/Button'; import paths from 'router/paths';
import Modal from 'components/modals/Modal'; import deepDifference from 'utilities/deep_difference';
import { selectOsqueryTable } from 'redux/nodes/components/QueryPages/actions';
import statusLabelsInterface from 'interfaces/status_labels';
import iconClassForLabel from 'utilities/icon_class_for_label'; import iconClassForLabel from 'utilities/icon_class_for_label';
import platformIconClass from 'utilities/platform_icon_class'; import platformIconClass from 'utilities/platform_icon_class';
import scrollToTop from 'utilities/scroll_to_top';
import helpers from './helpers';
const NEW_LABEL_HASH = '#new_label'; const NEW_LABEL_HASH = '#new_label';
const baseClass = 'manage-hosts'; const baseClass = 'manage-hosts';
@ -66,12 +65,18 @@ export class ManageHostsPage extends Component {
super(props); super(props);
this.state = { this.state = {
allHostCount: 0,
currentPaginationPage: 0,
hostsLoading: false,
hostsPerPage: 20,
isEditLabel: false, isEditLabel: false,
labelQueryText: '', labelQueryText: '',
pagedHosts: [],
showDeleteHostModal: false, showDeleteHostModal: false,
showAddHostModal: false, showAddHostModal: false,
selectedHost: null, selectedHost: null,
showDeleteLabelModal: false, showDeleteLabelModal: false,
showHostContainerSpinner: false,
}; };
} }
@ -79,6 +84,7 @@ export class ManageHostsPage extends Component {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(hostActions.loadAll()); dispatch(hostActions.loadAll());
this.buildSortedHosts();
return this.getEntities(); return this.getEntities();
} }
@ -91,6 +97,15 @@ export class ManageHostsPage extends Component {
return false; return false;
} }
shouldComponentUpdate (nextProps, nextState) {
if (isEqual(nextProps, this.props) && isEqual(nextState, this.state)) {
return false;
}
this.buildSortedHosts(nextProps, nextState);
return true;
}
componentWillUnmount () { componentWillUnmount () {
if (this.interval) { if (this.interval) {
global.window.clearInterval(this.interval); global.window.clearInterval(this.interval);
@ -181,6 +196,11 @@ export class ManageHostsPage extends Component {
dispatch(push(nextLocation)); dispatch(push(nextLocation));
this.setState({
currentPaginationPage: 0,
hostsLoading: true,
});
return false; return false;
}; };
} }
@ -193,6 +213,30 @@ export class ManageHostsPage extends Component {
return false; return false;
} }
onPaginationChange = (page) => {
this.setState({
currentPaginationPage: page - 1,
hostsLoading: true,
});
scrollToTop();
return true;
}
onPerPageChange = (option) => {
this.setState({
currentPaginationPage: 0,
hostsPerPage: Number(option.value),
hostsLoading: true,
showHostContainerSpinner: true,
});
scrollToTop();
return true;
}
onSaveAddLabel = (formData) => { onSaveAddLabel = (formData) => {
const { dispatch } = this.props; const { dispatch } = this.props;
@ -256,6 +300,38 @@ export class ManageHostsPage extends Component {
return false; return false;
} }
buildSortedHosts = (nextProps, nextState) => {
const { filterAllHosts, sortHosts } = this;
const { currentPaginationPage, hostsPerPage } = nextState || this.state;
const { hosts, selectedLabel } = this.props;
const sortedHosts = sortHosts(filterAllHosts(hosts, selectedLabel));
const fromIndex = currentPaginationPage * hostsPerPage;
const toIndex = fromIndex + hostsPerPage;
const pagedHosts = slice(sortedHosts, fromIndex, toIndex);
this.setState({
allHostCount: sortedHosts.length,
hostsLoading: false,
pagedHosts,
});
}
filterAllHosts = (hosts, selectedLabel) => {
const { filterHosts } = helpers;
return filterHosts(hosts, selectedLabel);
}
sortHosts = (hosts) => {
const alphaHosts = sortBy(hosts, (h) => { return h.hostname; });
const orderedHosts = orderBy(alphaHosts, 'status', 'desc');
return orderedHosts;
}
toggleAddHostModal = () => { toggleAddHostModal = () => {
const { showAddHostModal } = this.state; const { showAddHostModal } = this.state;
this.setState({ showAddHostModal: !showAddHostModal }); this.setState({ showAddHostModal: !showAddHostModal });
@ -290,19 +366,6 @@ export class ManageHostsPage extends Component {
return false; return false;
} }
filterHosts = () => {
const { hosts, selectedLabel } = this.props;
return helpers.filterHosts(hosts, selectedLabel);
}
sortHosts = (hosts) => {
const alphaHosts = sortBy(hosts, (h) => { return h.hostname; });
const orderedHosts = orderBy(alphaHosts, 'status', 'desc');
return orderedHosts;
}
renderAddHostModal = () => { renderAddHostModal = () => {
const { onFetchCertificate, toggleAddHostModal } = this; const { onFetchCertificate, toggleAddHostModal } = this;
const { showAddHostModal } = this.state; const { showAddHostModal } = this.state;
@ -473,79 +536,6 @@ export class ManageHostsPage extends Component {
); );
} }
renderNoHosts = () => {
const { selectedLabel } = this.props;
const { type } = selectedLabel || '';
const isCustom = type === 'custom';
return (
<div className={`${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-contact`}>
<p>Still having trouble? Want to talk to a human?</p>
<p>Contact Kolide Support:</p>
<p><a href="mailto:support@kolide.co">support@kolide.co</a></p>
</div>
</div>
);
}
renderHosts = () => {
const { display, isAddLabel, selectedLabel, loadingHosts } = this.props;
const { toggleDeleteHostModal, filterHosts, onQueryHost, sortHosts, renderNoHosts, toggleAddHostModal } = this;
if (isAddLabel) {
return false;
}
const filteredHosts = filterHosts();
const sortedHosts = sortHosts(filteredHosts);
if (loadingHosts) {
return <Spinner />;
}
if (sortedHosts.length === 0 && !loadingHosts) {
if (selectedLabel && selectedLabel.type === 'all') {
return <LonelyHost onClick={toggleAddHostModal} />;
}
return renderNoHosts();
}
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}
/>
);
});
}
return (
<HostsTable
hosts={sortedHosts}
onDestroyHost={toggleDeleteHostModal}
onQueryHost={onQueryHost}
/>
);
}
renderForm = () => { renderForm = () => {
const { isAddLabel, labelErrors, selectedLabel } = this.props; const { isAddLabel, labelErrors, selectedLabel } = this.props;
const { isEditLabel } = this.state; const { isEditLabel } = this.state;
@ -625,9 +615,21 @@ export class ManageHostsPage extends Component {
} }
render () { render () {
const { renderForm, renderHeader, renderHosts, renderSidePanel, renderAddHostModal, renderDeleteHostModal, renderDeleteLabelModal } = this; const {
const { display, isAddLabel, loadingLabels } = this.props; onQueryHost,
const { isEditLabel } = this.state; onPerPageChange,
onPaginationChange,
renderForm,
renderHeader,
renderSidePanel,
renderAddHostModal,
renderDeleteHostModal,
renderDeleteLabelModal,
toggleAddHostModal,
toggleDeleteHostModal,
} = this;
const { display, isAddLabel, loadingLabels, loadingHosts, selectedLabel } = this.props;
const { allHostCount, currentPaginationPage, hostsPerPage, hostsLoading, isEditLabel, pagedHosts } = this.state;
return ( return (
<div className="has-sidebar"> <div className="has-sidebar">
@ -637,7 +639,22 @@ export class ManageHostsPage extends Component {
<div className={`${baseClass} body-wrap`}> <div className={`${baseClass} body-wrap`}>
{renderHeader()} {renderHeader()}
<div className={`${baseClass}__list ${baseClass}__list--${display.toLowerCase()}`}> <div className={`${baseClass}__list ${baseClass}__list--${display.toLowerCase()}`}>
{renderHosts()} <HostContainer
hosts={pagedHosts}
selectedLabel={selectedLabel}
displayType={display}
loadingHosts={hostsLoading || loadingHosts}
toggleAddHostModal={toggleAddHostModal}
toggleDeleteHostModal={toggleDeleteHostModal}
onQueryHost={onQueryHost}
/>
{!(hostsLoading || loadingHosts) && <HostPagination
allHostCount={allHostCount}
currentPage={currentPaginationPage}
hostsPerPage={hostsPerPage}
onPaginationChange={onPaginationChange}
onPerPageChange={onPerPageChange}
/>}
</div> </div>
</div> </div>
} }

View File

@ -104,62 +104,7 @@ describe('ManageHostsPage - component', () => {
}); });
}); });
describe('host rendering', () => { describe('host filtering', () => {
it('renders Spinner while hosts are loading', () => {
const loadingProps = { ...props, loadingHosts: true };
const page = mount(<ManageHostsPage {...loadingProps} hosts={[]} selectedLabel={allHostsLabel} />);
expect(page.find('Spinner').length).toEqual(1);
});
it('does not render sidebar if labels are loading', () => {
const loadingProps = { ...props, loadingLabels: true };
const page = mount(<ManageHostsPage {...loadingProps} hosts={[]} selectedLabel={allHostsLabel} />);
expect(page.find('HostSidePanel').length).toEqual(0);
});
it('render LonelyHost if no hosts available', () => {
const page = mount(<ManageHostsPage {...props} hosts={[]} selectedLabel={allHostsLabel} />);
expect(page.find('LonelyHost').length).toEqual(1);
});
it('renders message if no hosts available and not on All Hosts', () => {
const page = mount(<ManageHostsPage {...props} hosts={[]} selectedLabel={customLabel} />);
expect(page.find('.manage-hosts__no-hosts').length).toEqual(1);
});
it('renders hosts as HostDetails by default', () => {
const page = mount(<ManageHostsPage {...props} hosts={[hostStub]} />);
expect(page.find('HostDetails').length).toEqual(1);
});
it('renders hosts as HostsTable when the display is "List"', () => {
const page = mount(<ManageHostsPage {...props} display="List" hosts={[hostStub]} />);
expect(page.find('HostsTable').length).toEqual(1);
});
it('toggles between displays', () => {
const ownProps = { location: {}, params: {} };
const component = connectedComponent(ConnectedManageHostsPage, { props: ownProps, mockStore });
const page = mount(component);
const button = page.find('Rocker').find('button');
const toggleDisplayAction = {
type: 'SET_DISPLAY',
payload: {
display: 'List',
},
};
button.simulate('click');
expect(mockStore.getActions()).toInclude(toggleDisplayAction);
});
it('filters hosts', () => { it('filters hosts', () => {
const allHostsLabelPageNode = mount( const allHostsLabelPageNode = mount(
<ManageHostsPage <ManageHostsPage
@ -176,8 +121,8 @@ describe('ManageHostsPage - component', () => {
/> />
).node; ).node;
expect(allHostsLabelPageNode.filterHosts()).toEqual([hostStub, offlineHost]); expect(allHostsLabelPageNode.filterAllHosts([hostStub, offlineHost], allHostsLabel)).toEqual([hostStub, offlineHost]);
expect(offlineHostsLabelPageNode.filterHosts()).toEqual([offlineHost]); expect(offlineHostsLabelPageNode.filterAllHosts([hostStub, offlineHost], offlineHostsLabel)).toEqual([offlineHost]);
}); });
}); });

View File

@ -93,56 +93,6 @@
} }
} }
&__list {
&--grid {
@include display(flex);
@include justify-content(space-around);
@include flex-wrap(wrap);
@include align-content(center);
margin: 0 auto;
}
}
&__no-hosts {
width: 440px;
margin: 35px auto;
font-size: 15px;
font-weight: $normal;
line-height: 2;
letter-spacing: normal;
color: rgba(32, 37, 50, 0.66);
h1 {
font-size: 32px;
font-weight: $normal;
line-height: normal;
letter-spacing: normal;
color: #48c586;
}
h2 {
font-size: 16px;
font-weight: $bold;
line-height: 1.5;
letter-spacing: -0.5px;
color: rgba(32, 37, 50, 0.66);
}
ul {
margin: 0;
padding-left: 20px;
}
}
&__no-hosts-contact {
text-align: right;
margin-top: 30px;
p {
margin: 0;
}
}
&__invite-modal { &__invite-modal {
.modal__header { .modal__header {
span { span {

View File

@ -1,8 +1,8 @@
import expect from 'expect'; import expect from 'expect';
import moment from 'moment'; import moment from 'moment';
import helpers from 'pages/hosts/ManageHostsPage/helpers';
import { hostStub, labelStub } from 'test/stubs'; import { hostStub, labelStub } from 'test/stubs';
import helpers from './helpers';
const macHost = { ...hostStub, id: 1, platform: 'darwin', status: 'mia' }; const macHost = { ...hostStub, id: 1, platform: 'darwin', status: 'mia' };
const ubuntuHost = { ...hostStub, id: 2, platform: 'ubuntu', status: 'offline' }; const ubuntuHost = { ...hostStub, id: 2, platform: 'ubuntu', status: 'offline' };

View File

@ -0,0 +1,14 @@
export const scrollToTop = () => {
const { window } = global;
const scrollStep = -window.scrollY / (500 / 15);
const scrollInterval = setInterval(() => {
if (window.scrollY !== 0) {
window.scrollBy(0, scrollStep);
} else {
clearInterval(scrollInterval);
}
}, 15);
};
export default scrollToTop;

169
package-lock.json generated Normal file
View File

@ -0,0 +1,169 @@
{
"name": "Kolide",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
"dev": true
},
"babel-runtime": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
"dev": true,
"requires": {
"core-js": "2.5.1",
"regenerator-runtime": "0.11.0"
}
},
"core-js": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz",
"integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=",
"dev": true
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"dev": true,
"requires": {
"iconv-lite": "0.4.19"
}
},
"fbjs": {
"version": "0.8.16",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz",
"integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=",
"dev": true,
"requires": {
"core-js": "1.2.7",
"isomorphic-fetch": "2.2.1",
"loose-envify": "1.3.1",
"object-assign": "4.1.1",
"promise": "7.3.1",
"setimmediate": "1.0.5",
"ua-parser-js": "0.7.17"
},
"dependencies": {
"core-js": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=",
"dev": true
},
"isomorphic-fetch": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
"dev": true,
"requires": {
"node-fetch": "1.7.3",
"whatwg-fetch": "2.0.3"
}
},
"whatwg-fetch": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz",
"integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=",
"dev": true
}
}
},
"iconv-lite": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==",
"dev": true
},
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
"dev": true
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
"dev": true
},
"loose-envify": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
"dev": true,
"requires": {
"js-tokens": "3.0.2"
}
},
"node-fetch": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
"dev": true,
"requires": {
"encoding": "0.1.12",
"is-stream": "1.1.0"
}
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
},
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"dev": true,
"requires": {
"asap": "2.0.6"
}
},
"prop-types": {
"version": "15.6.0",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz",
"integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=",
"dev": true,
"requires": {
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
"object-assign": "4.1.1"
}
},
"rc-pagination": {
"version": "1.12.10",
"resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-1.12.10.tgz",
"integrity": "sha512-Afoxbwf759ZPu3/W8/dr1HSXITvnFT4xm7EWI7DuJ94tvQ4mreBPDqRv6TfBJ6BWYbrwEpqpf7r8Zhz40yvgtA==",
"dev": true,
"requires": {
"babel-runtime": "6.26.0",
"prop-types": "15.6.0"
}
},
"regenerator-runtime": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz",
"integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==",
"dev": true
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
"dev": true
},
"ua-parser-js": {
"version": "0.7.17",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz",
"integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==",
"dev": true
}
}
}

View File

@ -44,6 +44,7 @@
"node-sass": "^4.5.0", "node-sass": "^4.5.0",
"normalizr": "^2.2.1", "normalizr": "^2.2.1",
"proxy-middleware": "^0.15.0", "proxy-middleware": "^0.15.0",
"rc-pagination": "^1.12.10",
"react": "^15.3.2", "react": "^15.3.2",
"react-ace": "^3.6.0", "react-ace": "^3.6.0",
"react-addons-css-transition-group": "^15.3.2", "react-addons-css-transition-group": "^15.3.2",
@ -112,8 +113,8 @@
"typescript-require": "^0.2.9-1", "typescript-require": "^0.2.9-1",
"url-loader": "0.5.8", "url-loader": "0.5.8",
"webpack": "2.2.1", "webpack": "2.2.1",
"webpack-notifier": "1.5.0",
"webpack-dev-middleware": "1.10.1", "webpack-dev-middleware": "1.10.1",
"webpack-hot-middleware": "2.17.1" "webpack-hot-middleware": "2.17.1",
"webpack-notifier": "1.5.0"
} }
} }

View File

@ -925,6 +925,13 @@ babel-register@^6.22.0, babel-register@^6.9.0:
mkdirp "^0.5.1" mkdirp "^0.5.1"
source-map-support "^0.4.2" source-map-support "^0.4.2"
babel-runtime@6.x:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.6.1, babel-runtime@^6.9.1: babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.6.1, babel-runtime@^6.9.1:
version "6.22.0" version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611"
@ -2365,6 +2372,18 @@ fbjs@^0.8.1, fbjs@^0.8.4:
setimmediate "^1.0.5" setimmediate "^1.0.5"
ua-parser-js "^0.7.9" ua-parser-js "^0.7.9"
fbjs@^0.8.16:
version "0.8.16"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
dependencies:
core-js "^1.0.0"
isomorphic-fetch "^2.1.1"
loose-envify "^1.0.0"
object-assign "^4.1.0"
promise "^7.1.1"
setimmediate "^1.0.5"
ua-parser-js "^0.7.9"
figures@^1.3.5: figures@^1.3.5:
version "1.7.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
@ -3631,7 +3650,7 @@ longest@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0: loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
dependencies: dependencies:
@ -4087,7 +4106,7 @@ oauth-sign@~0.8.1:
version "0.8.2" version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
object-assign@^4.0.1, object-assign@^4.1.0: object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -4607,6 +4626,14 @@ promise@^7.1.1:
dependencies: dependencies:
asap "~2.0.3" asap "~2.0.3"
prop-types@^15.5.7:
version "15.6.0"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
dependencies:
fbjs "^0.8.16"
loose-envify "^1.3.1"
object-assign "^4.1.1"
propagate@0.4.0: propagate@0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/propagate/-/propagate-0.4.0.tgz#f3fcca0a6fe06736a7ba572966069617c130b481" resolved "https://registry.yarnpkg.com/propagate/-/propagate-0.4.0.tgz#f3fcca0a6fe06736a7ba572966069617c130b481"
@ -4710,6 +4737,13 @@ raw-loader@0.5.1:
version "0.5.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
rc-pagination@^1.12.10:
version "1.12.10"
resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-1.12.10.tgz#212c24107fb0bea0ca86fbd6dcfda0ea709a45a2"
dependencies:
babel-runtime "6.x"
prop-types "^15.5.7"
rc@~1.1.6: rc@~1.1.6:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9" resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9"
@ -4960,6 +4994,10 @@ regenerator-runtime@^0.10.0:
version "0.10.1" version "0.10.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz#257f41961ce44558b18f7814af48c17559f9faeb" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz#257f41961ce44558b18f7814af48c17559f9faeb"
regenerator-runtime@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1"
regenerator-transform@0.9.8: regenerator-transform@0.9.8:
version "0.9.8" version "0.9.8"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.8.tgz#0f88bb2bc03932ddb7b6b7312e68078f01026d6c" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.8.tgz#0f88bb2bc03932ddb7b6b7312e68078f01026d6c"