Makes items routable on the Manage Hosts Sidebar (#592)

* Makes items routable on the Manage Hosts Sidebar

* slugify active label
This commit is contained in:
Mike Stone 2016-12-12 11:48:50 -05:00 committed by GitHub
parent 3aad263e6d
commit 0753510cc3
12 changed files with 105 additions and 63 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,7 +47,7 @@ const routes = (
</Route>
<Route path="hosts">
<Route path="new" component={NewHostPage} />
<Route path="manage" component={ManageHostsPage} />
<Route path="manage(/:active_label)" component={ManageHostsPage} />
</Route>
</Route>
</Route>

View File

@ -5,6 +5,7 @@ export default {
HOME: '/',
LOGIN: '/login',
LOGOUT: '/logout',
MANAGE_HOSTS: '/hosts/manage',
NEW_PACK: '/packs/new',
RESET_PASSWORD: '/login/reset',
};

View File

@ -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) => {

View File

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