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

[Cases] Add Severity bulk action & row action #142826

Merged
merged 8 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
@@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';
import { CaseSeverity } from '../../../../common/api';
import { severities } from '../../severity/config';

const SET_SEVERITY = ({
totalCases,
severity,
caseTitle,
}: {
totalCases: number;
severity: string;
caseTitle?: string;
}) =>
i18n.translate('xpack.cases.containers.markInProgressCases', {
values: { caseTitle, totalCases, severity },
defaultMessage:
'{totalCases, plural, =1 {Case "{caseTitle}" was} other {{totalCases} cases were}} set to {severity}',
Copy link
Contributor

Choose a reason for hiding this comment

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

if caseTitle is undefined will it show Case "undefined" was?

Copy link
Member Author

@cnasikas cnasikas Oct 6, 2022

Choose a reason for hiding this comment

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

The case is a mandatory field and it will never be undefined. Types also protect us from that, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

I might be misunderstanding but I'm referring to caseTitle?: string;, so it's optional and can be undefined.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, sorry my mistake 🙂. It is a pattern we had before that I followed. The title will only show if you have one case. Indeed, it is possible to show "undefined" if totalCases === 1 and caseTitlte === undefined. Do you want me to change the pattern to something else? Any ideas? I am not super fun of it for the reason you mentioned.

});

export const SET_SEVERITY_LOW = ({
totalCases,
caseTitle,
}: {
totalCases: number;
caseTitle?: string;
}) =>
SET_SEVERITY({
totalCases,
caseTitle,
severity: severities[CaseSeverity.LOW].label,
});

export const SET_SEVERITY_MEDIUM = ({
totalCases,
caseTitle,
}: {
totalCases: number;
caseTitle?: string;
}) =>
SET_SEVERITY({
totalCases,
caseTitle,
severity: severities[CaseSeverity.MEDIUM].label,
});

export const SET_SEVERITY_HIGH = ({
totalCases,
caseTitle,
}: {
totalCases: number;
caseTitle?: string;
}) =>
SET_SEVERITY({
totalCases,
caseTitle,
severity: severities[CaseSeverity.HIGH].label,
});

export const SET_SEVERITY_CRITICAL = ({
totalCases,
caseTitle,
}: {
totalCases: number;
caseTitle?: string;
}) =>
SET_SEVERITY({
totalCases,
caseTitle,
severity: severities[CaseSeverity.CRITICAL].label,
});

export const BULK_ACTION_STATUS_CLOSE = i18n.translate(
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
'xpack.cases.caseTable.bulkActions.status.close',
{
defaultMessage: 'Close selected',
}
);

export const BULK_ACTION_STATUS_OPEN = i18n.translate(
'xpack.cases.caseTable.bulkActions.status.open',
{
defaultMessage: 'Open selected',
}
);

export const BULK_ACTION_STATUS_IN_PROGRESS = i18n.translate(
'xpack.cases.caseTable.bulkActions.status.inProgress',
{
defaultMessage: 'Mark in progress',
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { AppMockRenderer, createAppMockRenderer } from '../../../common/mock';
import { act, renderHook } from '@testing-library/react-hooks';
import { useSeverityAction } from './use_severity_action';

import * as api from '../../../containers/api';
import { basicCase } from '../../../containers/mock';
import { CaseSeverity } from '../../../../common/api';

jest.mock('../../../containers/api');

describe('useSeverityAction', () => {
let appMockRender: AppMockRenderer;
const onAction = jest.fn();
const onActionSuccess = jest.fn();

beforeEach(() => {
appMockRender = createAppMockRenderer();
jest.clearAllMocks();
});

it('renders an action', async () => {
const { result } = renderHook(
() =>
useSeverityAction({
onAction,
onActionSuccess,
isDisabled: false,
}),
{
wrapper: appMockRender.AppWrapper,
}
);

expect(result.current.getActions([basicCase])).toMatchInlineSnapshot(`
Array [
Object {
"data-test-subj": "cases-bulk-action-severity-low",
"disabled": true,
"icon": "empty",
"key": "cases-bulk-action-severity-low",
"name": "Low",
"onClick": [Function],
},
Object {
"data-test-subj": "cases-bulk-action-severity-medium",
"disabled": false,
"icon": "empty",
"key": "cases-bulk-action-severity-medium",
"name": "Medium",
"onClick": [Function],
},
Object {
"data-test-subj": "cases-bulk-action-severity-high",
"disabled": false,
"icon": "empty",
"key": "cases-bulk-action-severity-high",
"name": "High",
"onClick": [Function],
},
Object {
"data-test-subj": "cases-bulk-action-severity-critical",
"disabled": false,
"icon": "empty",
"key": "cases-bulk-action-severity-critical",
"name": "Critical",
"onClick": [Function],
},
]
`);
});

it('update the severity cases', async () => {
const updateSpy = jest.spyOn(api, 'updateCases');

const { result, waitFor } = renderHook(
() => useSeverityAction({ onAction, onActionSuccess, isDisabled: false }),
{
wrapper: appMockRender.AppWrapper,
}
);

const actions = result.current.getActions([basicCase]);

for (const [index, severity] of [
CaseSeverity.LOW,
CaseSeverity.MEDIUM,
CaseSeverity.HIGH,
CaseSeverity.CRITICAL,
].entries()) {
act(() => {
// @ts-expect-error: onClick expects a MouseEvent argument
actions[index]!.onClick();
});

await waitFor(() => {
expect(onAction).toHaveBeenCalled();
expect(onActionSuccess).toHaveBeenCalled();
expect(updateSpy).toHaveBeenCalledWith(
[{ severity, id: basicCase.id, version: basicCase.version }],
expect.anything()
);
});
}
});

const singleCaseTests = [
[CaseSeverity.LOW, 0, 'Case "Another horrible breach!!" was set to Low'],
[CaseSeverity.MEDIUM, 1, 'Case "Another horrible breach!!" was set to Medium'],
[CaseSeverity.HIGH, 2, 'Case "Another horrible breach!!" was set to High'],
[CaseSeverity.CRITICAL, 3, 'Case "Another horrible breach!!" was set to Critical'],
];

it.each(singleCaseTests)(
'shows the success toaster correctly when updating the severity of the case: %s',
async (_, index, expectedMessage) => {
const { result, waitFor } = renderHook(
() => useSeverityAction({ onAction, onActionSuccess, isDisabled: false }),
{
wrapper: appMockRender.AppWrapper,
}
);

const actions = result.current.getActions([basicCase]);

act(() => {
// @ts-expect-error: onClick expects a MouseEvent argument
actions[index]!.onClick();
});

await waitFor(() => {
expect(appMockRender.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith(
expectedMessage
);
});
}
);

const multipleCasesTests: Array<[CaseSeverity, number, string]> = [
[CaseSeverity.LOW, 0, '2 cases were set to Low'],
[CaseSeverity.MEDIUM, 1, '2 cases were set to Medium'],
[CaseSeverity.HIGH, 2, '2 cases were set to High'],
[CaseSeverity.CRITICAL, 3, '2 cases were set to Critical'],
];

it.each(multipleCasesTests)(
'shows the success toaster correctly when updating the severity of the case: %s',
async (_, index, expectedMessage) => {
const { result, waitFor } = renderHook(
() => useSeverityAction({ onAction, onActionSuccess, isDisabled: false }),
{
wrapper: appMockRender.AppWrapper,
}
);

const actions = result.current.getActions([basicCase, basicCase]);

act(() => {
// @ts-expect-error: onClick expects a MouseEvent argument
actions[index]!.onClick();
});

await waitFor(() => {
expect(appMockRender.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith(
expectedMessage
);
});
}
);

const disabledTests: Array<[CaseSeverity, number]> = [
[CaseSeverity.LOW, 0],
[CaseSeverity.MEDIUM, 1],
[CaseSeverity.HIGH, 2],
[CaseSeverity.CRITICAL, 3],
];

it.each(disabledTests)('disables the severity button correctly: %s', async (severity, index) => {
const { result } = renderHook(
() => useSeverityAction({ onAction, onActionSuccess, isDisabled: false }),
{
wrapper: appMockRender.AppWrapper,
}
);

const actions = result.current.getActions([{ ...basicCase, severity }]);
expect(actions[index].disabled).toBe(true);
});

it.each(disabledTests)(
'disables the severity button correctly if isDisabled=true: %s',
async (severity, index) => {
const { result } = renderHook(
() => useSeverityAction({ onAction, onActionSuccess, isDisabled: true }),
{
wrapper: appMockRender.AppWrapper,
}
);

const actions = result.current.getActions([basicCase]);
expect(actions[index].disabled).toBe(true);
}
);
});
Loading