mirror of
https://github.com/empayre/fleet.git
synced 2024-11-07 17:28:54 +00:00
3953afb0b0
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.
631 lines
16 KiB
JavaScript
631 lines
16 KiB
JavaScript
import React, { PureComponent } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import AceEditor from 'react-ace';
|
|
import { connect } from 'react-redux';
|
|
import { push } from 'react-router-redux';
|
|
import { sortBy } from 'lodash';
|
|
|
|
import AddHostModal from 'components/hosts/AddHostModal';
|
|
import Button from 'components/buttons/Button';
|
|
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 LabelForm from 'components/forms/LabelForm';
|
|
import Modal from 'components/modals/Modal';
|
|
import QuerySidePanel from 'components/side_panels/QuerySidePanel';
|
|
import labelInterface from 'interfaces/label';
|
|
import hostInterface from 'interfaces/host';
|
|
import osqueryTableInterface from 'interfaces/osquery_table';
|
|
import statusLabelsInterface from 'interfaces/status_labels';
|
|
import enrollSecretInterface from 'interfaces/enroll_secret';
|
|
import { selectOsqueryTable } from 'redux/nodes/components/QueryPages/actions';
|
|
import {
|
|
getStatusLabelCounts,
|
|
setPagination,
|
|
} from 'redux/nodes/components/ManageHostsPage/actions';
|
|
import hostActions from 'redux/nodes/entities/hosts/actions';
|
|
import labelActions from 'redux/nodes/entities/labels/actions';
|
|
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 scrollToTop from 'utilities/scroll_to_top';
|
|
import helpers from './helpers';
|
|
|
|
const NEW_LABEL_HASH = '#new_label';
|
|
const baseClass = 'manage-hosts';
|
|
|
|
export class ManageHostsPage extends PureComponent {
|
|
static propTypes = {
|
|
config: configInterface,
|
|
dispatch: PropTypes.func,
|
|
hosts: PropTypes.arrayOf(hostInterface),
|
|
isAddLabel: PropTypes.bool,
|
|
labelErrors: PropTypes.shape({
|
|
base: PropTypes.string,
|
|
}),
|
|
labels: PropTypes.arrayOf(labelInterface),
|
|
loadingHosts: PropTypes.bool.isRequired,
|
|
loadingLabels: PropTypes.bool.isRequired,
|
|
enrollSecret: enrollSecretInterface,
|
|
selectedFilter: PropTypes.string,
|
|
selectedLabel: labelInterface,
|
|
selectedOsqueryTable: osqueryTableInterface,
|
|
statusLabels: statusLabelsInterface,
|
|
page: PropTypes.number,
|
|
perPage: PropTypes.number,
|
|
};
|
|
|
|
static defaultProps = {
|
|
page: 1,
|
|
perPage: 100,
|
|
loadingHosts: false,
|
|
loadingLabels: false,
|
|
};
|
|
|
|
constructor (props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
isEditLabel: false,
|
|
labelQueryText: '',
|
|
pagedHosts: [],
|
|
showDeleteHostModal: false,
|
|
showAddHostModal: false,
|
|
selectedHost: null,
|
|
showDeleteLabelModal: false,
|
|
showHostContainerSpinner: false,
|
|
};
|
|
}
|
|
|
|
componentDidMount () {
|
|
const { dispatch, page, perPage, selectedFilter } = this.props;
|
|
|
|
dispatch(setPagination(page, perPage, selectedFilter));
|
|
}
|
|
|
|
componentWillUnmount () {
|
|
this.clearHostUpdates();
|
|
return false;
|
|
}
|
|
|
|
onAddLabelClick = (evt) => {
|
|
evt.preventDefault();
|
|
|
|
const { dispatch } = this.props;
|
|
|
|
dispatch(push(`${PATHS.MANAGE_HOSTS}${NEW_LABEL_HASH}`));
|
|
|
|
return false;
|
|
}
|
|
|
|
onCancelAddLabel = () => {
|
|
const { dispatch } = this.props;
|
|
|
|
dispatch(push(PATHS.MANAGE_HOSTS));
|
|
|
|
return false;
|
|
}
|
|
|
|
onAddHostClick = (evt) => {
|
|
evt.preventDefault();
|
|
|
|
const { toggleAddHostModal } = this;
|
|
toggleAddHostModal();
|
|
|
|
return false;
|
|
}
|
|
|
|
onDestroyHost = (evt) => {
|
|
evt.preventDefault();
|
|
|
|
const { dispatch } = this.props;
|
|
const { selectedHost } = this.state;
|
|
|
|
dispatch(hostActions.destroy(selectedHost))
|
|
.then(() => {
|
|
this.toggleDeleteHostModal(null)();
|
|
|
|
dispatch(getStatusLabelCounts);
|
|
dispatch(renderFlash('success', `Host "${selectedHost.hostname}" was successfully deleted`));
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
onEditLabel = (formData) => {
|
|
const { dispatch, selectedLabel } = this.props;
|
|
const updateAttrs = deepDifference(formData, selectedLabel);
|
|
|
|
return dispatch(labelActions.update(selectedLabel, updateAttrs))
|
|
.then(() => {
|
|
this.toggleEditLabel();
|
|
|
|
return false;
|
|
})
|
|
.catch(() => false);
|
|
}
|
|
|
|
onLabelClick = (selectedLabel) => {
|
|
return (evt) => {
|
|
evt.preventDefault();
|
|
|
|
const { dispatch, perPage } = this.props;
|
|
const { MANAGE_HOSTS } = PATHS;
|
|
const { slug, type } = selectedLabel;
|
|
const nextLocation = type === 'all' ? MANAGE_HOSTS : `${MANAGE_HOSTS}/${slug}`;
|
|
|
|
dispatch(push(nextLocation));
|
|
|
|
dispatch(setPagination(1, perPage, selectedLabel.slug));
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
onOsqueryTableSelect = (tableName) => {
|
|
const { dispatch } = this.props;
|
|
|
|
dispatch(selectOsqueryTable(tableName));
|
|
|
|
return false;
|
|
}
|
|
|
|
onPaginationChange = (page) => {
|
|
const { dispatch, selectedFilter, perPage } = this.props;
|
|
|
|
dispatch(setPagination(page, perPage, selectedFilter));
|
|
|
|
scrollToTop();
|
|
|
|
return true;
|
|
}
|
|
|
|
onSaveAddLabel = (formData) => {
|
|
const { dispatch } = this.props;
|
|
|
|
return dispatch(labelActions.create(formData))
|
|
.then(() => {
|
|
dispatch(push(PATHS.MANAGE_HOSTS));
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
onDeleteLabel = () => {
|
|
const { toggleDeleteLabelModal } = this;
|
|
const { dispatch, selectedLabel } = this.props;
|
|
const { MANAGE_HOSTS } = PATHS;
|
|
|
|
return dispatch(labelActions.destroy(selectedLabel))
|
|
.then(() => {
|
|
toggleDeleteLabelModal();
|
|
dispatch(push(MANAGE_HOSTS));
|
|
return false;
|
|
});
|
|
}
|
|
|
|
onQueryHost = (host) => {
|
|
return (evt) => {
|
|
evt.preventDefault();
|
|
|
|
const { dispatch } = this.props;
|
|
const { NEW_QUERY } = PATHS;
|
|
|
|
dispatch(push({
|
|
pathname: NEW_QUERY,
|
|
query: { host_ids: [host.id] },
|
|
}));
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
clearHostUpdates () {
|
|
if (this.timeout) {
|
|
global.window.clearTimeout(this.timeout);
|
|
this.timeout = null;
|
|
}
|
|
}
|
|
|
|
filterAllHosts = (hosts, selectedLabel) => {
|
|
const { filterHosts } = helpers;
|
|
|
|
return filterHosts(hosts, selectedLabel);
|
|
}
|
|
|
|
sortHosts = (hosts) => {
|
|
return sortBy(hosts, (h) => { return h.hostname; });
|
|
}
|
|
|
|
toggleAddHostModal = () => {
|
|
const { showAddHostModal } = this.state;
|
|
this.setState({ showAddHostModal: !showAddHostModal });
|
|
return false;
|
|
}
|
|
|
|
toggleDeleteHostModal = (selectedHost) => {
|
|
return () => {
|
|
const { showDeleteHostModal } = this.state;
|
|
|
|
this.setState({
|
|
selectedHost,
|
|
showDeleteHostModal: !showDeleteHostModal,
|
|
});
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
toggleDeleteLabelModal = () => {
|
|
const { showDeleteLabelModal } = this.state;
|
|
|
|
this.setState({ showDeleteLabelModal: !showDeleteLabelModal });
|
|
return false;
|
|
}
|
|
|
|
toggleEditLabel = () => {
|
|
const { isEditLabel } = this.state;
|
|
|
|
this.setState({ isEditLabel: !isEditLabel });
|
|
|
|
return false;
|
|
}
|
|
|
|
renderAddHostModal = () => {
|
|
const { toggleAddHostModal } = this;
|
|
const { showAddHostModal } = this.state;
|
|
const { enrollSecret, config } = this.props;
|
|
|
|
if (!showAddHostModal) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
title="Add New Host"
|
|
onExit={toggleAddHostModal}
|
|
className={`${baseClass}__invite-modal`}
|
|
>
|
|
<AddHostModal
|
|
onReturnToApp={toggleAddHostModal}
|
|
enrollSecret={enrollSecret}
|
|
config={config}
|
|
/>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
renderDeleteHostModal = () => {
|
|
const { showDeleteHostModal, selectedHost } = this.state;
|
|
const { toggleDeleteHostModal, onDestroyHost } = this;
|
|
|
|
if (!showDeleteHostModal) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
title="Delete Host"
|
|
onExit={toggleDeleteHostModal(null)}
|
|
className={`${baseClass}__modal`}
|
|
>
|
|
<p>This action will delete the host <strong>{selectedHost.hostname}</strong> from your Fleet instance.</p>
|
|
<p>If the host comes back online it will automatically re-enroll. To prevent the host from re-enrolling please disable or uninstall osquery on the host.</p>
|
|
<div className={`${baseClass}__modal-buttons`}>
|
|
<Button onClick={onDestroyHost} variant="alert">Delete</Button>
|
|
<Button onClick={toggleDeleteHostModal(null)} variant="inverse">Cancel</Button>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
renderDeleteLabelModal = () => {
|
|
const { showDeleteLabelModal } = this.state;
|
|
const { toggleDeleteLabelModal, onDeleteLabel } = this;
|
|
|
|
if (!showDeleteLabelModal) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
title="Delete Label"
|
|
onExit={toggleDeleteLabelModal}
|
|
className={`${baseClass}_delete-label__modal`}
|
|
>
|
|
<p>Are you sure you wish to delete this label?</p>
|
|
<div className={`${baseClass}__modal-buttons`}>
|
|
<Button onClick={onDeleteLabel} variant="alert">Delete</Button>
|
|
<Button onClick={toggleDeleteLabelModal} variant="inverse">Cancel</Button>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
renderDeleteButton = () => {
|
|
const { toggleDeleteLabelModal, toggleEditLabel } = this;
|
|
const { selectedLabel: { type } } = this.props;
|
|
|
|
if (type !== 'custom') {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
<div className={`${baseClass}__delete-label`}>
|
|
<Button onClick={toggleEditLabel} variant="inverse">Edit</Button>
|
|
<Button onClick={toggleDeleteLabelModal} variant="alert">Delete</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
renderQuery = () => {
|
|
const { selectedLabel } = this.props;
|
|
const { slug, label_type: labelType, label_membership_type: membershipType, query } = selectedLabel;
|
|
|
|
if (membershipType === 'manual' && labelType !== 'builtin') {
|
|
return (
|
|
<h4 title="Manage manual labels with fleetctl">Manually managed</h4>
|
|
);
|
|
}
|
|
|
|
if (!query || slug === 'all-hosts') {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
<AceEditor
|
|
editorProps={{ $blockScrolling: Infinity }}
|
|
mode="kolide"
|
|
minLines={1}
|
|
maxLines={20}
|
|
name="label-header"
|
|
readOnly
|
|
setOptions={{ wrap: true }}
|
|
showGutter={false}
|
|
showPrintMargin={false}
|
|
theme="kolide"
|
|
value={query}
|
|
width="100%"
|
|
fontSize={14}
|
|
/>
|
|
);
|
|
}
|
|
|
|
renderHeader = () => {
|
|
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 hostCount = type === 'status' ? statusLabels[`${statusLabelKey}`] : count;
|
|
const hostsTotalDisplay = hostCount === 1 ? '1 host' : `${hostCount} hosts`;
|
|
const defaultDescription = 'No description available.';
|
|
|
|
return (
|
|
<div className={`${baseClass}__header`}>
|
|
{renderDeleteButton()}
|
|
<h1 className={`${baseClass}__title`}>
|
|
<span>{displayText}</span>
|
|
</h1>
|
|
<div className={`${baseClass}__description`}>
|
|
<p>{description || <em>{defaultDescription}</em>}</p>
|
|
</div>
|
|
<div className={`${baseClass}__topper`}>
|
|
<p className={`${baseClass}__host-count`}>{hostsTotalDisplay}</p>
|
|
</div>
|
|
{renderQuery()}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
renderForm = () => {
|
|
const { isAddLabel, labelErrors, selectedLabel } = this.props;
|
|
const { isEditLabel } = this.state;
|
|
const {
|
|
onCancelAddLabel,
|
|
onEditLabel,
|
|
onOsqueryTableSelect,
|
|
onSaveAddLabel,
|
|
toggleEditLabel,
|
|
} = this;
|
|
|
|
if (isAddLabel) {
|
|
return (
|
|
<div className="body-wrap">
|
|
<LabelForm
|
|
onCancel={onCancelAddLabel}
|
|
onOsqueryTableSelect={onOsqueryTableSelect}
|
|
handleSubmit={onSaveAddLabel}
|
|
serverErrors={labelErrors}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isEditLabel) {
|
|
return (
|
|
<div className="body-wrap">
|
|
<LabelForm
|
|
formData={selectedLabel}
|
|
onCancel={toggleEditLabel}
|
|
onOsqueryTableSelect={onOsqueryTableSelect}
|
|
handleSubmit={onEditLabel}
|
|
isEdit
|
|
serverErrors={labelErrors}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
renderSidePanel = () => {
|
|
let SidePanel;
|
|
const {
|
|
isAddLabel,
|
|
labels,
|
|
selectedFilter,
|
|
selectedOsqueryTable,
|
|
statusLabels,
|
|
} = this.props;
|
|
const { onAddHostClick, onAddLabelClick, onLabelClick, onOsqueryTableSelect } = this;
|
|
|
|
if (isAddLabel) {
|
|
SidePanel = (
|
|
<QuerySidePanel
|
|
key="query-side-panel"
|
|
onOsqueryTableSelect={onOsqueryTableSelect}
|
|
selectedOsqueryTable={selectedOsqueryTable}
|
|
/>
|
|
);
|
|
} else {
|
|
SidePanel = (
|
|
<HostSidePanel
|
|
key="hosts-side-panel"
|
|
labels={labels}
|
|
onAddHostClick={onAddHostClick}
|
|
onAddLabelClick={onAddLabelClick}
|
|
onLabelClick={onLabelClick}
|
|
selectedFilter={selectedFilter}
|
|
statusLabels={statusLabels}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return SidePanel;
|
|
}
|
|
|
|
render () {
|
|
const {
|
|
onQueryHost,
|
|
onPaginationChange,
|
|
renderForm,
|
|
renderHeader,
|
|
renderSidePanel,
|
|
renderAddHostModal,
|
|
renderDeleteHostModal,
|
|
renderDeleteLabelModal,
|
|
toggleAddHostModal,
|
|
toggleDeleteHostModal,
|
|
} = this;
|
|
const {
|
|
page,
|
|
perPage,
|
|
hosts,
|
|
isAddLabel,
|
|
loadingLabels,
|
|
loadingHosts,
|
|
selectedLabel,
|
|
statusLabels,
|
|
} = this.props;
|
|
const { isEditLabel } = this.state;
|
|
|
|
const sortedHosts = this.sortHosts(hosts);
|
|
|
|
|
|
let hostCount = 0;
|
|
if (hostCount === 0) {
|
|
switch (selectedLabel ? selectedLabel.id : '') {
|
|
case 'all-hosts':
|
|
hostCount = statusLabels.total_count;
|
|
break;
|
|
case 'new':
|
|
hostCount = statusLabels.new_count;
|
|
break;
|
|
case 'online':
|
|
hostCount = statusLabels.online_count;
|
|
break;
|
|
case 'offline':
|
|
hostCount = statusLabels.offline_count;
|
|
break;
|
|
case 'mia':
|
|
hostCount = statusLabels.mia_count;
|
|
break;
|
|
default:
|
|
hostCount = selectedLabel ? selectedLabel.count : 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="has-sidebar">
|
|
{renderForm()}
|
|
|
|
{!isAddLabel && !isEditLabel &&
|
|
<div className={`${baseClass} body-wrap`}>
|
|
{renderHeader()}
|
|
<div className={`${baseClass}__list`}>
|
|
<HostContainer
|
|
hosts={sortedHosts}
|
|
selectedLabel={selectedLabel}
|
|
loadingHosts={loadingHosts}
|
|
toggleAddHostModal={toggleAddHostModal}
|
|
toggleDeleteHostModal={toggleDeleteHostModal}
|
|
onQueryHost={onQueryHost}
|
|
/>
|
|
{!loadingHosts && <HostPagination
|
|
allHostCount={hostCount}
|
|
currentPage={page}
|
|
hostsPerPage={perPage}
|
|
onPaginationChange={onPaginationChange}
|
|
/>}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
{!loadingLabels && renderSidePanel()}
|
|
{renderAddHostModal()}
|
|
{renderDeleteHostModal()}
|
|
{renderDeleteLabelModal()}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
const mapStateToProps = (state, { location, params }) => {
|
|
const { active_label: activeLabel, label_id: labelID } = params;
|
|
const activeLabelSlug = activeLabel || 'all-hosts';
|
|
const selectedFilter = labelID ? `labels/${labelID}` : activeLabelSlug;
|
|
|
|
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;
|
|
const isAddLabel = location.hash === NEW_LABEL_HASH;
|
|
const selectedLabel = labelEntities.findBy(
|
|
{ slug: selectedFilter },
|
|
{ ignoreCase: true },
|
|
);
|
|
const { selectedOsqueryTable } = state.components.QueryPages;
|
|
const { errors: labelErrors, loading: loadingLabels } = state.entities.labels;
|
|
const { loading: loadingHosts } = state.entities.hosts;
|
|
const enrollSecret = state.app.enrollSecret;
|
|
const config = state.app.config;
|
|
|
|
return {
|
|
selectedFilter,
|
|
page,
|
|
perPage,
|
|
hosts,
|
|
isAddLabel,
|
|
labelErrors,
|
|
labels,
|
|
loadingHosts,
|
|
loadingLabels,
|
|
enrollSecret,
|
|
selectedLabel,
|
|
selectedOsqueryTable,
|
|
statusLabels,
|
|
config,
|
|
};
|
|
};
|
|
|
|
export default connect(mapStateToProps)(ManageHostsPage);
|