From 0753510cc3b3c73de4dc31df641cfc7875130e82 Mon Sep 17 00:00:00 2001 From: Mike Stone Date: Mon, 12 Dec 2016 11:48:50 -0500 Subject: [PATCH] Makes items routable on the Manage Hosts Sidebar (#592) * Makes items routable on the Manage Hosts Sidebar * slugify active label --- frontend/kolide/helpers.js | 14 +++++- frontend/kolide/helpers.tests.js | 7 +++ frontend/kolide/index.js | 29 ++++++++----- frontend/kolide/index.tests.js | 7 ++- .../hosts/ManageHostsPage/ManageHostsPage.jsx | 38 +++++++--------- .../ManageHostsPage/ManageHostsPage.tests.jsx | 43 +++++++++++++++++-- .../components/ManageHostsPage/actions.js | 15 +------ .../components/ManageHostsPage/reducer.js | 8 +--- frontend/router/index.jsx | 2 +- frontend/router/paths.js | 1 + frontend/test/mocks.js | 2 +- package.json | 2 +- 12 files changed, 105 insertions(+), 63 deletions(-) diff --git a/frontend/kolide/helpers.js b/frontend/kolide/helpers.js index 45635b7eb0..c0c6549596 100644 --- a/frontend/kolide/helpers.js +++ b/frontend/kolide/helpers.js @@ -1,8 +1,18 @@ -import { pick } from 'lodash'; +import { kebabCase, pick } from 'lodash'; const ORG_INFO_ATTRS = ['org_name', 'org_logo_url']; const ADMIN_ATTRS = ['email', 'name', 'password', 'password_confirmation', 'username']; +const labelSlug = (label) => { + const { display_text: displayText } = label; + + if (!displayText) return undefined; + + const lowerDisplayText = displayText.toLowerCase(); + + return kebabCase(lowerDisplayText); +}; + const setupData = (formData) => { const orgInfo = pick(formData, ORG_INFO_ATTRS); const adminInfo = pick(formData, ADMIN_ATTRS); @@ -19,4 +29,4 @@ const setupData = (formData) => { }; }; -export default { setupData }; +export default { labelSlug, setupData }; diff --git a/frontend/kolide/helpers.tests.js b/frontend/kolide/helpers.tests.js index cac9a3d9aa..9ec5a710c5 100644 --- a/frontend/kolide/helpers.tests.js +++ b/frontend/kolide/helpers.tests.js @@ -3,6 +3,13 @@ import expect from 'expect'; import helpers from 'kolide/helpers'; describe('Kolide API - helpers', () => { + describe('#labelSlug', () => { + it('creates a slug for the label', () => { + expect(helpers.labelSlug({ display_text: 'All Hosts' })).toEqual('all-hosts'); + expect(helpers.labelSlug({ display_text: 'windows' })).toEqual('windows'); + }); + }); + describe('#setupData', () => { const formData = { email: 'hi@gnar.dog', diff --git a/frontend/kolide/index.js b/frontend/kolide/index.js index 3e742fdf23..587267a343 100644 --- a/frontend/kolide/index.js +++ b/frontend/kolide/index.js @@ -9,8 +9,11 @@ class Kolide extends Base { return this.authenticatedPost(this.endpoint(LABELS), JSON.stringify({ description, name, query })) .then((response) => { + const { label } = response; + return { - ...response.label, + ...label, + slug: helpers.labelSlug(label), type: 'custom', }; }); @@ -124,18 +127,24 @@ class Kolide extends Base { return this.authenticatedGet(this.endpoint(LABELS)) .then((response) => { + const labelTypeForDisplayText = { + 'All Hosts': 'all', + 'MS Windows': 'platform', + 'CentOS Linux': 'platform', + 'Mac OS X': 'platform', + 'Ubuntu Linux': 'platform', + }; const labels = response.labels.map((label) => { - return { ...label, type: 'custom' }; + return { + ...label, + slug: helpers.labelSlug(label), + type: labelTypeForDisplayText[label.display_text] || 'custom', + }; }); const stubbedLabels = [ - { id: 100, display_text: 'All Hosts', type: 'all', count: 22 }, - { id: 40, display_text: 'ONLINE', type: 'status', count: 20 }, - { id: 50, display_text: 'OFFLINE', type: 'status', count: 2 }, - { id: 55, display_text: 'MIA', description: '(offline > 30 days)', type: 'status', count: 3 }, - { id: 60, display_text: 'macOS', type: 'platform', count: 1 }, - { id: 70, display_text: 'Windows', type: 'platform', count: 1 }, - { id: 80, display_text: 'Ubuntu', type: 'platform', count: 10 }, - { id: 90, display_text: 'Centos', type: 'platform', count: 10 }, + { id: 40, display_text: 'ONLINE', slug: 'online', type: 'status', count: 20 }, + { id: 50, display_text: 'OFFLINE', slug: 'offline', type: 'status', count: 2 }, + { id: 55, display_text: 'MIA', description: '(offline > 30 days)', slug: 'mia', type: 'status', count: 3 }, ]; return labels.concat(stubbedLabels); diff --git a/frontend/kolide/index.tests.js b/frontend/kolide/index.tests.js index 2b7bb7cd4e..1e27d1a2a8 100644 --- a/frontend/kolide/index.tests.js +++ b/frontend/kolide/index.tests.js @@ -50,7 +50,12 @@ describe('Kolide - API client', () => { Kolide.createLabel(labelParams) .then((labelResponse) => { expect(request.isDone()).toEqual(true); - expect(labelResponse).toEqual({ ...labelParams, type: 'custom' }); + expect(labelResponse).toEqual({ + ...labelParams, + display_text: name, + slug: 'label-name', + type: 'custom', + }); done(); }) .catch(done); diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx index 768bd2a8ad..00a0ccec48 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx @@ -1,7 +1,6 @@ import React, { Component, PropTypes } from 'react'; import AceEditor from 'react-ace'; import { connect } from 'react-redux'; -import { filter } from 'lodash'; import { push } from 'react-router-redux'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; @@ -15,12 +14,13 @@ import HostSidePanel from 'components/side_panels/HostSidePanel'; import HostsTable from 'components/hosts/HostsTable'; import Icon from 'components/Icon'; import osqueryTableInterface from 'interfaces/osquery_table'; +import paths from 'router/paths'; import QueryComposer from 'components/queries/QueryComposer'; import QuerySidePanel from 'components/side_panels/QuerySidePanel'; import { renderFlash } from 'redux/nodes/notifications/actions'; import Rocker from 'components/buttons/Rocker'; import { selectOsqueryTable } from 'redux/nodes/components/QueryPages/actions'; -import { setDisplay, setSelectedLabel } from 'redux/nodes/components/ManageHostsPage/actions'; +import { setDisplay } from 'redux/nodes/components/ManageHostsPage/actions'; import { showRightSidePanel, removeRightSidePanel } from 'redux/nodes/app/actions'; import validateQuery from 'components/forms/validators/validate_query'; @@ -54,9 +54,7 @@ export class ManageHostsPage extends Component { dispatch, hosts, labels, - selectedLabel, } = this.props; - const allHostLabel = filter(labels, { type: 'all' })[0]; dispatch(showRightSidePanel); @@ -68,21 +66,6 @@ export class ManageHostsPage extends Component { dispatch(labelActions.loadAll()); } - if (!selectedLabel) { - dispatch(setSelectedLabel(allHostLabel)); - } - - return false; - } - - componentWillReceiveProps (nextProps) { - const { dispatch, labels, selectedLabel } = nextProps; - const allHostLabel = filter(labels, { type: 'all' })[0]; - - if (!selectedLabel && !!allHostLabel) { - dispatch(setSelectedLabel(allHostLabel)); - } - return false; } @@ -126,8 +109,11 @@ export class ManageHostsPage extends Component { evt.preventDefault(); const { dispatch } = this.props; + const { MANAGE_HOSTS } = paths; + const { slug } = selectedLabel; + const nextLocation = slug === 'all-hosts' ? MANAGE_HOSTS : `${MANAGE_HOSTS}/${slug}`; - dispatch(setSelectedLabel(selectedLabel)); + dispatch(push(nextLocation)); return false; }; @@ -334,11 +320,17 @@ export class ManageHostsPage extends Component { } } -const mapStateToProps = (state, { location }) => { - const { display, selectedLabel } = state.components.ManageHostsPage; +const mapStateToProps = (state, { location, params }) => { + const activeLabelSlug = params.active_label || 'all-hosts'; + const { display } = state.components.ManageHostsPage; const { entities: hosts } = entityGetter(state).get('hosts'); - const { entities: labels } = entityGetter(state).get('labels'); + const labelEntities = entityGetter(state).get('labels'); + const { entities: labels } = labelEntities; const isAddLabel = location.hash === NEW_LABEL_HASH; + const selectedLabel = labelEntities.findBy( + { slug: activeLabelSlug }, + { ignoreCase: true }, + ); const { selectedOsqueryTable } = state.components.QueryPages; return { diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx index 75153c4e86..e94530822d 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx @@ -21,6 +21,8 @@ const host = { uptime: 3600000000000, uuid: '1234-5678-9101', }; +const allHostsLabel = { id: 1, display_text: 'All Hosts', slug: 'all-hosts', type: 'all', count: 22 }; +const windowsLabel = { id: 2, display_text: 'Windows', slug: 'windows', type: 'platform', count: 22 }; const mockStore = reduxMockStore({ components: { ManageHostsPage: { @@ -31,6 +33,16 @@ const mockStore = reduxMockStore({ selectedOsqueryTable: stubbedOsqueryTable, }, }, + entities: { + labels: { + data: { + 1: allHostsLabel, + 2: windowsLabel, + 3: { id: 3, display_text: 'Ubuntu', slug: 'ubuntu', type: 'platform', count: 22 }, + 4: { id: 4, display_text: 'ONLINE', slug: 'online', type: 'status', count: 22 }, + }, + }, + }, }); describe('ManageHostsPage - component', () => { @@ -54,7 +66,7 @@ describe('ManageHostsPage - component', () => { }); it('renders a QuerySidePanel when adding a new label', () => { - const ownProps = { location: { hash: '#new_label' } }; + const ownProps = { location: { hash: '#new_label' }, params: {} }; const component = connectedComponent(ConnectedManageHostsPage, { props: ownProps, mockStore }); const page = mount(component); @@ -76,7 +88,7 @@ describe('ManageHostsPage - component', () => { }); it('toggles between displays', () => { - const ownProps = { location: {} }; + const ownProps = { location: {}, params: {} }; const component = connectedComponent(ConnectedManageHostsPage, { props: ownProps, mockStore }); const page = mount(component); const button = page.find('Rocker').find('input'); @@ -97,7 +109,7 @@ describe('ManageHostsPage - component', () => { beforeEach(() => createAceSpy()); afterEach(restoreSpies); - const ownProps = { location: { hash: '#new_label' } }; + const ownProps = { location: { hash: '#new_label' }, params: {} }; const component = connectedComponent(ConnectedManageHostsPage, { props: ownProps, mockStore }); it('renders a QueryComposer component', () => { @@ -112,4 +124,29 @@ describe('ManageHostsPage - component', () => { expect(page.find('QueryComposer').text()).toInclude('New Label Query'); }); }); + + describe('Active label', () => { + beforeEach(() => createAceSpy()); + afterEach(restoreSpies); + + it('Displays the all hosts label as the active label by default', () => { + const ownProps = { location: {}, params: {} }; + const component = connectedComponent(ConnectedManageHostsPage, { props: ownProps, mockStore }); + const page = mount(component); + + expect(page.find('HostSidePanel').props()).toInclude({ + selectedLabel: allHostsLabel, + }); + }); + + it('Displays the windows label as the active label', () => { + const ownProps = { location: {}, params: { active_label: 'windows' } }; + const component = connectedComponent(ConnectedManageHostsPage, { props: ownProps, mockStore }); + const page = mount(component); + + expect(page.find('HostSidePanel').props()).toInclude({ + selectedLabel: windowsLabel, + }); + }); + }); }); diff --git a/frontend/redux/nodes/components/ManageHostsPage/actions.js b/frontend/redux/nodes/components/ManageHostsPage/actions.js index cac2b76644..faed919781 100644 --- a/frontend/redux/nodes/components/ManageHostsPage/actions.js +++ b/frontend/redux/nodes/components/ManageHostsPage/actions.js @@ -8,17 +8,4 @@ export const setDisplay = (display) => { }; }; -export const SET_SELECTED_LABEL = 'SET_SELECTED_LABEL'; -export const setSelectedLabel = (selectedLabel) => { - return { - type: SET_SELECTED_LABEL, - payload: { - selectedLabel, - }, - }; -}; - -export default { - setDisplay, - setSelectedLabel, -}; +export default { setDisplay }; diff --git a/frontend/redux/nodes/components/ManageHostsPage/reducer.js b/frontend/redux/nodes/components/ManageHostsPage/reducer.js index a046bbdeef..08099ece62 100644 --- a/frontend/redux/nodes/components/ManageHostsPage/reducer.js +++ b/frontend/redux/nodes/components/ManageHostsPage/reducer.js @@ -1,8 +1,7 @@ -import { SET_DISPLAY, SET_SELECTED_LABEL } from './actions'; +import { SET_DISPLAY } from './actions'; export const initialState = { display: 'Grid', - selectedLabel: null, }; export default (state = initialState, { type, payload }) => { @@ -12,11 +11,6 @@ export default (state = initialState, { type, payload }) => { ...state, display: payload.display, }; - case SET_SELECTED_LABEL: - return { - ...state, - selectedLabel: payload.selectedLabel, - }; default: return state; } diff --git a/frontend/router/index.jsx b/frontend/router/index.jsx index 9d9d1c0dbf..e5fcc074ad 100644 --- a/frontend/router/index.jsx +++ b/frontend/router/index.jsx @@ -47,7 +47,7 @@ const routes = ( - + diff --git a/frontend/router/paths.js b/frontend/router/paths.js index aa772b079a..85e30f664f 100644 --- a/frontend/router/paths.js +++ b/frontend/router/paths.js @@ -5,6 +5,7 @@ export default { HOME: '/', LOGIN: '/login', LOGOUT: '/logout', + MANAGE_HOSTS: '/hosts/manage', NEW_PACK: '/packs/new', RESET_PASSWORD: '/login/reset', }; diff --git a/frontend/test/mocks.js b/frontend/test/mocks.js index d7bf4f0a97..35b7868d41 100644 --- a/frontend/test/mocks.js +++ b/frontend/test/mocks.js @@ -21,7 +21,7 @@ export const validCreateLabelRequest = (bearerToken, labelParams) => { }, }) .post('/api/v1/kolide/labels', JSON.stringify(labelParams)) - .reply(201, { label: labelParams }); + .reply(201, { label: { ...labelParams, display_text: labelParams.name } }); }; export const validCreateQueryRequest = (bearerToken, queryParams) => { diff --git a/package.json b/package.json index 9e5a0f60b4..4cf2e7bce6 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "react-ace": "^3.6.0", "react-addons-css-transition-group": "^15.3.2", "react-dom": "^15.3.2", - "react-entity-getter": "0.0.2", + "react-entity-getter": "0.0.5", "react-redux": "^4.4.5", "react-router": "^2.7.0", "react-router-redux": "^4.0.5",