[Security Solution] Case ui enhancement (#91863)

* 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 <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Christos Nasikas <christos.nasikas@elastic.co>
This commit is contained in:
Angela Chuang 2021-03-02 14:13:55 +00:00 committed by GitHub
parent 1bdf0022ee
commit 356d4609e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 315 additions and 86 deletions

View file

@ -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,
@ -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 },

View file

@ -28,7 +28,7 @@ export const addStatusFilter = ({
appendFilter,
type = CASE_SAVED_OBJECT,
}: {
status: CaseStatuses | undefined;
status?: CaseStatuses;
appendFilter?: string;
type?: string;
}) => {

View file

@ -46,6 +46,7 @@ import {
backToCases,
createCase,
fillCasesMandatoryfields,
filterStatusOpen,
} from '../../tasks/create_new_case';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
@ -74,6 +75,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');

View file

@ -25,6 +25,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"]';

View file

@ -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,
@ -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) => {

View file

@ -13,6 +13,7 @@ import { Case, SubCase } from '../../containers/types';
import { UpdateCase } from '../../containers/use_get_cases';
import { statuses } from '../status';
import * as i18n from './translations';
import { isIndividual } from './helpers';
interface GetActions {
caseStatus: string;
@ -20,16 +21,14 @@ interface GetActions {
deleteCaseOnClick: (deleteCase: Case) => void;
}
const hasSubCases = (subCases: SubCase[] | null | undefined) =>
subCases != null && subCases?.length > 0;
export const getActions = ({
caseStatus,
dispatchUpdate,
deleteCaseOnClick,
}: GetActions): Array<DefaultItemIconButtonAction<Case>> => {
const openCaseAction = {
available: (item: Case) => caseStatus !== CaseStatuses.open && !hasSubCases(item.subCases),
available: (item: Case | SubCase) => item.status !== CaseStatuses.open,
enabled: (item: Case | SubCase) => isIndividual(item),
description: statuses[CaseStatuses.open].actions.single.title,
icon: statuses[CaseStatuses.open].icon,
name: statuses[CaseStatuses.open].actions.single.title,
@ -45,8 +44,8 @@ export const getActions = ({
};
const makeInProgressAction = {
available: (item: Case) =>
caseStatus !== CaseStatuses['in-progress'] && !hasSubCases(item.subCases),
available: (item: Case) => item.status !== CaseStatuses['in-progress'],
enabled: (item: Case | SubCase) => isIndividual(item),
description: statuses[CaseStatuses['in-progress']].actions.single.title,
icon: statuses[CaseStatuses['in-progress']].icon,
name: statuses[CaseStatuses['in-progress']].actions.single.title,
@ -62,7 +61,8 @@ export const getActions = ({
};
const closeCaseAction = {
available: (item: Case) => caseStatus !== CaseStatuses.closed && !hasSubCases(item.subCases),
available: (item: Case | SubCase) => item.status !== CaseStatuses.closed,
enabled: (item: Case | SubCase) => isIndividual(item),
description: statuses[CaseStatuses.closed].actions.single.title,
icon: statuses[CaseStatuses.closed].icon,
name: statuses[CaseStatuses.closed].actions.single.title,
@ -78,6 +78,9 @@ export const getActions = ({
};
return [
openCaseAction,
makeInProgressAction,
closeCaseAction,
{
description: i18n.DELETE_CASE,
icon: 'trash',
@ -86,8 +89,5 @@ export const getActions = ({
type: 'icon',
'data-test-subj': 'action-delete',
},
openCaseAction,
makeInProgressAction,
closeCaseAction,
];
};

View file

@ -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';
@ -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} />;

View file

@ -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 }> => {

View file

@ -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');
@ -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} />
@ -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} />
@ -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: [
@ -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>
@ -297,6 +381,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 },
});
@ -342,6 +435,7 @@ describe('AllCases', () => {
it('Bulk delete', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed },
selectedCases: useGetCasesMockState.data.cases,
});
@ -377,9 +471,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,
});
@ -420,6 +583,7 @@ describe('AllCases', () => {
it('Bulk in-progress status update', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
selectedCases: useGetCasesMockState.data.cases,
});

View file

@ -54,7 +54,9 @@ import { SecurityPageName } from '../../../app/types';
import { useKibana } from '../../../common/lib/kibana';
import { APP_ID } from '../../../../common/constants';
import { Stats } from '../status';
import { SELECTABLE_MESSAGE_COLLECTIONS } from '../../translations';
import { getExpandedRowMap } from './expanded_row';
import { isSelectedCasesIncludeCollections } from './helpers';
const Div = styled.div`
margin-top: ${({ theme }) => theme.eui.paddingSizes.m};
@ -268,10 +270,17 @@ export const AllCases = React.memo<AllCasesProps>(
deleteCasesAction: toggleBulkDeleteModal,
selectedCaseIds,
updateCaseStatus: handleUpdateCaseStatus,
includeCollections: isSelectedCasesIncludeCollections(selectedCases),
})}
/>
),
[selectedCaseIds, filterOptions.status, toggleBulkDeleteModal, handleUpdateCaseStatus]
[
selectedCases,
selectedCaseIds,
filterOptions.status,
toggleBulkDeleteModal,
handleUpdateCaseStatus,
]
);
const handleDispatchUpdate = useCallback(
(args: Omit<UpdateCase, 'refetchCasesStatus'>) => {
@ -379,9 +388,8 @@ export const AllCases = React.memo<AllCasesProps>(
const euiBasicTableSelectionProps = useMemo<EuiTableSelectionType<Case>>(
() => ({
selectable: (theCase) => isEmpty(theCase.subCases),
onSelectionChange: setSelectedCases,
selectableMessage: (selectable) => (!selectable ? i18n.SELECTABLE_MESSAGE_COLLECTIONS : ''),
selectableMessage: (selectable) => (!selectable ? SELECTABLE_MESSAGE_COLLECTIONS : ''),
}),
[setSelectedCases]
);
@ -410,6 +418,8 @@ export const AllCases = React.memo<AllCasesProps>(
[isModal, onRowClick]
);
const enableBuckActions = userCanCrud && !isModal;
return (
<>
{!isEmpty(actionsErrors) && (
@ -506,10 +516,12 @@ export const AllCases = React.memo<AllCasesProps>(
</UtilityBarGroup>
{!isModal && (
<UtilityBarGroup data-test-subj="case-table-utility-bar-actions">
<UtilityBarText data-test-subj="case-table-selected-case-count">
{i18n.SHOWING_SELECTED_CASES(selectedCases.length)}
</UtilityBarText>
{userCanCrud && (
{enableBuckActions && (
<UtilityBarText data-test-subj="case-table-selected-case-count">
{i18n.SHOWING_SELECTED_CASES(selectedCases.length)}
</UtilityBarText>
)}
{enableBuckActions && (
<UtilityBarAction
data-test-subj="case-table-bulk-actions"
iconSide="right"
@ -529,7 +541,7 @@ export const AllCases = React.memo<AllCasesProps>(
<BasicTable
columns={memoizedGetCasesColumns}
data-test-subj="cases-table"
isSelectable={userCanCrud && !isModal}
isSelectable={enableBuckActions}
itemId="id"
items={data.cases}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
@ -556,7 +568,7 @@ export const AllCases = React.memo<AllCasesProps>(
onChange={tableOnChangeCallback}
pagination={memoizedPagination}
rowProps={tableRowProps}
selection={userCanCrud && !isModal ? euiBasicTableSelectionProps : undefined}
selection={enableBuckActions ? euiBasicTableSelectionProps : undefined}
sorting={sorting}
className={classnames({ isModal })}
/>

View file

@ -11,8 +11,10 @@ import { waitFor } from '@testing-library/react';
import { CaseStatuses } from '../../../../../case/common/api';
import { StatusFilter } from './status_filter';
import { StatusAll } from '../status';
const stats = {
[StatusAll]: 0,
[CaseStatuses.open]: 2,
[CaseStatuses['in-progress']]: 5,
[CaseStatuses.closed]: 7,

View file

@ -7,14 +7,13 @@
import React, { memo } from 'react';
import { EuiSuperSelect, EuiSuperSelectOption, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { CaseStatuses } from '../../../../../case/common/api';
import { Status, statuses } from '../status';
import { Status, statuses, StatusAll, CaseStatusWithAllStatus } from '../status';
interface Props {
stats: Record<CaseStatuses, number>;
selectedStatus: CaseStatuses;
onStatusChanged: (status: CaseStatuses) => void;
disabledStatuses?: CaseStatuses[];
stats: Record<CaseStatusWithAllStatus, number | null>;
selectedStatus: CaseStatusWithAllStatus;
onStatusChanged: (status: CaseStatusWithAllStatus) => void;
disabledStatuses?: CaseStatusWithAllStatus[];
}
const StatusFilterComponent: React.FC<Props> = ({
@ -23,15 +22,18 @@ const StatusFilterComponent: React.FC<Props> = ({
onStatusChanged,
disabledStatuses = [],
}) => {
const caseStatuses = Object.keys(statuses) as CaseStatuses[];
const options: Array<EuiSuperSelectOption<CaseStatuses>> = caseStatuses.map((status) => ({
const caseStatuses = Object.keys(statuses) as CaseStatusWithAllStatus[];
const options: Array<EuiSuperSelectOption<CaseStatusWithAllStatus>> = [
StatusAll,
...caseStatuses,
].map((status) => ({
value: status,
inputDisplay: (
<EuiFlexGroup gutterSize="xs" alignItems={'center'}>
<EuiFlexItem grow={false}>
<Status type={status} />
</EuiFlexItem>
<EuiFlexItem grow={false}>{` (${stats[status]})`}</EuiFlexItem>
{status !== StatusAll && <EuiFlexItem grow={false}>{` (${stats[status]})`}</EuiFlexItem>}
</EuiFlexGroup>
),
disabled: disabledStatuses.includes(status),

View file

@ -15,6 +15,7 @@ import { FilterOptions } from '../../containers/types';
import { useGetTags } from '../../containers/use_get_tags';
import { useGetReporters } from '../../containers/use_get_reporters';
import { FilterPopover } from '../filter_popover';
import { CaseStatusWithAllStatus, StatusAll } from '../status';
import { StatusFilter } from './status_filter';
import * as i18n from './translations';
@ -42,7 +43,12 @@ const StatusFilterWrapper = styled(EuiFlexItem)`
* @param onFilterChanged change listener to be notified on filter changes
*/
const defaultInitial = { search: '', reporters: [], status: CaseStatuses.open, tags: [] };
const defaultInitial = {
search: '',
reporters: [],
status: StatusAll,
tags: [],
};
const CasesTableFiltersComponent = ({
countClosedCases,
@ -126,7 +132,7 @@ const CasesTableFiltersComponent = ({
);
const onStatusChanged = useCallback(
(status: CaseStatuses) => {
(status: CaseStatusWithAllStatus) => {
onFilterChanged({ status });
},
[onFilterChanged]
@ -134,6 +140,7 @@ const CasesTableFiltersComponent = ({
const stats = useMemo(
() => ({
[StatusAll]: null,
[CaseStatuses.open]: countOpenCases ?? 0,
[CaseStatuses['in-progress']]: countInProgressCases ?? 0,
[CaseStatuses.closed]: countClosedCases ?? 0,

View file

@ -9,15 +9,16 @@ import React from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
import { CaseStatuses } from '../../../../../case/common/api';
import { statuses } from '../status';
import { statuses, CaseStatusWithAllStatus } from '../status';
import * as i18n from './translations';
interface GetBulkItems {
caseStatus: CaseStatuses;
caseStatus: CaseStatusWithAllStatus;
closePopover: () => void;
deleteCasesAction: (cases: string[]) => void;
selectedCaseIds: string[];
updateCaseStatus: (status: string) => void;
includeCollections: boolean;
}
export const getBulkItems = ({
@ -26,13 +27,14 @@ export const getBulkItems = ({
deleteCasesAction,
selectedCaseIds,
updateCaseStatus,
includeCollections,
}: GetBulkItems) => {
let statusMenuItems: JSX.Element[] = [];
const openMenuItem = (
<EuiContextMenuItem
data-test-subj="cases-bulk-open-button"
disabled={selectedCaseIds.length === 0}
disabled={selectedCaseIds.length === 0 || includeCollections}
key="cases-bulk-open-button"
icon={statuses[CaseStatuses.open].icon}
onClick={() => {
@ -47,7 +49,7 @@ export const getBulkItems = ({
const inProgressMenuItem = (
<EuiContextMenuItem
data-test-subj="cases-bulk-in-progress-button"
disabled={selectedCaseIds.length === 0}
disabled={selectedCaseIds.length === 0 || includeCollections}
key="cases-bulk-in-progress-button"
icon={statuses[CaseStatuses['in-progress']].icon}
onClick={() => {
@ -62,7 +64,7 @@ export const getBulkItems = ({
const closeMenuItem = (
<EuiContextMenuItem
data-test-subj="cases-bulk-close-button"
disabled={selectedCaseIds.length === 0}
disabled={selectedCaseIds.length === 0 || includeCollections}
key="cases-bulk-close-button"
icon={statuses[CaseStatuses.closed].icon}
onClick={() => {

View file

@ -8,8 +8,8 @@
import React, { memo, useCallback, useMemo, useState } from 'react';
import { memoize } from 'lodash/fp';
import { EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui';
import { CaseStatuses } from '../../../../../case/common/api';
import { Status, statuses } from '../status';
import { caseStatuses, CaseStatuses } from '../../../../../case/common/api';
import { Status } from '../status';
interface Props {
currentStatus: CaseStatuses;
@ -34,7 +34,6 @@ const StatusContextMenuComponent: React.FC<Props> = ({ currentStatus, onStatusCh
[closePopover, onStatusChanged]
);
const caseStatuses = Object.keys(statuses) as CaseStatuses[];
const panelItems = caseStatuses.map((status: CaseStatuses) => (
<EuiContextMenuItem
key={status}

View file

@ -4,36 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
import { CaseStatuses } from '../../../../../case/common/api';
import * as i18n from './translations';
import { AllCaseStatus, Statuses, StatusAll } from './types';
type Statuses = Record<
CaseStatuses,
{
color: string;
label: string;
icon: EuiIconType;
actions: {
bulk: {
title: string;
};
single: {
title: string;
description?: string;
};
};
actionBar: {
title: string;
};
button: {
label: string;
};
stats: {
title: string;
};
}
>;
export const allCaseStatus: AllCaseStatus = {
[StatusAll]: { color: 'hollow', label: i18n.ALL },
};
export const statuses: Statuses = {
[CaseStatuses.open]: {

View file

@ -8,3 +8,4 @@
export * from './status';
export * from './config';
export * from './stats';
export * from './types';

View file

@ -9,12 +9,12 @@ import React, { memo, useMemo } from 'react';
import { noop } from 'lodash/fp';
import { EuiBadge } from '@elastic/eui';
import { CaseStatuses } from '../../../../../case/common/api';
import { statuses } from './config';
import { allCaseStatus, statuses } from './config';
import { CaseStatusWithAllStatus, StatusAll } from './types';
import * as i18n from './translations';
interface Props {
type: CaseStatuses;
type: CaseStatusWithAllStatus;
withArrow?: boolean;
onClick?: () => void;
}
@ -22,7 +22,7 @@ interface Props {
const StatusComponent: React.FC<Props> = ({ type, withArrow = false, onClick = noop }) => {
const props = useMemo(
() => ({
color: statuses[type].color,
color: type === StatusAll ? allCaseStatus[StatusAll].color : statuses[type].color,
...(withArrow ? { iconType: 'arrowDown', iconSide: 'right' as const } : {}),
}),
[withArrow, type]
@ -35,7 +35,7 @@ const StatusComponent: React.FC<Props> = ({ type, withArrow = false, onClick = n
iconOnClickAriaLabel={i18n.STATUS_ICON_ARIA}
data-test-subj={`status-badge-${type}`}
>
{statuses[type].label}
{type === StatusAll ? allCaseStatus[StatusAll].label : statuses[type].label}
</EuiBadge>
);
};

View file

@ -8,6 +8,10 @@
import { i18n } from '@kbn/i18n';
export * from '../../translations';
export const ALL = i18n.translate('xpack.securitySolution.case.status.all', {
defaultMessage: 'All',
});
export const OPEN = i18n.translate('xpack.securitySolution.case.status.open', {
defaultMessage: 'Open',
});

View file

@ -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 { EuiIconType } from '@elastic/eui/src/components/icon/icon';
import { CaseStatuses } from '../../../../../case/common/api';
export const StatusAll = 'all' as const;
type StatusAllType = typeof StatusAll;
export type CaseStatusWithAllStatus = CaseStatuses | StatusAllType;
export type AllCaseStatus = Record<StatusAllType, { color: string; label: string }>;
export type Statuses = Record<
CaseStatuses,
{
color: string;
label: string;
icon: EuiIconType;
actions: {
bulk: {
title: string;
};
single: {
title: string;
description?: string;
};
};
actionBar: {
title: string;
};
button: {
label: string;
};
stats: {
title: string;
};
}
>;

View file

@ -137,7 +137,6 @@ describe('Case Configuration API', () => {
...DEFAULT_QUERY_PARAMS,
reporters: [],
tags: [],
status: CaseStatuses.open,
},
signal: abortCtrl.signal,
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { assign } from 'lodash';
import { assign, omit } from 'lodash';
import {
CasePatchRequest,
@ -14,7 +14,6 @@ import {
CasesFindResponse,
CasesResponse,
CasesStatusResponse,
CaseStatuses,
CaseType,
CaseUserActionsResponse,
CommentRequest,
@ -45,6 +44,7 @@ import {
} from '../../../../case/common/api/helpers';
import { KibanaServices } from '../../common/lib/kibana';
import { StatusAll } from '../components/status';
import {
ActionLicense,
@ -169,7 +169,7 @@ export const getCases = async ({
onlyCollectionType: false,
search: '',
reporters: [],
status: CaseStatuses.open,
status: StatusAll,
tags: [],
},
queryParams = {
@ -190,7 +190,7 @@ export const getCases = async ({
};
const response = await KibanaServices.get().http.fetch<CasesFindResponse>(`${CASES_URL}/_find`, {
method: 'GET',
query,
query: query.status === StatusAll ? omit(query, ['status']) : query,
signal,
});
return convertAllCasesToCamel(decodeCasesFindResponse(response));

View file

@ -17,11 +17,10 @@ import {
CaseType,
AssociationType,
} from '../../../../case/common/api';
import { CaseStatusWithAllStatus } from '../components/status';
export { CaseConnector, ActionConnector, CaseStatuses } from '../../../../case/common/api';
export type AllCaseType = AssociationType & CaseType;
export type Comment = CommentRequest & {
associationType: AssociationType;
id: string;
@ -96,7 +95,7 @@ export interface QueryParams {
export interface FilterOptions {
search: string;
status: CaseStatuses;
status: CaseStatusWithAllStatus;
tags: string[];
reporters: User[];
onlyCollectionType?: boolean;

View file

@ -6,12 +6,12 @@
*/
import { useCallback, useEffect, useReducer, useRef } from 'react';
import { CaseStatuses } from '../../../../case/common/api';
import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants';
import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case, UpdateByKey } from './types';
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
import * as i18n from './translations';
import { getCases, patchCase } from './api';
import { StatusAll } from '../components/status';
export interface UseGetCasesState {
data: AllCases;
@ -95,7 +95,7 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS
export const DEFAULT_FILTER_OPTIONS: FilterOptions = {
search: '',
reporters: [],
status: CaseStatuses.open,
status: StatusAll,
tags: [],
onlyCollectionType: false,
};