mirror of
https://github.com/fleetdm/fleet
synced 2026-05-20 23:48:52 +00:00
Add targets to packs (#831)
* Allow form field values to be an array * Send the server host and label ids on create * Get and display the targets in a pack * Adds target_type to labels and hosts * Allow updating a pack’s targets as well as name and description * Adds select targets dropdown to edit pack page * Adds targets to dropdown when pack is edited
This commit is contained in:
parent
c2084026d1
commit
d1a18bcb89
14 changed files with 184 additions and 50 deletions
|
|
@ -4,8 +4,9 @@ import Button from 'components/buttons/Button';
|
|||
import Form from 'components/forms/Form';
|
||||
import formFieldInterface from 'interfaces/form_field';
|
||||
import InputField from 'components/forms/fields/InputField';
|
||||
import SelectTargetsDropdown from 'components/forms/fields/SelectTargetsDropdown';
|
||||
|
||||
const fieldNames = ['description', 'name'];
|
||||
const fieldNames = ['description', 'name', 'targets'];
|
||||
|
||||
class EditPackForm extends Component {
|
||||
static propTypes = {
|
||||
|
|
@ -13,13 +14,23 @@ class EditPackForm extends Component {
|
|||
fields: PropTypes.shape({
|
||||
description: formFieldInterface.isRequired,
|
||||
name: formFieldInterface.isRequired,
|
||||
targets: formFieldInterface.isRequired,
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onFetchTargets: PropTypes.func,
|
||||
targetsCount: PropTypes.number,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { className, fields, handleSubmit, onCancel } = this.props;
|
||||
const {
|
||||
className,
|
||||
fields,
|
||||
handleSubmit,
|
||||
onCancel,
|
||||
onFetchTargets,
|
||||
targetsCount,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<form className={className} onSubmit={handleSubmit}>
|
||||
|
|
@ -29,6 +40,15 @@ class EditPackForm extends Component {
|
|||
<InputField
|
||||
{...fields.description}
|
||||
/>
|
||||
<SelectTargetsDropdown
|
||||
{...fields.targets}
|
||||
label="select pack targets"
|
||||
name="selected-pack-targets"
|
||||
onFetchTargets={onFetchTargets}
|
||||
onSelect={fields.targets.onChange}
|
||||
selectedTargets={fields.targets.value}
|
||||
targetsCount={targetsCount}
|
||||
/>
|
||||
<Button onClick={onCancel} type="button" variant="inverse">CANCEL</Button>
|
||||
<Button type="submit" variant="brand">SAVE</Button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import EditPackForm from 'components/forms/packs/EditPackForm';
|
|||
import Icon from 'components/icons/Icon';
|
||||
import packInterface from 'interfaces/pack';
|
||||
import SelectTargetsDropdown from 'components/forms/fields/SelectTargetsDropdown';
|
||||
import targetInterface from 'interfaces/target';
|
||||
|
||||
const baseClass = 'edit-pack-form';
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ class EditPackFormWrapper extends Component {
|
|||
onEditPack: PropTypes.func.isRequired,
|
||||
onFetchTargets: PropTypes.func,
|
||||
pack: packInterface.isRequired,
|
||||
packTargets: PropTypes.arrayOf(targetInterface),
|
||||
targetsCount: PropTypes.number,
|
||||
};
|
||||
|
||||
|
|
@ -30,6 +32,7 @@ class EditPackFormWrapper extends Component {
|
|||
onEditPack,
|
||||
onFetchTargets,
|
||||
pack,
|
||||
packTargets,
|
||||
targetsCount,
|
||||
} = this.props;
|
||||
|
||||
|
|
@ -37,9 +40,11 @@ class EditPackFormWrapper extends Component {
|
|||
return (
|
||||
<EditPackForm
|
||||
className={className}
|
||||
formData={pack}
|
||||
formData={{ ...pack, targets: packTargets }}
|
||||
handleSubmit={handleSubmit}
|
||||
onCancel={onCancelEditPack}
|
||||
onFetchTargets={onFetchTargets}
|
||||
targetsCount={targetsCount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -63,6 +68,7 @@ class EditPackFormWrapper extends Component {
|
|||
name="selected-pack-targets"
|
||||
onFetchTargets={onFetchTargets}
|
||||
onSelect={noop}
|
||||
selectedTargets={packTargets}
|
||||
targetsCount={targetsCount}
|
||||
disabled
|
||||
className={`${baseClass}__select-targets`}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ export default PropTypes.shape({
|
|||
error: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
|
||||
value: PropTypes.oneOfType(
|
||||
[PropTypes.array, PropTypes.bool, PropTypes.number, PropTypes.string]
|
||||
),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import { PropTypes } from 'react';
|
||||
import hostInterface from 'interfaces/host';
|
||||
import labelInterface from 'interfaces/label';
|
||||
|
||||
export default PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
label: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
target_type: PropTypes.string,
|
||||
});
|
||||
export default PropTypes.oneOfType([hostInterface, labelInterface]);
|
||||
|
|
|
|||
|
|
@ -47,11 +47,15 @@ export const formatConfigDataForServer = (config) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const formatSelectedTargetsForApi = (selectedTargets) => {
|
||||
export const formatSelectedTargetsForApi = (selectedTargets, appendID = false) => {
|
||||
const targets = selectedTargets || [];
|
||||
const hosts = flatMap(targets, filterTarget('hosts'));
|
||||
const labels = flatMap(targets, filterTarget('labels'));
|
||||
|
||||
if (appendID) {
|
||||
return { host_ids: hosts, label_ids: labels };
|
||||
}
|
||||
|
||||
return { hosts, labels };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,15 @@ describe('Kolide API - helpers', () => {
|
|||
labels: [1, 2],
|
||||
});
|
||||
});
|
||||
|
||||
it('appends `_id` when appendID is specified', () => {
|
||||
const targets = [host1, host2, label1, label2];
|
||||
|
||||
expect(formatSelectedTargetsForApi(targets, true)).toEqual({
|
||||
host_ids: [6, 5],
|
||||
label_ids: [1, 2],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setupData', () => {
|
||||
|
|
|
|||
|
|
@ -32,10 +32,11 @@ class Kolide extends Base {
|
|||
});
|
||||
}
|
||||
|
||||
createPack = ({ name, description }) => {
|
||||
createPack = ({ name, description, targets }) => {
|
||||
const { PACKS } = endpoints;
|
||||
const packTargets = helpers.formatSelectedTargetsForApi(targets, true);
|
||||
|
||||
return this.authenticatedPost(this.endpoint(PACKS), JSON.stringify({ description, name }))
|
||||
return this.authenticatedPost(this.endpoint(PACKS), JSON.stringify({ description, name, ...packTargets }))
|
||||
.then((response) => { return response.pack; });
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +122,7 @@ class Kolide extends Base {
|
|||
const { HOSTS } = endpoints;
|
||||
|
||||
return this.authenticatedGet(this.endpoint(HOSTS))
|
||||
.then((response) => { return response.hosts; });
|
||||
.then(response => response.hosts);
|
||||
}
|
||||
|
||||
getLabelHosts = (labelID) => {
|
||||
|
|
@ -358,11 +359,12 @@ class Kolide extends Base {
|
|||
return this.authenticatedPatch(this.endpoint(CONFIG), JSON.stringify(configData));
|
||||
}
|
||||
|
||||
updatePack = ({ id: packID }, updateParams) => {
|
||||
updatePack = (pack, { description, name, targets }) => {
|
||||
const { PACKS } = endpoints;
|
||||
const updatePackEndpoint = `${this.baseURL}${PACKS}/${packID}`;
|
||||
const updatePackEndpoint = `${this.baseURL}${PACKS}/${pack.id}`;
|
||||
const packTargets = helpers.formatSelectedTargetsForApi(targets, true);
|
||||
|
||||
return this.authenticatedPatch(updatePackEndpoint, JSON.stringify(updateParams))
|
||||
return this.authenticatedPatch(updatePackEndpoint, JSON.stringify({ description, name, ...packTargets }))
|
||||
.then((response) => { return response.pack; });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import nock from 'nock';
|
|||
import Kolide from 'kolide';
|
||||
import helpers from 'kolide/helpers';
|
||||
import mocks from 'test/mocks';
|
||||
import { queryStub, userStub } from 'test/stubs';
|
||||
import { hostStub, queryStub, userStub } from 'test/stubs';
|
||||
|
||||
const {
|
||||
invalidForgotPasswordRequest,
|
||||
|
|
@ -80,7 +80,7 @@ describe('Kolide - API client', () => {
|
|||
|
||||
it('#createPack', (done) => {
|
||||
const { description, name } = pack;
|
||||
const params = { description, name };
|
||||
const params = { description, name, host_ids: [], label_ids: [] };
|
||||
const request = validCreatePackRequest(bearerToken, params);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
|
|
@ -105,7 +105,9 @@ describe('Kolide - API client', () => {
|
|||
});
|
||||
|
||||
it('#updatePack', (done) => {
|
||||
const updatePackParams = { name: 'New Pack Name' };
|
||||
const targets = [hostStub];
|
||||
const packTargets = helpers.formatSelectedTargetsForApi(targets, true);
|
||||
const updatePackParams = { name: 'New Pack Name', ...packTargets };
|
||||
const request = validUpdatePackRequest(bearerToken, pack, updatePackParams);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { noop, size, find } from 'lodash';
|
||||
import { filter, includes, isEqual, noop, size, find } from 'lodash';
|
||||
import { push } from 'react-router-redux';
|
||||
|
||||
import EditPackFormWrapper from 'components/packs/EditPackFormWrapper';
|
||||
import hostActions from 'redux/nodes/entities/hosts/actions';
|
||||
import hostInterface from 'interfaces/host';
|
||||
import labelActions from 'redux/nodes/entities/labels/actions';
|
||||
import labelInterface from 'interfaces/label';
|
||||
import packActions from 'redux/nodes/entities/packs/actions';
|
||||
import ScheduleQuerySidePanel from 'components/side_panels/ScheduleQuerySidePanel';
|
||||
import packInterface from 'interfaces/pack';
|
||||
|
|
@ -24,7 +28,9 @@ export class EditPackPage extends Component {
|
|||
isLoadingPack: PropTypes.bool,
|
||||
isLoadingScheduledQueries: PropTypes.bool,
|
||||
pack: packInterface,
|
||||
packHosts: PropTypes.arrayOf(hostInterface),
|
||||
packID: PropTypes.string,
|
||||
packLabels: PropTypes.arrayOf(labelInterface),
|
||||
scheduledQueries: PropTypes.arrayOf(queryInterface),
|
||||
};
|
||||
|
||||
|
|
@ -41,7 +47,16 @@ export class EditPackPage extends Component {
|
|||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { allQueries, dispatch, isLoadingPack, pack, packID, scheduledQueries } = this.props;
|
||||
const {
|
||||
allQueries,
|
||||
dispatch,
|
||||
isLoadingPack,
|
||||
pack,
|
||||
packHosts,
|
||||
packID,
|
||||
packLabels,
|
||||
scheduledQueries,
|
||||
} = this.props;
|
||||
const { load } = packActions;
|
||||
const { loadAll } = queryActions;
|
||||
|
||||
|
|
@ -49,6 +64,16 @@ export class EditPackPage extends Component {
|
|||
dispatch(load(packID));
|
||||
}
|
||||
|
||||
if (pack) {
|
||||
if (!packHosts || packHosts.length !== pack.host_ids.length) {
|
||||
dispatch(hostActions.loadAll());
|
||||
}
|
||||
|
||||
if (!packLabels || packLabels.length !== pack.label_ids.length) {
|
||||
dispatch(labelActions.loadAll());
|
||||
}
|
||||
}
|
||||
|
||||
if (!size(scheduledQueries)) {
|
||||
dispatch(scheduledQueryActions.loadAll({ id: packID }));
|
||||
}
|
||||
|
|
@ -60,6 +85,20 @@ export class EditPackPage extends Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps ({ dispatch, pack, packHosts, packLabels }) {
|
||||
if (!isEqual(pack, this.props.pack)) {
|
||||
if (!packHosts || packHosts.length !== pack.host_ids.length) {
|
||||
dispatch(hostActions.loadAll());
|
||||
}
|
||||
|
||||
if (!packLabels || packLabels.length !== pack.label_ids.length) {
|
||||
dispatch(labelActions.loadAll());
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onCancelEditPack = () => {
|
||||
const { dispatch, isEdit, packID } = this.props;
|
||||
|
||||
|
|
@ -97,10 +136,10 @@ export class EditPackPage extends Component {
|
|||
}
|
||||
|
||||
handlePackFormSubmit = (formData) => {
|
||||
const { dispatch } = this.props;
|
||||
const { dispatch, pack } = this.props;
|
||||
const { update } = packActions;
|
||||
|
||||
return dispatch(update(formData));
|
||||
return dispatch(update(pack, formData));
|
||||
}
|
||||
|
||||
handleRemoveScheduledQueries = (scheduledQueryIDs) => {
|
||||
|
|
@ -149,7 +188,9 @@ export class EditPackPage extends Component {
|
|||
onToggleEdit,
|
||||
} = this;
|
||||
const { targetsCount, selectedQuery } = this.state;
|
||||
const { allQueries, isEdit, isLoadingScheduledQueries, pack, scheduledQueries } = this.props;
|
||||
const { allQueries, isEdit, isLoadingScheduledQueries, pack, packHosts, packLabels, scheduledQueries } = this.props;
|
||||
|
||||
const packTargets = [...packHosts, ...packLabels];
|
||||
|
||||
if (!pack || isLoadingScheduledQueries) {
|
||||
return false;
|
||||
|
|
@ -166,6 +207,7 @@ export class EditPackPage extends Component {
|
|||
onEditPack={onToggleEdit}
|
||||
onFetchTargets={onFetchTargets}
|
||||
pack={pack}
|
||||
packTargets={packTargets}
|
||||
targetsCount={targetsCount}
|
||||
/>
|
||||
<ScheduledQueriesListWrapper
|
||||
|
|
@ -194,6 +236,12 @@ const mapStateToProps = (state, { params, route }) => {
|
|||
const scheduledQueries = entityGetter.get('scheduled_queries').where({ pack_id: packID });
|
||||
const isLoadingScheduledQueries = state.entities.scheduled_queries.loading;
|
||||
const isEdit = route.path === 'edit';
|
||||
const packHosts = pack ? filter(state.entities.hosts.data, (host) => {
|
||||
return includes(pack.host_ids, host.id);
|
||||
}) : [];
|
||||
const packLabels = pack ? filter(state.entities.labels.data, (label) => {
|
||||
return includes(pack.label_ids, label.id);
|
||||
}) : [];
|
||||
|
||||
return {
|
||||
allQueries,
|
||||
|
|
@ -201,7 +249,9 @@ const mapStateToProps = (state, { params, route }) => {
|
|||
isLoadingPack,
|
||||
isLoadingScheduledQueries,
|
||||
pack,
|
||||
packHosts,
|
||||
packID,
|
||||
packLabels,
|
||||
scheduledQueries,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import EditPackPage from './EditPackPage';
|
|||
describe('EditPackPage - component', () => {
|
||||
const store = {
|
||||
entities: {
|
||||
hosts: { data: {} },
|
||||
labels: { data: {} },
|
||||
packs: {
|
||||
data: {
|
||||
[packStub.id]: packStub,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { connect } from 'react-redux';
|
|||
import { noop } from 'lodash';
|
||||
import { push } from 'react-router-redux';
|
||||
|
||||
import Kolide from 'kolide';
|
||||
import packActions from 'redux/nodes/entities/packs/actions';
|
||||
import PackForm from 'components/forms/packs/PackForm';
|
||||
import PackInfoSidePanel from 'components/side_panels/PackInfoSidePanel';
|
||||
|
|
@ -47,37 +46,15 @@ export class PackComposerPage extends Component {
|
|||
}
|
||||
|
||||
handleSubmit = (formData) => {
|
||||
const { create, load } = packActions;
|
||||
const { create } = packActions;
|
||||
const { dispatch } = this.props;
|
||||
const { visitPackPage } = this;
|
||||
|
||||
return dispatch(create(formData))
|
||||
.then((pack) => {
|
||||
const { id: packID } = pack;
|
||||
const { targets } = formData;
|
||||
|
||||
if (!targets) {
|
||||
return visitPackPage(packID);
|
||||
}
|
||||
|
||||
const promises = targets.map((target) => {
|
||||
const { id: targetID } = target;
|
||||
|
||||
if (target.target_type === 'labels') {
|
||||
Kolide.addLabelToPack(packID, targetID);
|
||||
}
|
||||
|
||||
// TODO: Add host to pack when API is available
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(() => {
|
||||
dispatch(load(packID))
|
||||
.then(() => {
|
||||
return visitPackPage(packID);
|
||||
});
|
||||
});
|
||||
return visitPackPage(packID);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,5 +7,8 @@ const { HOSTS: schema } = schemas;
|
|||
export default reduxConfig({
|
||||
entityName: 'hosts',
|
||||
loadAllFunc: Kolide.getHosts,
|
||||
parseEntityFunc: (host) => {
|
||||
return { ...host, target_type: 'hosts' };
|
||||
},
|
||||
schema,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,5 +8,8 @@ export default reduxConfig({
|
|||
createFunc: Kolide.createLabel,
|
||||
entityName: 'labels',
|
||||
loadAllFunc: Kolide.getLabels,
|
||||
parseEntityFunc: (label) => {
|
||||
return { ...label, target_type: 'labels' };
|
||||
},
|
||||
schema,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,60 @@ export const configStub = {
|
|||
},
|
||||
};
|
||||
|
||||
export const hostStub = {
|
||||
created_at: '2017-01-10T19:18:55Z',
|
||||
updated_at: '2017-01-10T20:13:52Z',
|
||||
deleted_at: null,
|
||||
deleted: false,
|
||||
id: 1,
|
||||
detail_updated_at: '2017-01-10T20:01:48Z',
|
||||
seen_time: '2017-01-10T20:13:54Z',
|
||||
hostname: '52883a0ba916',
|
||||
uuid: 'FD87130B-09A9-683D-9095-D92CD20728CA',
|
||||
platform: 'ubuntu',
|
||||
osquery_version: '2.1.2',
|
||||
os_version: 'Ubuntu 14.4.',
|
||||
build: '',
|
||||
platform_like: 'debian',
|
||||
code_name: '',
|
||||
uptime: 45469000000000,
|
||||
memory: 2094940160,
|
||||
cpu_type: '6',
|
||||
cpu_subtype: '78',
|
||||
cpu_brand: 'Intel(R) Core(TM) i5-6267U CPU @ 2.90GHz',
|
||||
cpu_physical_cores: 2,
|
||||
cpu_logical_cores: 2,
|
||||
hardware_vendor: ' ',
|
||||
hardware_model: 'BHYVE',
|
||||
hardware_version: '1.0',
|
||||
hardware_serial: 'None',
|
||||
computer_name: '52883a0ba916',
|
||||
primary_ip_id: 1,
|
||||
network_interfaces: [
|
||||
{
|
||||
id: 1,
|
||||
interface: 'eth0',
|
||||
address: '172.19.0.8',
|
||||
mask: '255.255.0.0',
|
||||
broadcast: '172.19.0.8',
|
||||
point_to_point: '',
|
||||
mac: '02:42:ac:13:00:08',
|
||||
type: 1,
|
||||
mtu: 1500,
|
||||
metric: 0,
|
||||
ipackets: 512,
|
||||
opackets: 317,
|
||||
ibytes: 97231,
|
||||
obytes: 43502,
|
||||
ierrors: 0,
|
||||
oerrors: 0,
|
||||
last_change: -1,
|
||||
},
|
||||
],
|
||||
status: 'online',
|
||||
display_text: '52883a0ba916',
|
||||
};
|
||||
|
||||
export const packStub = {
|
||||
created_at: '0001-01-01T00:00:00Z',
|
||||
updated_at: '0001-01-01T00:00:00Z',
|
||||
|
|
@ -41,6 +95,8 @@ export const packStub = {
|
|||
platform: '',
|
||||
created_by: 1,
|
||||
disabled: false,
|
||||
host_ids: [],
|
||||
label_ids: [],
|
||||
};
|
||||
|
||||
export const queryStub = {
|
||||
|
|
@ -82,6 +138,7 @@ export const userStub = {
|
|||
export default {
|
||||
adminUserStub,
|
||||
configStub,
|
||||
hostStub,
|
||||
packStub,
|
||||
queryStub,
|
||||
scheduledQueryStub,
|
||||
|
|
|
|||
Loading…
Reference in a new issue