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 ( ); } renderDeleteHostModal = () => { const { showDeleteHostModal, selectedHost } = this.state; const { toggleDeleteHostModal, onDestroyHost } = this; if (!showDeleteHostModal) { return false; } return (

This action will delete the host {selectedHost.hostname} from your Fleet instance.

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.

); } renderDeleteLabelModal = () => { const { showDeleteLabelModal } = this.state; const { toggleDeleteLabelModal, onDeleteLabel } = this; if (!showDeleteLabelModal) { return false; } return (

Are you sure you wish to delete this label?

); } renderDeleteButton = () => { const { toggleDeleteLabelModal, toggleEditLabel } = this; const { selectedLabel: { type } } = this.props; if (type !== 'custom') { return false; } return (
); } renderQuery = () => { const { selectedLabel } = this.props; const { slug, label_type: labelType, label_membership_type: membershipType, query } = selectedLabel; if (membershipType === 'manual' && labelType !== 'builtin') { return (

Manually managed

); } if (!query || slug === 'all-hosts') { return false; } return ( ); } renderHeader = () => { const { 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 (

{displayText}

{description || {defaultDescription}}

{hostsTotalDisplay}

{renderDeleteButton()}
); } renderForm = () => { const { isAddLabel, labelErrors, selectedLabel } = this.props; const { isEditLabel } = this.state; const { onCancelAddLabel, onEditLabel, onOsqueryTableSelect, onSaveAddLabel, toggleEditLabel, } = this; if (isAddLabel) { return (
); } if (isEditLabel) { return (
); } return false; } renderSidePanel = () => { let SidePanel; const { isAddLabel, labels, selectedFilter, selectedOsqueryTable, statusLabels, } = this.props; const { onAddLabelClick, onLabelClick, onOsqueryTableSelect } = this; if (isAddLabel) { SidePanel = ( ); } else { SidePanel = ( ); } return SidePanel; } render () { const { onQueryHost, onPaginationChange, renderForm, renderHeader, renderSidePanel, renderAddHostModal, renderDeleteHostModal, renderDeleteLabelModal, renderQuery, toggleAddHostModal, toggleDeleteHostModal, } = this; const { page, perPage, hosts, isAddLabel, loadingLabels, loadingHosts, selectedLabel, statusLabels, } = this.props; const { isEditLabel } = this.state; const { onAddHostClick } = this; 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 (
{renderForm()} {!isAddLabel && !isEditLabel &&
{renderHeader()}
{selectedLabel && renderQuery()}
{!loadingHosts && }
} {!loadingLabels && renderSidePanel()} {renderAddHostModal()} {renderDeleteHostModal()} {renderDeleteLabelModal()}
); } } 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);