Skip to content

Commit

Permalink
[Security Solution] Case ui enhancement (elastic#91863)
Browse files Browse the repository at this point in the history
* ui enhancement

* fix actions

* unit test

* update row actions

* add case status all

* update find status

* fix type

* remove all case count from dropdown

* fix type error

* fix unit test

* disable bulk actions on status all

* clean up

* fix types

* fix cypress tests

* review

* review

* update status is only available for individual cases

* update available actions on status all

* fix unit test

* remove lodash get

* rename status all

* omit status if it is set to all

* do not sent status if itis set to all

* Remove all status from the backend

* Hide actions on all status

* fix unit test

Co-authored-by: Kibana Machine <[email protected]>
Co-authored-by: Christos Nasikas <[email protected]>
# Conflicts:
#	x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx
#	x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx
#	x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx
#	x-pack/plugins/security_solution/public/cases/components/status/config.ts
  • Loading branch information
angorayc committed Mar 3, 2021
1 parent df76ebc commit da39fe0
Show file tree
Hide file tree
Showing 24 changed files with 301 additions and 73 deletions.
2 changes: 0 additions & 2 deletions x-pack/plugins/case/server/routes/api/cases/find_cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export function initFindCasesApi({ caseService, router, logger }: RouteDeps) {
CasesFindRequestRt.decode(request.query),
fold(throwErrors(Boom.badRequest), identity)
);

const queryArgs = {
tags: queryParams.tags,
reporters: queryParams.reporters,
Expand All @@ -47,7 +46,6 @@ export function initFindCasesApi({ caseService, router, logger }: RouteDeps) {
};

const caseQueries = constructQueryOptions(queryArgs);

const cases = await caseService.findCasesGroupedByID({
client,
caseOptions: { ...queryParams, ...caseQueries.case },
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/case/server/routes/api/cases/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const addStatusFilter = ({
appendFilter,
type = CASE_SAVED_OBJECT,
}: {
status: CaseStatuses | undefined;
status?: CaseStatuses;
appendFilter?: string;
type?: string;
}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
backToCases,
createCase,
fillCasesMandatoryfields,
filterStatusOpen,
} from '../../tasks/create_new_case';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';

Expand Down Expand Up @@ -75,6 +76,7 @@ describe('Cases', () => {
attachTimeline(this.mycase);
createCase();
backToCases();
filterStatusOpen();

cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases');
cy.get(ALL_CASES_OPEN_CASES_STATS).should('have.text', 'Open cases1');
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/cypress/screens/all_cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const ALL_CASES_NAME = '[data-test-subj="case-details-link"]';

export const ALL_CASES_OPEN_CASES_COUNT = '[data-test-subj="case-status-filter"]';

export const ALL_CASES_OPEN_FILTER = '[data-test-subj="case-status-filter-open"]';

export const ALL_CASES_OPEN_CASES_STATS = '[data-test-subj="openStatsHeader"]';

export const ALL_CASES_OPENED_ON = '[data-test-subj="case-table-column-createdAt"]';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ServiceNowconnectorOptions,
TestCase,
} from '../objects/case';
import { ALL_CASES_OPEN_CASES_COUNT, ALL_CASES_OPEN_FILTER } from '../screens/all_cases';

import {
BACK_TO_CASES_BTN,
Expand Down Expand Up @@ -40,6 +41,11 @@ export const backToCases = () => {
cy.get(BACK_TO_CASES_BTN).click({ force: true });
};

export const filterStatusOpen = () => {
cy.get(ALL_CASES_OPEN_CASES_COUNT).click();
cy.get(ALL_CASES_OPEN_FILTER).click();
};

export const fillCasesMandatoryfields = (newCase: TestCase) => {
cy.get(TITLE_INPUT).type(newCase.name, { force: true });
newCase.tags.forEach((tag) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ import { CaseStatuses } from '../../../../../case/common/api';
import { Case, SubCase } from '../../containers/types';
import { UpdateCase } from '../../containers/use_get_cases';
import * as i18n from './translations';
import { isIndividual } from './helpers';

interface GetActions {
caseStatus: string;
dispatchUpdate: Dispatch<Omit<UpdateCase, 'refetchCasesStatus'>>;
deleteCaseOnClick: (deleteCase: Case) => void;
}

const hasSubCases = (subCases: SubCase[] | null | undefined) =>
subCases != null && subCases?.length > 0;

export const getActions = ({
caseStatus,
dispatchUpdate,
Expand All @@ -36,8 +34,8 @@ export const getActions = ({
'data-test-subj': 'action-delete',
},
{
available: (item) => caseStatus === CaseStatuses.open && !hasSubCases(item.subCases),
description: i18n.CLOSE_CASE,
available: (item: Case | SubCase) => item.status !== CaseStatuses.closed,
enabled: (item: Case | SubCase) => isIndividual(item),
icon: 'folderCheck',
name: i18n.CLOSE_CASE,
onClick: (theCase: Case) =>
Expand All @@ -51,9 +49,10 @@ export const getActions = ({
'data-test-subj': 'action-close',
},
{
available: (item) => caseStatus !== CaseStatuses.open && !hasSubCases(item.subCases),
available: (item: Case | SubCase) => item.status !== CaseStatuses.open,
enabled: (item: Case | SubCase) => isIndividual(item),
description: i18n.REOPEN_CASE,
icon: 'folderExclamation',
icon: 'folderOpen',
name: i18n.REOPEN_CASE,
onClick: (theCase: Case) =>
dispatchUpdate({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
import styled from 'styled-components';
import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types';

import { CaseStatuses } from '../../../../../case/common/api';
import { CaseStatuses, CaseType } from '../../../../../case/common/api';
import { getEmptyTagValue } from '../../../common/components/empty_value';
import { Case, SubCase } from '../../containers/types';
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
Expand Down Expand Up @@ -204,7 +204,7 @@ export const getCasesColumns = (
name: i18n.STATUS,
render: (theCase: Case) => {
if (theCase?.subCases == null || theCase.subCases.length === 0) {
if (theCase.status == null) {
if (theCase.status == null || theCase.type === CaseType.collection) {
return getEmptyTagValue();
}
return <Status type={theCase.status} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@
*/

import { filter } from 'lodash/fp';
import { AssociationType, CaseStatuses } from '../../../../../case/common/api';
import { AssociationType, CaseStatuses, CaseType } from '../../../../../case/common/api';
import { Case, SubCase } from '../../containers/types';
import { statuses } from '../status';

export const isSelectedCasesIncludeCollections = (selectedCases: Case[]) =>
selectedCases.length > 0 &&
selectedCases.some((caseObj: Case) => caseObj.type === CaseType.collection);

export const isSubCase = (theCase: Case | SubCase): theCase is SubCase =>
(theCase as SubCase).caseParentId !== undefined &&
(theCase as SubCase).associationType === AssociationType.subCase;

export const isCollection = (theCase: Case | SubCase | null | undefined) =>
theCase != null && (theCase as Case).type === CaseType.collection;

export const isIndividual = (theCase: Case | SubCase | null | undefined) =>
theCase != null && (theCase as Case).type === CaseType.individual;

export const getSubCasesStatusCountsBadges = (
subCases: SubCase[]
): Array<{ name: CaseStatuses; color: string; count: number }> => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useUpdateCases } from '../../containers/use_bulk_update_case';
import { useGetActionLicense } from '../../containers/use_get_action_license';
import { getCasesColumns } from './columns';
import { AllCases } from '.';
import { StatusAll } from '../status';

jest.mock('../../containers/use_bulk_update_case');
jest.mock('../../containers/use_delete_cases');
Expand Down Expand Up @@ -111,6 +112,11 @@ describe('AllCases', () => {
});

it('should render AllCases', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
});

const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} />
Expand Down Expand Up @@ -144,6 +150,11 @@ describe('AllCases', () => {
});

it('should render the stats', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed },
});

const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} />
Expand Down Expand Up @@ -202,6 +213,7 @@ describe('AllCases', () => {
it('should render empty fields', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
data: {
...defaultGetCases.data,
cases: [
Expand Down Expand Up @@ -240,6 +252,78 @@ describe('AllCases', () => {
});
});

it('should render correct actions for case (with type individual and filter status open)', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
});
const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} />
</TestProviders>
);
wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click');
await waitFor(() => {
expect(wrapper.find('[data-test-subj="action-open"]').exists()).toBeFalsy();
expect(
wrapper.find('[data-test-subj="action-in-progress"]').first().props().disabled
).toBeFalsy();
expect(wrapper.find('[data-test-subj="action-close"]').first().props().disabled).toBeFalsy();
expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toBeFalsy();
});
});

it('should enable correct actions for sub cases', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
data: {
...defaultGetCases.data,
cases: [
{
...defaultGetCases.data.cases[0],
id: 'my-case-with-subcases',
createdAt: null,
createdBy: null,
status: null,
subCases: [
{
id: 'sub-case-id',
},
],
tags: null,
title: null,
totalComment: null,
totalAlerts: null,
type: CaseType.collection,
},
],
},
});
const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} />
</TestProviders>
);
await waitFor(() => {
wrapper
.find(
'[data-test-subj="sub-cases-table-my-case-with-subcases"] [data-test-subj="euiCollapsedItemActionsButton"]'
)
.last()
.simulate('click');
expect(wrapper.find('[data-test-subj="action-open"]').first().props().disabled).toEqual(true);
expect(
wrapper.find('[data-test-subj="action-in-progress"]').first().props().disabled
).toEqual(true);
expect(wrapper.find('[data-test-subj="action-close"]').first().props().disabled).toEqual(
true
);
expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toEqual(
false
);
});
});

it('should not render case link or actions on modal=true', async () => {
const wrapper = mount(
<TestProviders>
Expand Down Expand Up @@ -296,6 +380,15 @@ describe('AllCases', () => {
it('opens case when row action icon clicked', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
data: {
...defaultGetCases.data,
cases: [
{
...defaultGetCases.data.cases[0],
status: CaseStatuses.closed,
},
],
},
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed },
});

Expand All @@ -320,6 +413,7 @@ describe('AllCases', () => {
it('Bulk delete', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed },
selectedCases: useGetCasesMockState.data.cases,
});

Expand Down Expand Up @@ -355,9 +449,78 @@ describe('AllCases', () => {
});
});

it('Renders only bulk delete on status all', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
filterOptions: { ...defaultGetCases.filterOptions, status: StatusAll },
selectedCases: [...useGetCasesMockState.data.cases],
});

const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} />
</TestProviders>
);
await waitFor(() => {
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').exists()).toEqual(
false
);
expect(wrapper.find('[data-test-subj="cases-bulk-close-button"]').exists()).toEqual(false);
expect(
wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().props().disabled
).toEqual(false);
});
});

it('Renders correct bulk actoins for case collection when filter status is set to all - enable only bulk delete if any collection is selected', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
selectedCases: [
...useGetCasesMockState.data.cases,
{
...useGetCasesMockState.data.cases[0],
type: CaseType.collection,
},
],
});

useDeleteCasesMock
.mockReturnValueOnce({
...defaultDeleteCases,
isDisplayConfirmDeleteModal: false,
})
.mockReturnValue({
...defaultDeleteCases,
isDisplayConfirmDeleteModal: true,
});

const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} />
</TestProviders>
);
await waitFor(() => {
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false);
expect(
wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().props().disabled
).toEqual(true);
expect(
wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().props().disabled
).toEqual(true);
expect(
wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().props().disabled
).toEqual(false);
});
});

it('Bulk close status update', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
selectedCases: useGetCasesMockState.data.cases,
});

Expand Down
Loading

0 comments on commit da39fe0

Please sign in to comment.