[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:
parent
1bdf0022ee
commit
356d4609e8
24 changed files with 315 additions and 86 deletions
|
@ -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 },
|
||||
|
|
|
@ -28,7 +28,7 @@ export const addStatusFilter = ({
|
|||
appendFilter,
|
||||
type = CASE_SAVED_OBJECT,
|
||||
}: {
|
||||
status: CaseStatuses | undefined;
|
||||
status?: CaseStatuses;
|
||||
appendFilter?: string;
|
||||
type?: string;
|
||||
}) => {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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"]';
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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 }> => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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 })}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]: {
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
export * from './status';
|
||||
export * from './config';
|
||||
export * from './stats';
|
||||
export * from './types';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
>;
|
|
@ -137,7 +137,6 @@ describe('Case Configuration API', () => {
|
|||
...DEFAULT_QUERY_PARAMS,
|
||||
reporters: [],
|
||||
tags: [],
|
||||
status: CaseStatuses.open,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue