diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index b993ae6602ae9..7ed9bfb3f2294 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -144,7 +144,7 @@ export interface FieldMappings { export type UpdateKey = keyof Pick< CasePatchRequest, - 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' + 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' | 'severity' >; export interface UpdateByKey { diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 10005b2c87bce..dbc57e163d3ff 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -198,6 +198,10 @@ export const MARKED_CASE_AS = i18n.translate('xpack.cases.caseView.markedCaseAs' defaultMessage: 'marked case as', }); +export const SET_SEVERITY_TO = i18n.translate('xpack.cases.caseView.setSeverityTo', { + defaultMessage: 'set severity to', +}); + export const OPEN_CASES = i18n.translate('xpack.cases.caseTable.openCases', { defaultMessage: 'Open cases', }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx index b9e4beb5d7e26..452601d142848 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx @@ -7,6 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; +import { CaseSeverity } from '../../../../common/api'; import { useConnectors } from '../../../containers/configure/use_connectors'; import { useCaseViewNavigation } from '../../../common/navigation'; import { UpdateKey, UseFetchAlertData } from '../../../../common/ui/types'; @@ -23,6 +24,7 @@ import * as i18n from '../translations'; import { getNoneConnector, normalizeActionConnector } from '../../configure_cases/utils'; import { getConnectorById } from '../../utils'; import { UseGetCaseUserActions } from '../../../containers/use_get_case_user_actions'; +import { SeveritySidebarSelector } from '../../severity/sidebar_selector'; export const CaseViewActivity = ({ initLoadingData, @@ -108,6 +110,12 @@ export const CaseViewActivity = ({ (newTags) => onUpdateField({ key: 'tags', value: newTags }), [onUpdateField] ); + + const onUpdateSeverity = useCallback( + (newSeverity: CaseSeverity) => onUpdateField({ key: 'severity', value: newSeverity }), + [onUpdateField] + ); + const { loading: isLoadingConnectors, connectors } = useConnectors(); const [connectorName, isValidConnector] = useMemo(() => { @@ -180,6 +188,12 @@ export const CaseViewActivity = ({ )} + (value); + if (caseData.severity !== value) { + callUpdate('severity', severityUpdate); + } default: return null; } diff --git a/x-pack/plugins/cases/public/components/severity/config.ts b/x-pack/plugins/cases/public/components/severity/config.ts new file mode 100644 index 0000000000000..945eb94640d3c --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/config.ts @@ -0,0 +1,29 @@ +/* + * 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 { euiLightVars } from '@kbn/ui-theme'; +import { CaseSeverity } from '../../../common/api'; +import { CRITICAL, HIGH, LOW, MEDIUM } from './translations'; + +export const severities = { + [CaseSeverity.LOW]: { + color: euiLightVars.euiColorVis0, + label: LOW, + }, + [CaseSeverity.MEDIUM]: { + color: euiLightVars.euiColorVis5, + label: MEDIUM, + }, + [CaseSeverity.HIGH]: { + color: euiLightVars.euiColorVis7, + label: HIGH, + }, + [CaseSeverity.CRITICAL]: { + color: euiLightVars.euiColorVis9, + label: CRITICAL, + }, +}; diff --git a/x-pack/plugins/cases/public/components/severity/selector.test.tsx b/x-pack/plugins/cases/public/components/severity/selector.test.tsx new file mode 100644 index 0000000000000..126dc64e7af1b --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/selector.test.tsx @@ -0,0 +1,60 @@ +/* + * 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 { CaseSeverity } from '../../../common/api'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { SeveritySelector } from './selector'; +import userEvent from '@testing-library/user-event'; + +describe('Severity field selector', () => { + const onSeverityChange = jest.fn(); + it('renders a list of severity fields', () => { + const result = render( + + ); + + expect(result.getByTestId('case-severity-selection')).toBeTruthy(); + expect(result.getAllByTestId('case-severity-selection-medium').length).toBeTruthy(); + }); + + it('renders a list of severity options when clicked', () => { + const result = render( + + ); + userEvent.click(result.getByTestId('case-severity-selection')); + expect(result.getByTestId('case-severity-selection-low')).toBeTruthy(); + expect(result.getAllByTestId('case-severity-selection-medium').length).toBeTruthy(); + expect(result.getByTestId('case-severity-selection-high')).toBeTruthy(); + expect(result.getByTestId('case-severity-selection-critical')).toBeTruthy(); + }); + + it('calls onSeverityChange with the newly selected severity when clicked', () => { + const result = render( + + ); + userEvent.click(result.getByTestId('case-severity-selection')); + expect(result.getByTestId('case-severity-selection-low')).toBeTruthy(); + userEvent.click(result.getByTestId('case-severity-selection-low')); + expect(onSeverityChange).toHaveBeenLastCalledWith('low'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/severity/selector.tsx b/x-pack/plugins/cases/public/components/severity/selector.tsx new file mode 100644 index 0000000000000..0d1ff4b319f2b --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/selector.tsx @@ -0,0 +1,64 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiSuperSelect, + EuiSuperSelectOption, +} from '@elastic/eui'; +import React from 'react'; +import { CaseSeverity } from '../../../common/api'; +import { severities } from './config'; + +interface Props { + selectedSeverity: CaseSeverity; + onSeverityChange: (status: CaseSeverity) => void; + isLoading: boolean; + isDisabled: boolean; +} + +export const SeveritySelector: React.FC = ({ + selectedSeverity, + onSeverityChange, + isLoading, + isDisabled, +}) => { + const caseSeverities = Object.keys(severities) as CaseSeverity[]; + const options: Array> = caseSeverities.map((severity) => { + const severityData = severities[severity]; + return { + value: severity, + inputDisplay: ( + + + {severityData.label} + + + ), + }; + }); + + return ( + + ); +}; +SeveritySelector.displayName = 'SeveritySelector'; diff --git a/x-pack/plugins/cases/public/components/severity/sidebar_selector.tsx b/x-pack/plugins/cases/public/components/severity/sidebar_selector.tsx new file mode 100644 index 0000000000000..ff591e342793f --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/sidebar_selector.tsx @@ -0,0 +1,43 @@ +/* + * 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 { EuiFlexItem, EuiHorizontalRule, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; +import { CaseSeverity } from '../../../common/api'; +import { SeveritySelector } from './selector'; +import { SEVERITY_TITLE } from './translations'; + +interface Props { + selectedSeverity: CaseSeverity; + onSeverityChange: (status: CaseSeverity) => void; + isLoading: boolean; + isDisabled: boolean; +} + +export const SeveritySidebarSelector: React.FC = ({ + selectedSeverity, + onSeverityChange, + isLoading, + isDisabled, +}) => { + return ( + + +

{SEVERITY_TITLE}

+
+ + + +
+ ); +}; +SeveritySidebarSelector.displayName = 'SeveritySidebarSelector'; diff --git a/x-pack/plugins/cases/public/components/severity/translations.ts b/x-pack/plugins/cases/public/components/severity/translations.ts new file mode 100644 index 0000000000000..b5982c70ed690 --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/translations.ts @@ -0,0 +1,28 @@ +/* + * 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'; + +export const LOW = i18n.translate('xpack.cases.severity.low', { + defaultMessage: 'Low', +}); + +export const MEDIUM = i18n.translate('xpack.cases.severity.medium', { + defaultMessage: 'Medium', +}); + +export const HIGH = i18n.translate('xpack.cases.severity.high', { + defaultMessage: 'High', +}); + +export const CRITICAL = i18n.translate('xpack.cases.severity.critical', { + defaultMessage: 'Critical', +}); + +export const SEVERITY_TITLE = i18n.translate('xpack.cases.severity.title', { + defaultMessage: 'Severity', +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/builder.tsx b/x-pack/plugins/cases/public/components/user_actions/builder.tsx index 019e37396a7ce..36298bbae601b 100644 --- a/x-pack/plugins/cases/public/components/user_actions/builder.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/builder.tsx @@ -10,6 +10,7 @@ import { createConnectorUserActionBuilder } from './connector'; import { createDescriptionUserActionBuilder } from './description'; import { createPushedUserActionBuilder } from './pushed'; import { createSettingsUserActionBuilder } from './settings'; +import { createSeverityUserActionBuilder } from './severity'; import { createStatusUserActionBuilder } from './status'; import { createTagsUserActionBuilder } from './tags'; import { createTitleUserActionBuilder } from './title'; @@ -20,10 +21,7 @@ export const builderMap: UserActionBuilderMap = { tags: createTagsUserActionBuilder, title: createTitleUserActionBuilder, status: createStatusUserActionBuilder, - // TODO: Build severity user action - severity: () => ({ - build: () => [], - }), + severity: createSeverityUserActionBuilder, pushed: createPushedUserActionBuilder, comment: createCommentUserActionBuilder, description: createDescriptionUserActionBuilder, diff --git a/x-pack/plugins/cases/public/components/user_actions/severity.test.tsx b/x-pack/plugins/cases/public/components/user_actions/severity.test.tsx new file mode 100644 index 0000000000000..d92a5cb5a153d --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/severity.test.tsx @@ -0,0 +1,39 @@ +/* + * 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 { EuiCommentList } from '@elastic/eui'; +import { Actions, CaseSeverity } from '../../../common/api'; +import React from 'react'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; +import { getUserAction } from '../../containers/mock'; +import { getMockBuilderArgs } from './mock'; +import { createSeverityUserActionBuilder } from './severity'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); + +const builderArgs = getMockBuilderArgs(); +describe('createSeverityUserActionBuilder', () => { + let appMockRenderer: AppMockRenderer; + beforeEach(() => { + appMockRenderer = createAppMockRenderer(); + }); + it('renders correctly', () => { + const userAction = getUserAction('severity', Actions.update, { + payload: { severity: CaseSeverity.LOW }, + }); + const builder = createSeverityUserActionBuilder({ + ...builderArgs, + userAction, + }); + const createdUserAction = builder.build(); + + const result = appMockRenderer.render(); + expect(result.getByTestId('severity-update-user-action-severity-title')).toBeTruthy(); + expect(result.getByTestId('severity-update-user-action-severity-title-low')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/severity.tsx b/x-pack/plugins/cases/public/components/user_actions/severity.tsx new file mode 100644 index 0000000000000..3e2cf8605b080 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/severity.tsx @@ -0,0 +1,53 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiHealth } from '@elastic/eui'; +import React from 'react'; +import { SeverityUserAction } from '../../../common/api/cases/user_actions/severity'; +import { SET_SEVERITY_TO } from '../create/translations'; +import { createCommonUpdateUserActionBuilder } from './common'; +import { UserActionBuilder, UserActionResponse } from './types'; +import { severities } from '../severity/config'; + +const getLabelTitle = (userAction: UserActionResponse) => { + const severity = userAction.payload.severity; + const severityData = severities[severity]; + if (severityData === undefined) { + return null; + } + return ( + + {SET_SEVERITY_TO} + + {severityData.label} + + + ); +}; + +export const createSeverityUserActionBuilder: UserActionBuilder = ({ + userAction, + handleOutlineComment, +}) => ({ + build: () => { + const severityUserAction = userAction as UserActionResponse; + const label = getLabelTitle(severityUserAction); + const commonBuilder = createCommonUpdateUserActionBuilder({ + userAction, + handleOutlineComment, + label, + icon: 'dot', + }); + + return commonBuilder.build(); + }, +});