+
- Query Packs
+ Query Packs
-
-
+
+
+ {renderCTAs()}
-
-
-
- | Name |
- Queries |
- Status |
- Author |
- Number of Hosts |
- Last Updated |
-
-
-
- {packs.map((pack) => {
- return renderPack(pack);
- })}
-
-
+
+ {renderSidePanel()}
);
}
}
-const mapStateToProps = (state) => {
- const { entities: packs } = entityGetter(state).get('packs');
+const mapStateToProps = (state, { location }) => {
+ const packEntities = entityGetter(state).get('packs');
+ const scheduledQueryEntities = entityGetter(state).get('scheduled_queries');
+ const { entities: packs } = packEntities;
+ const selectedPackID = get(location, 'query.selectedPack');
+ const selectedPack = selectedPackID && packEntities.findBy({ id: selectedPackID });
+ const selectedScheduledQueries = selectedPack && scheduledQueryEntities.where({ pack_id: selectedPack.id });
- return { packs };
+ return { packs, selectedPack, selectedScheduledQueries };
};
export default connect(mapStateToProps)(AllPacksPage);
diff --git a/frontend/pages/packs/AllPacksPage/AllPacksPage.tests.jsx b/frontend/pages/packs/AllPacksPage/AllPacksPage.tests.jsx
new file mode 100644
index 0000000000..216c1e11d4
--- /dev/null
+++ b/frontend/pages/packs/AllPacksPage/AllPacksPage.tests.jsx
@@ -0,0 +1,178 @@
+import React from 'react';
+import expect from 'expect';
+import { find } from 'lodash';
+import { mount } from 'enzyme';
+
+import ConnectedAllPacksPage, { AllPacksPage } from 'pages/packs/AllPacksPage/AllPacksPage';
+import { connectedComponent, fillInFormInput, reduxMockStore } from 'test/helpers';
+import { packStub } from 'test/stubs';
+
+const store = {
+ entities: {
+ packs: {
+ data: {
+ [packStub.id]: packStub,
+ 101: {
+ ...packStub,
+ id: 101,
+ name: 'My unique pack name',
+ },
+ },
+ },
+ },
+};
+
+describe('AllPacksPage - component', () => {
+ it('filters the packs list', () => {
+ const Component = connectedComponent(ConnectedAllPacksPage, {
+ mockStore: reduxMockStore(store),
+ });
+ const page = mount(Component).find('AllPacksPage');
+ const packsFilterInput = page.find({ name: 'pack-filter' }).find('input');
+
+ expect(page.node.getPacks().length).toEqual(2);
+
+ fillInFormInput(packsFilterInput, 'My unique pack name');
+
+ expect(page.node.getPacks().length).toEqual(1);
+ });
+
+ it('renders a PacksList component', () => {
+ const page = mount(connectedComponent(ConnectedAllPacksPage));
+
+ expect(page.find('PacksList').length).toEqual(1);
+ });
+
+ it('renders the PackInfoSidePanel by default', () => {
+ const page = mount(connectedComponent(ConnectedAllPacksPage));
+
+ expect(page.find('PackInfoSidePanel').length).toEqual(1);
+ });
+
+ it('updates checkedPackIDs in state when the select all packs Checkbox is toggled', () => {
+ const page = mount(
);
+ const selectAllPacks = page.find({ name: 'select-all-packs' });
+
+ expect(page.state('checkedPackIDs')).toEqual([]);
+
+ selectAllPacks.simulate('change');
+
+ expect(page.state('checkedPackIDs')).toEqual([packStub.id]);
+
+ selectAllPacks.simulate('change');
+
+ expect(page.state('checkedPackIDs')).toEqual([]);
+ });
+
+ it('updates checkedPackIDs in state when a pack row Checkbox is toggled', () => {
+ const page = mount(
);
+ const selectPack = page.find({ name: `select-pack-${packStub.id}` });
+
+ expect(page.state('checkedPackIDs')).toEqual([]);
+
+ selectPack.simulate('change');
+
+ expect(page.state('checkedPackIDs')).toEqual([packStub.id]);
+
+ selectPack.simulate('change');
+
+ expect(page.state('checkedPackIDs')).toEqual([]);
+ });
+
+ describe('bulk actions', () => {
+ const packs = [packStub, { ...packStub, id: 101, name: 'My unique pack name' }];
+
+ it('displays the bulk action buttons when a pack is checked', () => {
+ const page = mount(
);
+ const selectAllPacks = page.find({ name: 'select-all-packs' });
+
+ selectAllPacks.simulate('change');
+
+ expect(page.state('checkedPackIDs')).toEqual([packStub.id, 101]);
+ expect(page.find('.all-packs-page__bulk-action-btn--disable').length).toEqual(1);
+ expect(page.find('.all-packs-page__bulk-action-btn--enable').length).toEqual(1);
+ expect(page.find('.all-packs-page__bulk-action-btn--delete').length).toEqual(1);
+ });
+
+ it('dispatches the pack update function when disable is clicked', () => {
+ const mockStore = reduxMockStore(store);
+ const Component = connectedComponent(ConnectedAllPacksPage, { mockStore });
+ const page = mount(Component).find('AllPacksPage');
+ const selectAllPacks = page.find({ name: 'select-all-packs' });
+
+ selectAllPacks.simulate('change');
+
+ const disableBtn = page.find('.all-packs-page__bulk-action-btn--disable');
+
+ disableBtn.simulate('click');
+
+ const dispatchedActions = mockStore.getActions();
+
+ expect(dispatchedActions).toInclude({ type: 'packs_UPDATE_REQUEST' });
+ });
+
+ it('dispatches the pack update function when enable is clicked', () => {
+ const mockStore = reduxMockStore(store);
+ const Component = connectedComponent(ConnectedAllPacksPage, { mockStore });
+ const page = mount(Component).find('AllPacksPage');
+ const selectAllPacks = page.find({ name: 'select-all-packs' });
+
+ selectAllPacks.simulate('change');
+
+ const enableBtn = page.find('.all-packs-page__bulk-action-btn--enable');
+
+ enableBtn.simulate('click');
+
+ const dispatchedActions = mockStore.getActions();
+
+ expect(dispatchedActions).toInclude({ type: 'packs_UPDATE_REQUEST' });
+ });
+
+ it('dispatches the pack destroy function when delete is clicked', () => {
+ const mockStore = reduxMockStore(store);
+ const Component = connectedComponent(ConnectedAllPacksPage, { mockStore });
+ const page = mount(Component).find('AllPacksPage');
+ const selectAllPacks = page.find({ name: 'select-all-packs' });
+
+ selectAllPacks.simulate('change');
+
+ const deleteBtn = page.find('.all-packs-page__bulk-action-btn--delete');
+
+ deleteBtn.simulate('click');
+
+ const dispatchedActions = mockStore.getActions();
+
+ expect(dispatchedActions).toInclude({ type: 'packs_DESTROY_REQUEST' });
+ });
+ });
+
+ describe('selecting a pack', () => {
+ it('updates the URL when a pack is selected', () => {
+ const mockStore = reduxMockStore(store);
+ const Component = connectedComponent(ConnectedAllPacksPage, { mockStore });
+ const page = mount(Component).find('AllPacksPage');
+ const firstRow = page.find('Row').first();
+
+ expect(page.prop('selectedPack')).toNotExist();
+
+ firstRow.find('ClickableTableRow').first().simulate('click');
+
+ const dispatchedActions = mockStore.getActions();
+ const locationChangeAction = find(dispatchedActions, { type: '@@router/CALL_HISTORY_METHOD' });
+
+ expect(locationChangeAction.payload.args).toEqual([{
+ pathname: '/packs/manage',
+ query: { selectedPack: packStub.id },
+ }]);
+ });
+
+ it('sets the selectedPack prop', () => {
+ const mockStore = reduxMockStore(store);
+ const props = { location: { query: { selectedPack: packStub.id } } };
+ const Component = connectedComponent(ConnectedAllPacksPage, { mockStore, props });
+ const page = mount(Component).find('AllPacksPage');
+
+ expect(page.prop('selectedPack')).toEqual(packStub);
+ });
+ });
+});
diff --git a/frontend/pages/packs/AllPacksPage/_styles.scss b/frontend/pages/packs/AllPacksPage/_styles.scss
index 21f1e226fa..2f5767e53d 100644
--- a/frontend/pages/packs/AllPacksPage/_styles.scss
+++ b/frontend/pages/packs/AllPacksPage/_styles.scss
@@ -1,4 +1,50 @@
.all-packs-page {
+ &__bulk-action-btn {
+ color: $link;
+ font-size: 15px;
+ font-weight: $normal;
+ margin-left: 26px;
+
+ &--disable {
+ margin-left: 0;
+
+ i {
+ color: $alert;
+ }
+ }
+
+ &--enable {
+ i {
+ color: $success;
+ }
+ }
+
+ &--delete {
+ i {
+ color: $silver;
+ }
+ }
+ }
+
+ &__pack-count {
+ color: #858495;
+ font-size: 14px;
+ font-weight: $bold;
+ letter-spacing: -0.5px;
+ margin: 0;
+ }
+
+ &__search-create-section {
+ @include display(flex);
+ @include justify-content(space-between);
+
+ input {
+ &[name='pack-filter'] {
+ width: 240px;
+ }
+ }
+ }
+
&__title {
color: $text-medium;
display: inline-block;
diff --git a/frontend/redux/nodes/entities/packs/config.js b/frontend/redux/nodes/entities/packs/config.js
index 90a6875b34..3d0fe33c09 100644
--- a/frontend/redux/nodes/entities/packs/config.js
+++ b/frontend/redux/nodes/entities/packs/config.js
@@ -1,13 +1,15 @@
-import Kolide from '../../../../kolide';
-import reduxConfig from '../base/reduxConfig';
-import schemas from '../base/schemas';
+import Kolide from 'kolide';
+import reduxConfig from 'redux/nodes/entities/base/reduxConfig';
+import schemas from 'redux/nodes/entities/base/schemas';
const { PACKS: schema } = schemas;
export default reduxConfig({
createFunc: Kolide.createPack,
+ destroyFunc: Kolide.destroyPack,
entityName: 'packs',
loadAllFunc: Kolide.getPacks,
loadFunc: Kolide.getPack,
schema,
+ updateFunc: Kolide.updatePack,
});
diff --git a/frontend/router/index.jsx b/frontend/router/index.jsx
index d8b9375194..e027cab06e 100644
--- a/frontend/router/index.jsx
+++ b/frontend/router/index.jsx
@@ -49,7 +49,7 @@ const routes = (
-
+
diff --git a/frontend/test/mocks.js b/frontend/test/mocks.js
index 54fc6a9d3d..7da8749480 100644
--- a/frontend/test/mocks.js
+++ b/frontend/test/mocks.js
@@ -64,6 +64,16 @@ export const validCreateScheduledQueryRequest = (bearerToken, formData) => {
.reply(201, { scheduled_query: scheduledQueryStub });
};
+export const validDestroyPackRequest = (bearerToken, pack) => {
+ return nock('http://localhost:8080', {
+ reqHeaders: {
+ Authorization: `Bearer ${bearerToken}`,
+ },
+ })
+ .delete(`/api/v1/kolide/packs/${pack.id}`)
+ .reply(200, {});
+};
+
export const validDestroyScheduledQueryRequest = (bearerToken, scheduledQuery) => {
return nock('http://localhost:8080', {
reqHeaders: {
@@ -305,6 +315,16 @@ export const validUpdateConfigRequest = (bearerToken, configData) => {
.reply(200, {});
};
+export const validUpdatePackRequest = (bearerToken, pack, formData) => {
+ return nock('http://localhost:8080', {
+ reqHeaders: {
+ Authorization: `Bearer ${bearerToken}`,
+ },
+ })
+ .patch(`/api/v1/kolide/packs/${pack.id}`, JSON.stringify(formData))
+ .reply(200, { pack: { ...pack, ...formData } });
+};
+
export const validUpdateQueryRequest = (bearerToken, query, formData) => {
return nock('http://localhost:8080', {
reqHeaders: {
@@ -330,6 +350,7 @@ export default {
validCreatePackRequest,
validCreateQueryRequest,
validCreateScheduledQueryRequest,
+ validDestroyPackRequest,
validDestroyScheduledQueryRequest,
validForgotPasswordRequest,
validGetConfigRequest,
@@ -349,6 +370,7 @@ export default {
validRunQueryRequest,
validSetupRequest,
validUpdateConfigRequest,
+ validUpdatePackRequest,
validUpdateQueryRequest,
validUpdateUserRequest,
validUser,