Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Case] Add the ability to mark cases in progress in cases table. #89341

Merged
merged 2 commits into from
Feb 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
import { case1 } from '../../objects/case';

import {
ALL_CASES_CLOSE_ACTION,
ALL_CASES_CLOSED_CASES_STATS,
ALL_CASES_COMMENTS_COUNT,
ALL_CASES_DELETE_ACTION,
ALL_CASES_IN_PROGRESS_CASES_STATS,
ALL_CASES_ITEM_ACTIONS_BTN,
ALL_CASES_NAME,
ALL_CASES_OPEN_CASES_COUNT,
ALL_CASES_OPEN_CASES_STATS,
Expand Down Expand Up @@ -91,8 +90,7 @@ describe('Cases', () => {
cy.get(ALL_CASES_COMMENTS_COUNT).should('have.text', '0');
cy.get(ALL_CASES_OPENED_ON).should('include.text', 'ago');
cy.get(ALL_CASES_SERVICE_NOW_INCIDENT).should('have.text', 'Not pushed');
cy.get(ALL_CASES_DELETE_ACTION).should('exist');
cy.get(ALL_CASES_CLOSE_ACTION).should('exist');
cy.get(ALL_CASES_ITEM_ACTIONS_BTN).should('exist');

goToCaseDetails();

Expand Down
6 changes: 2 additions & 4 deletions x-pack/plugins/security_solution/cypress/screens/all_cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ export const ALL_CASES_CASE = (id: string) => {
return `[data-test-subj="cases-table-row-${id}"]`;
};

export const ALL_CASES_CLOSE_ACTION = '[data-test-subj="action-close"]';

export const ALL_CASES_CLOSED_CASES_STATS = '[data-test-subj="closedStatsHeader"]';

export const ALL_CASES_COMMENTS_COUNT = '[data-test-subj="case-table-column-commentCount"]';
Expand All @@ -19,10 +17,10 @@ export const ALL_CASES_CREATE_NEW_CASE_BTN = '[data-test-subj="createNewCaseBtn"

export const ALL_CASES_CREATE_NEW_CASE_TABLE_BTN = '[data-test-subj="cases-table-add-case"]';

export const ALL_CASES_DELETE_ACTION = '[data-test-subj="action-delete"]';

export const ALL_CASES_IN_PROGRESS_CASES_STATS = '[data-test-subj="inProgressStatsHeader"]';

export const ALL_CASES_ITEM_ACTIONS_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]';

export const ALL_CASES_NAME = '[data-test-subj="case-details-link"]';

export const ALL_CASES_OPEN_CASES_COUNT = '[data-test-subj="case-status-filter"]';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_t
import { CaseStatuses } from '../../../../../case/common/api';
import { Case, SubCase } from '../../containers/types';
import { UpdateCase } from '../../containers/use_get_cases';
import { statuses } from '../status';
import * as i18n from './translations';

interface GetActions {
Expand All @@ -26,43 +27,67 @@ export const getActions = ({
caseStatus,
dispatchUpdate,
deleteCaseOnClick,
}: GetActions): Array<DefaultItemIconButtonAction<Case>> => [
{
description: i18n.DELETE_CASE,
icon: 'trash',
name: i18n.DELETE_CASE,
onClick: deleteCaseOnClick,
type: 'icon',
'data-test-subj': 'action-delete',
},
{
available: (item) => caseStatus === CaseStatuses.open && !hasSubCases(item.subCases),
description: i18n.CLOSE_CASE,
icon: 'folderCheck',
name: i18n.CLOSE_CASE,
}: GetActions): Array<DefaultItemIconButtonAction<Case>> => {
const openCaseAction = {
available: (item: Case) => caseStatus !== CaseStatuses.open && !hasSubCases(item.subCases),
description: statuses[CaseStatuses.open].actions.single.title,
icon: statuses[CaseStatuses.open].icon,
name: statuses[CaseStatuses.open].actions.single.title,
onClick: (theCase: Case) =>
dispatchUpdate({
updateKey: 'status',
updateValue: CaseStatuses.closed,
updateValue: CaseStatuses.open,
caseId: theCase.id,
version: theCase.version,
}),
type: 'icon',
'data-test-subj': 'action-close',
},
{
available: (item) => caseStatus !== CaseStatuses.open && !hasSubCases(item.subCases),
description: i18n.REOPEN_CASE,
icon: 'folderExclamation',
name: i18n.REOPEN_CASE,
type: 'icon' as const,
'data-test-subj': 'action-open',
};

const makeInProgressAction = {
available: (item: Case) =>
caseStatus !== CaseStatuses['in-progress'] && !hasSubCases(item.subCases),
description: statuses[CaseStatuses['in-progress']].actions.single.title,
icon: statuses[CaseStatuses['in-progress']].icon,
name: statuses[CaseStatuses['in-progress']].actions.single.title,
onClick: (theCase: Case) =>
dispatchUpdate({
updateKey: 'status',
updateValue: CaseStatuses.open,
updateValue: CaseStatuses['in-progress'],
caseId: theCase.id,
version: theCase.version,
}),
type: 'icon',
'data-test-subj': 'action-open',
},
];
type: 'icon' as const,
'data-test-subj': 'action-in-progress',
};

const closeCaseAction = {
available: (item: Case) => caseStatus !== CaseStatuses.closed && !hasSubCases(item.subCases),
description: statuses[CaseStatuses.closed].actions.single.title,
icon: statuses[CaseStatuses.closed].icon,
name: statuses[CaseStatuses.closed].actions.single.title,
onClick: (theCase: Case) =>
dispatchUpdate({
updateKey: 'status',
updateValue: CaseStatuses.closed,
caseId: theCase.id,
version: theCase.version,
}),
type: 'icon' as const,
'data-test-subj': 'action-close',
};

return [
{
description: i18n.DELETE_CASE,
icon: 'trash',
name: i18n.DELETE_CASE,
onClick: deleteCaseOnClick,
type: 'icon',
'data-test-subj': 'action-delete',
},
openCaseAction,
makeInProgressAction,
closeCaseAction,
];
};
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ describe('AllCases', () => {
</TestProviders>
);
await waitFor(() => {
wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click');
wrapper.find('[data-test-subj="action-close"]').first().simulate('click');
const firstCase = useGetCasesMockState.data.cases[0];
expect(dispatchUpdateCaseProperty).toBeCalledWith({
Expand All @@ -305,6 +306,7 @@ describe('AllCases', () => {
</TestProviders>
);
await waitFor(() => {
wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click');
wrapper.find('[data-test-subj="action-open"]').first().simulate('click');
const firstCase = useGetCasesMockState.data.cases[0];
expect(dispatchUpdateCaseProperty).toBeCalledWith({
Expand All @@ -317,6 +319,26 @@ describe('AllCases', () => {
});
});

it('put case in progress when row action icon clicked', async () => {
const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} />
</TestProviders>
);
await waitFor(() => {
wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click');
wrapper.find('[data-test-subj="action-in-progress"]').first().simulate('click');
const firstCase = useGetCasesMockState.data.cases[0];
expect(dispatchUpdateCaseProperty).toBeCalledWith({
caseId: firstCase.id,
updateKey: 'status',
updateValue: CaseStatuses['in-progress'],
refetchCasesStatus: fetchCasesStatus,
version: firstCase.version,
});
});
});

it('Bulk delete', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
Expand Down Expand Up @@ -395,6 +417,27 @@ describe('AllCases', () => {
});
});

it('Bulk in-progress status update', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
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');
wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().simulate('click');
expect(updateBulkStatus).toBeCalledWith(
useGetCasesMockState.data.cases,
CaseStatuses['in-progress']
);
});
});

it('isDeleted is true, refetch', async () => {
useDeleteCasesMock.mockReturnValue({
...defaultDeleteCases,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import React from 'react';
import { EuiContextMenuItem } from '@elastic/eui';

import { CaseStatuses } from '../../../../../case/common/api';
import { statuses } from '../status';
import * as i18n from './translations';

interface GetBulkItems {
caseStatus: string;
caseStatus: CaseStatuses;
closePopover: () => void;
deleteCasesAction: (cases: string[]) => void;
selectedCaseIds: string[];
Expand All @@ -26,34 +27,72 @@ export const getBulkItems = ({
selectedCaseIds,
updateCaseStatus,
}: GetBulkItems) => {
let statusMenuItems: JSX.Element[] = [];

const openMenuItem = (
<EuiContextMenuItem
data-test-subj="cases-bulk-open-button"
disabled={selectedCaseIds.length === 0}
key="cases-bulk-open-button"
icon={statuses[CaseStatuses.open].icon}
onClick={() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please put into useCallback

Copy link
Member Author

@cnasikas cnasikas Feb 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get this error. I think is ok to leave it as it is as I have refactor to make it work.

React Hook "useCallback" is called in function "getBulkItems" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter.

closePopover();
updateCaseStatus(CaseStatuses.open);
}}
>
{statuses[CaseStatuses.open].actions.bulk.title}
</EuiContextMenuItem>
);

const inProgressMenuItem = (
<EuiContextMenuItem
data-test-subj="cases-bulk-in-progress-button"
disabled={selectedCaseIds.length === 0}
key="cases-bulk-in-progress-button"
icon={statuses[CaseStatuses['in-progress']].icon}
onClick={() => {
closePopover();
updateCaseStatus(CaseStatuses['in-progress']);
}}
>
{statuses[CaseStatuses['in-progress']].actions.bulk.title}
</EuiContextMenuItem>
);

const closeMenuItem = (
<EuiContextMenuItem
data-test-subj="cases-bulk-close-button"
disabled={selectedCaseIds.length === 0}
key="cases-bulk-close-button"
icon={statuses[CaseStatuses.closed].icon}
onClick={() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please put into useCallback

closePopover();
updateCaseStatus(CaseStatuses.closed);
}}
>
{statuses[CaseStatuses.closed].actions.bulk.title}
</EuiContextMenuItem>
);

switch (caseStatus) {
case CaseStatuses.open:
statusMenuItems = [inProgressMenuItem, closeMenuItem];
break;

case CaseStatuses['in-progress']:
statusMenuItems = [openMenuItem, closeMenuItem];
break;

case CaseStatuses.closed:
statusMenuItems = [openMenuItem, inProgressMenuItem];
break;

default:
break;
}

return [
caseStatus === CaseStatuses.open ? (
<EuiContextMenuItem
data-test-subj="cases-bulk-close-button"
disabled={selectedCaseIds.length === 0}
key={i18n.BULK_ACTION_CLOSE_SELECTED}
icon="folderCheck"
onClick={() => {
closePopover();
updateCaseStatus(CaseStatuses.closed);
}}
>
{i18n.BULK_ACTION_CLOSE_SELECTED}
</EuiContextMenuItem>
) : (
<EuiContextMenuItem
data-test-subj="cases-bulk-open-button"
disabled={selectedCaseIds.length === 0}
key={i18n.BULK_ACTION_OPEN_SELECTED}
icon="folderExclamation"
onClick={() => {
closePopover();
updateCaseStatus(CaseStatuses.open);
}}
>
{i18n.BULK_ACTION_OPEN_SELECTED}
</EuiContextMenuItem>
),
...statusMenuItems,
<EuiContextMenuItem
data-test-subj="cases-bulk-delete-button"
key={i18n.BULK_ACTION_DELETE_SELECTED}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,6 @@

import { i18n } from '@kbn/i18n';

export const BULK_ACTION_CLOSE_SELECTED = i18n.translate(
'xpack.securitySolution.case.caseTable.bulkActions.closeSelectedTitle',
{
defaultMessage: 'Close selected',
}
);

export const BULK_ACTION_OPEN_SELECTED = i18n.translate(
'xpack.securitySolution.case.caseTable.bulkActions.openSelectedTitle',
{
defaultMessage: 'Reopen selected',
}
);

export const BULK_ACTION_DELETE_SELECTED = i18n.translate(
'xpack.securitySolution.case.caseTable.bulkActions.deleteSelectedTitle',
{
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ describe('StatusActionButton', () => {

expect(
wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType')
).toBe('folderCheck');
).toBe('folderClosed');
});

it('it renders the correct button icon: status closed', () => {
const wrapper = mount(<StatusActionButton {...defaultProps} status={CaseStatuses.closed} />);

expect(
wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType')
).toBe('folderCheck');
).toBe('folderOpen');
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const StatusActionButtonComponent: React.FC<Props> = ({
return (
<EuiButton
data-test-subj="case-view-status-action-button"
iconType={statuses[caseStatuses[nextStatusIndex]].button.icon}
iconType={statuses[caseStatuses[nextStatusIndex]].icon}
isDisabled={disabled}
isLoading={isLoading}
onClick={onClick}
Expand Down
Loading