[Security Solution][Case] Add in-progress
status to case (#84321)
This commit is contained in:
parent
554ee9ebf9
commit
fcccb016f4
|
@ -15,12 +15,24 @@ import { CaseConnectorRt, ESCaseConnector, ConnectorPartialFieldsRt } from '../c
|
|||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
export { ActionTypeExecutorResult } from '../../../../actions/server/types';
|
||||
|
||||
const StatusRt = rt.union([rt.literal('open'), rt.literal('closed')]);
|
||||
export enum CaseStatuses {
|
||||
open = 'open',
|
||||
'in-progress' = 'in-progress',
|
||||
closed = 'closed',
|
||||
}
|
||||
|
||||
const CaseStatusRt = rt.union([
|
||||
rt.literal(CaseStatuses.open),
|
||||
rt.literal(CaseStatuses['in-progress']),
|
||||
rt.literal(CaseStatuses.closed),
|
||||
]);
|
||||
|
||||
export const caseStatuses = Object.values(CaseStatuses);
|
||||
|
||||
const CaseBasicRt = rt.type({
|
||||
connector: CaseConnectorRt,
|
||||
description: rt.string,
|
||||
status: StatusRt,
|
||||
status: CaseStatusRt,
|
||||
tags: rt.array(rt.string),
|
||||
title: rt.string,
|
||||
});
|
||||
|
@ -68,7 +80,7 @@ export const CaseExternalServiceRequestRt = CaseExternalServiceBasicRt;
|
|||
|
||||
export const CasesFindRequestRt = rt.partial({
|
||||
tags: rt.union([rt.array(rt.string), rt.string]),
|
||||
status: StatusRt,
|
||||
status: CaseStatusRt,
|
||||
reporters: rt.union([rt.array(rt.string), rt.string]),
|
||||
defaultSearchOperator: rt.union([rt.literal('AND'), rt.literal('OR')]),
|
||||
fields: rt.array(rt.string),
|
||||
|
@ -177,7 +189,6 @@ export type CasesResponse = rt.TypeOf<typeof CasesResponseRt>;
|
|||
export type CasesFindResponse = rt.TypeOf<typeof CasesFindResponseRt>;
|
||||
export type CasePatchRequest = rt.TypeOf<typeof CasePatchRequestRt>;
|
||||
export type CasesPatchRequest = rt.TypeOf<typeof CasesPatchRequestRt>;
|
||||
export type Status = rt.TypeOf<typeof StatusRt>;
|
||||
export type CaseExternalServiceRequest = rt.TypeOf<typeof CaseExternalServiceRequestRt>;
|
||||
export type ServiceConnectorCaseParams = rt.TypeOf<typeof ServiceConnectorCaseParamsRt>;
|
||||
export type ServiceConnectorCaseResponse = rt.TypeOf<typeof ServiceConnectorCaseResponseRt>;
|
||||
|
|
|
@ -8,6 +8,7 @@ import * as rt from 'io-ts';
|
|||
|
||||
export const CasesStatusResponseRt = rt.type({
|
||||
count_open_cases: rt.number,
|
||||
count_in_progress_cases: rt.number,
|
||||
count_closed_cases: rt.number,
|
||||
});
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ConnectorTypes, CasePostRequest } from '../../../common/api';
|
||||
import { ConnectorTypes, CasePostRequest, CaseStatuses } from '../../../common/api';
|
||||
|
||||
import {
|
||||
createMockSavedObjectsRepository,
|
||||
|
@ -60,7 +60,7 @@ describe('create', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
|
@ -126,7 +126,7 @@ describe('create', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
|
@ -169,7 +169,7 @@ describe('create', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
|
@ -316,7 +316,7 @@ describe('create', () => {
|
|||
title: 'a title',
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
tags: ['defacement'],
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ConnectorTypes, CasesPatchRequest } from '../../../common/api';
|
||||
import { ConnectorTypes, CasesPatchRequest, CaseStatuses } from '../../../common/api';
|
||||
import {
|
||||
createMockSavedObjectsRepository,
|
||||
mockCaseNoConnectorId,
|
||||
|
@ -27,7 +27,7 @@ describe('update', () => {
|
|||
cases: [
|
||||
{
|
||||
id: 'mock-id-1',
|
||||
status: 'closed' as const,
|
||||
status: CaseStatuses.closed,
|
||||
version: 'WzAsMV0=',
|
||||
},
|
||||
],
|
||||
|
@ -56,7 +56,7 @@ describe('update', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
id: 'mock-id-1',
|
||||
external_service: null,
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
tags: ['defacement'],
|
||||
title: 'Super Bad Security Issue',
|
||||
totalComment: 0,
|
||||
|
@ -79,8 +79,8 @@ describe('update', () => {
|
|||
username: 'awesome',
|
||||
},
|
||||
action_field: ['status'],
|
||||
new_value: 'closed',
|
||||
old_value: 'open',
|
||||
new_value: CaseStatuses.closed,
|
||||
old_value: CaseStatuses.open,
|
||||
},
|
||||
references: [
|
||||
{
|
||||
|
@ -98,7 +98,7 @@ describe('update', () => {
|
|||
cases: [
|
||||
{
|
||||
id: 'mock-id-1',
|
||||
status: 'open' as const,
|
||||
status: CaseStatuses.open,
|
||||
version: 'WzAsMV0=',
|
||||
},
|
||||
],
|
||||
|
@ -106,7 +106,10 @@ describe('update', () => {
|
|||
|
||||
const savedObjectsClient = createMockSavedObjectsRepository({
|
||||
caseSavedObject: [
|
||||
{ ...mockCases[0], attributes: { ...mockCases[0].attributes, status: 'closed' } },
|
||||
{
|
||||
...mockCases[0],
|
||||
attributes: { ...mockCases[0].attributes, status: CaseStatuses.closed },
|
||||
},
|
||||
...mockCases.slice(1),
|
||||
],
|
||||
});
|
||||
|
@ -130,7 +133,7 @@ describe('update', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
id: 'mock-id-1',
|
||||
external_service: null,
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
title: 'Super Bad Security Issue',
|
||||
totalComment: 0,
|
||||
|
@ -146,7 +149,7 @@ describe('update', () => {
|
|||
cases: [
|
||||
{
|
||||
id: 'mock-no-connector_id',
|
||||
status: 'closed' as const,
|
||||
status: CaseStatuses.closed,
|
||||
version: 'WzAsMV0=',
|
||||
},
|
||||
],
|
||||
|
@ -177,7 +180,7 @@ describe('update', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
tags: ['defacement'],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
|
||||
|
@ -231,7 +234,7 @@ describe('update', () => {
|
|||
description: 'Oh no, a bad meanie going LOLBins all over the place!',
|
||||
external_service: null,
|
||||
title: 'Another bad one',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['LOLBins'],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: {
|
||||
|
@ -314,7 +317,7 @@ describe('update', () => {
|
|||
cases: [
|
||||
{
|
||||
id: 'mock-id-1',
|
||||
status: 'open' as const,
|
||||
status: CaseStatuses.open,
|
||||
version: 'WzAsMV0=',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
ESCasePatchRequest,
|
||||
CasePatchRequest,
|
||||
CasesResponse,
|
||||
CaseStatuses,
|
||||
} from '../../../common/api';
|
||||
import { buildCaseUserActions } from '../../services/user_actions/helpers';
|
||||
import {
|
||||
|
@ -98,12 +99,15 @@ export const update = ({
|
|||
cases: updateFilterCases.map((thisCase) => {
|
||||
const { id: caseId, version, ...updateCaseAttributes } = thisCase;
|
||||
let closedInfo = {};
|
||||
if (updateCaseAttributes.status && updateCaseAttributes.status === 'closed') {
|
||||
if (updateCaseAttributes.status && updateCaseAttributes.status === CaseStatuses.closed) {
|
||||
closedInfo = {
|
||||
closed_at: updatedDt,
|
||||
closed_by: { email, full_name, username },
|
||||
};
|
||||
} else if (updateCaseAttributes.status && updateCaseAttributes.status === 'open') {
|
||||
} else if (
|
||||
updateCaseAttributes.status &&
|
||||
updateCaseAttributes.status === CaseStatuses.open
|
||||
) {
|
||||
closedInfo = {
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Logger } from '../../../../../../src/core/server';
|
|||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
import { actionsMock } from '../../../../actions/server/mocks';
|
||||
import { validateParams } from '../../../../actions/server/lib';
|
||||
import { ConnectorTypes, CommentType } from '../../../common/api';
|
||||
import { ConnectorTypes, CommentType, CaseStatuses } from '../../../common/api';
|
||||
import {
|
||||
createCaseServiceMock,
|
||||
createConfigureServiceMock,
|
||||
|
@ -785,7 +785,7 @@ describe('case connector', () => {
|
|||
tags: ['case', 'connector'],
|
||||
description: 'Yo fields!!',
|
||||
external_service: null,
|
||||
status: 'open' as const,
|
||||
status: CaseStatuses.open,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
version: 'WzksMV0=',
|
||||
|
@ -868,7 +868,7 @@ describe('case connector', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
id: 'mock-id-1',
|
||||
external_service: null,
|
||||
status: 'open' as const,
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
title: 'Update title',
|
||||
totalComment: 0,
|
||||
|
@ -937,7 +937,7 @@ describe('case connector', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open' as const,
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
ESCaseAttributes,
|
||||
ConnectorTypes,
|
||||
CommentType,
|
||||
CaseStatuses,
|
||||
} from '../../../../common/api';
|
||||
|
||||
export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
||||
|
@ -35,7 +36,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: {
|
||||
|
@ -69,7 +70,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
|||
description: 'Oh no, a bad meanie destroying data!',
|
||||
external_service: null,
|
||||
title: 'Damaging Data Destruction Detected',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['Data Destruction'],
|
||||
updated_at: '2019-11-25T22:32:00.900Z',
|
||||
updated_by: {
|
||||
|
@ -107,7 +108,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
|||
description: 'Oh no, a bad meanie going LOLBins all over the place!',
|
||||
external_service: null,
|
||||
title: 'Another bad one',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['LOLBins'],
|
||||
updated_at: '2019-11-25T22:32:17.947Z',
|
||||
updated_by: {
|
||||
|
@ -148,7 +149,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
|||
},
|
||||
description: 'Oh no, a bad meanie going LOLBins all over the place!',
|
||||
external_service: null,
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
title: 'Another bad one',
|
||||
tags: ['LOLBins'],
|
||||
updated_at: '2019-11-25T22:32:17.947Z',
|
||||
|
@ -179,7 +180,7 @@ export const mockCaseNoConnectorId: SavedObject<Partial<ESCaseAttributes>> = {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: {
|
||||
|
|
|
@ -38,6 +38,10 @@ describe('FIND all cases', () => {
|
|||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.cases).toHaveLength(4);
|
||||
// mockSavedObjectsRepository do not support filters and returns all cases every time.
|
||||
expect(response.payload.count_open_cases).toEqual(4);
|
||||
expect(response.payload.count_closed_cases).toEqual(4);
|
||||
expect(response.payload.count_in_progress_cases).toEqual(4);
|
||||
});
|
||||
|
||||
it(`has proper connector id on cases with configured connector`, async () => {
|
||||
|
|
|
@ -11,7 +11,13 @@ import { fold } from 'fp-ts/lib/Either';
|
|||
import { identity } from 'fp-ts/lib/function';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { CasesFindResponseRt, CasesFindRequestRt, throwErrors } from '../../../../common/api';
|
||||
import {
|
||||
CasesFindResponseRt,
|
||||
CasesFindRequestRt,
|
||||
throwErrors,
|
||||
CaseStatuses,
|
||||
caseStatuses,
|
||||
} from '../../../../common/api';
|
||||
import { transformCases, sortToSnake, wrapError, escapeHatch } from '../utils';
|
||||
import { RouteDeps, TotalCommentByCase } from '../types';
|
||||
import { CASE_SAVED_OBJECT } from '../../../saved_object_types';
|
||||
|
@ -20,7 +26,7 @@ import { CASES_URL } from '../../../../common/constants';
|
|||
const combineFilters = (filters: string[], operator: 'OR' | 'AND'): string =>
|
||||
filters?.filter((i) => i !== '').join(` ${operator} `);
|
||||
|
||||
const getStatusFilter = (status: 'open' | 'closed', appendFilter?: string) =>
|
||||
const getStatusFilter = (status: CaseStatuses, appendFilter?: string) =>
|
||||
`${CASE_SAVED_OBJECT}.attributes.status: ${status}${
|
||||
!isEmpty(appendFilter) ? ` AND ${appendFilter}` : ''
|
||||
}`;
|
||||
|
@ -75,30 +81,21 @@ export function initFindCasesApi({ caseService, caseConfigureService, router }:
|
|||
client,
|
||||
};
|
||||
|
||||
const argsOpenCases = {
|
||||
const statusArgs = caseStatuses.map((caseStatus) => ({
|
||||
client,
|
||||
options: {
|
||||
fields: [],
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
filter: getStatusFilter('open', myFilters),
|
||||
filter: getStatusFilter(caseStatus, myFilters),
|
||||
},
|
||||
};
|
||||
}));
|
||||
|
||||
const argsClosedCases = {
|
||||
client,
|
||||
options: {
|
||||
fields: [],
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
filter: getStatusFilter('closed', myFilters),
|
||||
},
|
||||
};
|
||||
const [cases, openCases, closesCases] = await Promise.all([
|
||||
const [cases, openCases, inProgressCases, closedCases] = await Promise.all([
|
||||
caseService.findCases(args),
|
||||
caseService.findCases(argsOpenCases),
|
||||
caseService.findCases(argsClosedCases),
|
||||
...statusArgs.map((arg) => caseService.findCases(arg)),
|
||||
]);
|
||||
|
||||
const totalCommentsFindByCases = await Promise.all(
|
||||
cases.saved_objects.map((c) =>
|
||||
caseService.getAllCaseComments({
|
||||
|
@ -133,7 +130,8 @@ export function initFindCasesApi({ caseService, caseConfigureService, router }:
|
|||
transformCases(
|
||||
cases,
|
||||
openCases.total ?? 0,
|
||||
closesCases.total ?? 0,
|
||||
inProgressCases.total ?? 0,
|
||||
closedCases.total ?? 0,
|
||||
totalCommentsByCases
|
||||
)
|
||||
),
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from '../__fixtures__';
|
||||
import { initPatchCasesApi } from './patch_cases';
|
||||
import { mockCaseConfigure, mockCaseNoConnectorId } from '../__fixtures__/mock_saved_objects';
|
||||
import { ConnectorTypes } from '../../../../common/api/connectors';
|
||||
import { ConnectorTypes, CaseStatuses } from '../../../../common/api';
|
||||
|
||||
describe('PATCH cases', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
|
@ -36,7 +36,7 @@ describe('PATCH cases', () => {
|
|||
cases: [
|
||||
{
|
||||
id: 'mock-id-1',
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
version: 'WzAsMV0=',
|
||||
},
|
||||
],
|
||||
|
@ -67,7 +67,7 @@ describe('PATCH cases', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
id: 'mock-id-1',
|
||||
external_service: null,
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
tags: ['defacement'],
|
||||
title: 'Super Bad Security Issue',
|
||||
totalComment: 0,
|
||||
|
@ -86,7 +86,7 @@ describe('PATCH cases', () => {
|
|||
cases: [
|
||||
{
|
||||
id: 'mock-id-4',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
version: 'WzUsMV0=',
|
||||
},
|
||||
],
|
||||
|
@ -118,7 +118,7 @@ describe('PATCH cases', () => {
|
|||
description: 'Oh no, a bad meanie going LOLBins all over the place!',
|
||||
id: 'mock-id-4',
|
||||
external_service: null,
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['LOLBins'],
|
||||
title: 'Another bad one',
|
||||
totalComment: 0,
|
||||
|
@ -129,6 +129,56 @@ describe('PATCH cases', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it(`Change case to in-progress`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: '/api/cases',
|
||||
method: 'patch',
|
||||
body: {
|
||||
cases: [
|
||||
{
|
||||
id: 'mock-id-1',
|
||||
status: CaseStatuses['in-progress'],
|
||||
version: 'WzAsMV0=',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const theContext = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseSavedObject: mockCases,
|
||||
})
|
||||
);
|
||||
|
||||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload).toEqual([
|
||||
{
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
comments: [],
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' },
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
id: 'mock-id-1',
|
||||
external_service: null,
|
||||
status: CaseStatuses['in-progress'],
|
||||
tags: ['defacement'],
|
||||
title: 'Super Bad Security Issue',
|
||||
totalComment: 0,
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
|
||||
version: 'WzE3LDFd',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`Patches a case without a connector.id`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: '/api/cases',
|
||||
|
@ -137,7 +187,7 @@ describe('PATCH cases', () => {
|
|||
cases: [
|
||||
{
|
||||
id: 'mock-no-connector_id',
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
version: 'WzAsMV0=',
|
||||
},
|
||||
],
|
||||
|
@ -163,7 +213,7 @@ describe('PATCH cases', () => {
|
|||
cases: [
|
||||
{
|
||||
id: 'mock-id-3',
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
version: 'WzUsMV0=',
|
||||
},
|
||||
],
|
||||
|
@ -225,7 +275,7 @@ describe('PATCH cases', () => {
|
|||
cases: [
|
||||
{
|
||||
id: 'mock-id-1',
|
||||
case: { status: 'closed' },
|
||||
case: { status: CaseStatuses.closed },
|
||||
version: 'badv=',
|
||||
},
|
||||
],
|
||||
|
@ -250,7 +300,7 @@ describe('PATCH cases', () => {
|
|||
cases: [
|
||||
{
|
||||
id: 'mock-id-1',
|
||||
case: { status: 'open' },
|
||||
case: { status: CaseStatuses.open },
|
||||
version: 'WzAsMV0=',
|
||||
},
|
||||
],
|
||||
|
@ -276,7 +326,7 @@ describe('PATCH cases', () => {
|
|||
cases: [
|
||||
{
|
||||
id: 'mock-id-does-not-exist',
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
version: 'WzAsMV0=',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { initPostCaseApi } from './post_case';
|
||||
import { CASES_URL } from '../../../../common/constants';
|
||||
import { mockCaseConfigure } from '../__fixtures__/mock_saved_objects';
|
||||
import { ConnectorTypes } from '../../../../common/api/connectors';
|
||||
import { ConnectorTypes, CaseStatuses } from '../../../../common/api';
|
||||
|
||||
describe('POST cases', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
|
@ -54,6 +54,7 @@ describe('POST cases', () => {
|
|||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.id).toEqual('mock-it');
|
||||
expect(response.payload.status).toEqual('open');
|
||||
expect(response.payload.created_by.username).toEqual('awesome');
|
||||
expect(response.payload.connector).toEqual({
|
||||
id: 'none',
|
||||
|
@ -104,7 +105,7 @@ describe('POST cases', () => {
|
|||
body: {
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
connector: null,
|
||||
},
|
||||
|
@ -191,7 +192,7 @@ describe('POST cases', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
id: 'mock-it',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
title: 'Super Bad Security Issue',
|
||||
totalComment: 0,
|
||||
|
|
|
@ -18,7 +18,12 @@ import {
|
|||
getCommentContextFromAttributes,
|
||||
} from '../utils';
|
||||
|
||||
import { CaseExternalServiceRequestRt, CaseResponseRt, throwErrors } from '../../../../common/api';
|
||||
import {
|
||||
CaseExternalServiceRequestRt,
|
||||
CaseResponseRt,
|
||||
throwErrors,
|
||||
CaseStatuses,
|
||||
} from '../../../../common/api';
|
||||
import { buildCaseUserActionItem } from '../../../services/user_actions/helpers';
|
||||
import { RouteDeps } from '../types';
|
||||
import { CASE_DETAILS_URL } from '../../../../common/constants';
|
||||
|
@ -77,7 +82,7 @@ export function initPushCaseUserActionApi({
|
|||
actionsClient.getAll(),
|
||||
]);
|
||||
|
||||
if (myCase.attributes.status === 'closed') {
|
||||
if (myCase.attributes.status === CaseStatuses.closed) {
|
||||
throw Boom.conflict(
|
||||
`This case ${myCase.attributes.title} is closed. You can not pushed if the case is closed.`
|
||||
);
|
||||
|
@ -117,7 +122,7 @@ export function initPushCaseUserActionApi({
|
|||
...(myCaseConfigure.total > 0 &&
|
||||
myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing'
|
||||
? {
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
closed_at: pushedDate,
|
||||
closed_by: { email, full_name, username },
|
||||
}
|
||||
|
@ -153,7 +158,7 @@ export function initPushCaseUserActionApi({
|
|||
actionBy: { username, full_name, email },
|
||||
caseId,
|
||||
fields: ['status'],
|
||||
newValue: 'closed',
|
||||
newValue: CaseStatuses.closed,
|
||||
oldValue: myCase.attributes.status,
|
||||
}),
|
||||
]
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { RouteDeps } from '../../types';
|
||||
import { wrapError } from '../../utils';
|
||||
|
||||
import { CasesStatusResponseRt } from '../../../../../common/api';
|
||||
import { CasesStatusResponseRt, caseStatuses } from '../../../../../common/api';
|
||||
import { CASE_SAVED_OBJECT } from '../../../../saved_object_types';
|
||||
import { CASE_STATUS_URL } from '../../../../../common/constants';
|
||||
|
||||
|
@ -20,34 +20,24 @@ export function initGetCasesStatusApi({ caseService, router }: RouteDeps) {
|
|||
async (context, request, response) => {
|
||||
try {
|
||||
const client = context.core.savedObjects.client;
|
||||
const argsOpenCases = {
|
||||
const args = caseStatuses.map((status) => ({
|
||||
client,
|
||||
options: {
|
||||
fields: [],
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
filter: `${CASE_SAVED_OBJECT}.attributes.status: open`,
|
||||
filter: `${CASE_SAVED_OBJECT}.attributes.status: ${status}`,
|
||||
},
|
||||
};
|
||||
}));
|
||||
|
||||
const argsClosedCases = {
|
||||
client,
|
||||
options: {
|
||||
fields: [],
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
filter: `${CASE_SAVED_OBJECT}.attributes.status: closed`,
|
||||
},
|
||||
};
|
||||
|
||||
const [openCases, closesCases] = await Promise.all([
|
||||
caseService.findCases(argsOpenCases),
|
||||
caseService.findCases(argsClosedCases),
|
||||
]);
|
||||
const [openCases, inProgressCases, closesCases] = await Promise.all(
|
||||
args.map((arg) => caseService.findCases(arg))
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
body: CasesStatusResponseRt.encode({
|
||||
count_open_cases: openCases.total,
|
||||
count_in_progress_cases: inProgressCases.total,
|
||||
count_closed_cases: closesCases.total,
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
mockCaseComments,
|
||||
mockCaseNoConnectorId,
|
||||
} from './__fixtures__/mock_saved_objects';
|
||||
import { ConnectorTypes, ESCaseConnector, CommentType } from '../../../common/api';
|
||||
import { ConnectorTypes, ESCaseConnector, CommentType, CaseStatuses } from '../../../common/api';
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('transformNewCase', () => {
|
||||
|
@ -57,7 +57,7 @@ describe('Utils', () => {
|
|||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: { email: 'elastic@elastic.co', full_name: 'Elastic', username: 'elastic' },
|
||||
external_service: null,
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
});
|
||||
|
@ -80,7 +80,7 @@ describe('Utils', () => {
|
|||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: { email: undefined, full_name: undefined, username: undefined },
|
||||
external_service: null,
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
});
|
||||
|
@ -106,7 +106,7 @@ describe('Utils', () => {
|
|||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: { email: null, full_name: null, username: null },
|
||||
external_service: null,
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
});
|
||||
|
@ -247,6 +247,7 @@ describe('Utils', () => {
|
|||
},
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
extraCaseData
|
||||
);
|
||||
expect(res).toEqual({
|
||||
|
@ -259,6 +260,7 @@ describe('Utils', () => {
|
|||
),
|
||||
count_open_cases: 2,
|
||||
count_closed_cases: 2,
|
||||
count_in_progress_cases: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -289,7 +291,7 @@ describe('Utils', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: {
|
||||
|
@ -328,7 +330,7 @@ describe('Utils', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: {
|
||||
|
@ -374,7 +376,7 @@ describe('Utils', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: {
|
||||
|
@ -484,7 +486,7 @@ describe('Utils', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: {
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
CommentType,
|
||||
excess,
|
||||
throwErrors,
|
||||
CaseStatuses,
|
||||
} from '../../../common/api';
|
||||
import { transformESConnectorToCaseConnector } from './cases/helpers';
|
||||
|
||||
|
@ -61,7 +62,7 @@ export const transformNewCase = ({
|
|||
created_at: createdDate,
|
||||
created_by: { email, full_name, username },
|
||||
external_service: null,
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
});
|
||||
|
@ -103,6 +104,7 @@ export function wrapError(error: any): CustomHttpResponseOptions<ResponseError>
|
|||
export const transformCases = (
|
||||
cases: SavedObjectsFindResponse<ESCaseAttributes>,
|
||||
countOpenCases: number,
|
||||
countInProgressCases: number,
|
||||
countClosedCases: number,
|
||||
totalCommentByCase: TotalCommentByCase[]
|
||||
): CasesFindResponse => ({
|
||||
|
@ -111,6 +113,7 @@ export const transformCases = (
|
|||
total: cases.total,
|
||||
cases: flattenCaseSavedObjects(cases.saved_objects, totalCommentByCase),
|
||||
count_open_cases: countOpenCases,
|
||||
count_in_progress_cases: countInProgressCases,
|
||||
count_closed_cases: countClosedCases,
|
||||
});
|
||||
|
||||
|
|
|
@ -8,10 +8,10 @@ import { case1 } from '../objects/case';
|
|||
|
||||
import {
|
||||
ALL_CASES_CLOSE_ACTION,
|
||||
ALL_CASES_CLOSED_CASES_COUNT,
|
||||
ALL_CASES_CLOSED_CASES_STATS,
|
||||
ALL_CASES_COMMENTS_COUNT,
|
||||
ALL_CASES_DELETE_ACTION,
|
||||
ALL_CASES_IN_PROGRESS_CASES_STATS,
|
||||
ALL_CASES_NAME,
|
||||
ALL_CASES_OPEN_CASES_COUNT,
|
||||
ALL_CASES_OPEN_CASES_STATS,
|
||||
|
@ -70,8 +70,8 @@ describe('Cases', () => {
|
|||
cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases');
|
||||
cy.get(ALL_CASES_OPEN_CASES_STATS).should('have.text', 'Open cases1');
|
||||
cy.get(ALL_CASES_CLOSED_CASES_STATS).should('have.text', 'Closed cases0');
|
||||
cy.get(ALL_CASES_OPEN_CASES_COUNT).should('have.text', 'Open cases (1)');
|
||||
cy.get(ALL_CASES_CLOSED_CASES_COUNT).should('have.text', 'Closed cases (0)');
|
||||
cy.get(ALL_CASES_IN_PROGRESS_CASES_STATS).should('have.text', 'In progress cases0');
|
||||
cy.get(ALL_CASES_OPEN_CASES_COUNT).should('have.text', 'Open (1)');
|
||||
cy.get(ALL_CASES_REPORTERS_COUNT).should('have.text', 'Reporter1');
|
||||
cy.get(ALL_CASES_TAGS_COUNT).should('have.text', 'Tags2');
|
||||
cy.get(ALL_CASES_NAME).should('have.text', case1.name);
|
||||
|
@ -89,7 +89,7 @@ describe('Cases', () => {
|
|||
|
||||
const expectedTags = case1.tags.join('');
|
||||
cy.get(CASE_DETAILS_PAGE_TITLE).should('have.text', case1.name);
|
||||
cy.get(CASE_DETAILS_STATUS).should('have.text', 'open');
|
||||
cy.get(CASE_DETAILS_STATUS).should('have.text', 'Open');
|
||||
cy.get(CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME).should('have.text', case1.reporter);
|
||||
cy.get(CASE_DETAILS_USER_ACTION_DESCRIPTION_EVENT).should('have.text', 'added description');
|
||||
cy.get(CASE_DETAILS_DESCRIPTION).should(
|
||||
|
|
|
@ -10,8 +10,6 @@ export const ALL_CASES_CASE = (id: string) => {
|
|||
|
||||
export const ALL_CASES_CLOSE_ACTION = '[data-test-subj="action-close"]';
|
||||
|
||||
export const ALL_CASES_CLOSED_CASES_COUNT = '[data-test-subj="closed-case-count"]';
|
||||
|
||||
export const ALL_CASES_CLOSED_CASES_STATS = '[data-test-subj="closedStatsHeader"]';
|
||||
|
||||
export const ALL_CASES_COMMENTS_COUNT = '[data-test-subj="case-table-column-commentCount"]';
|
||||
|
@ -22,9 +20,11 @@ export const ALL_CASES_CREATE_NEW_CASE_TABLE_BTN = '[data-test-subj="cases-table
|
|||
|
||||
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_NAME = '[data-test-subj="case-details-link"]';
|
||||
|
||||
export const ALL_CASES_OPEN_CASES_COUNT = '[data-test-subj="open-case-count"]';
|
||||
export const ALL_CASES_OPEN_CASES_COUNT = '[data-test-subj="case-status-filter"]';
|
||||
|
||||
export const ALL_CASES_OPEN_CASES_STATS = '[data-test-subj="openStatsHeader"]';
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ export const CASE_DETAILS_PAGE_TITLE = '[data-test-subj="header-page-title"]';
|
|||
export const CASE_DETAILS_PUSH_TO_EXTERNAL_SERVICE_BTN =
|
||||
'[data-test-subj="push-to-external-service"]';
|
||||
|
||||
export const CASE_DETAILS_STATUS = '[data-test-subj="case-view-status"]';
|
||||
export const CASE_DETAILS_STATUS = '[data-test-subj="case-view-status-dropdown"]';
|
||||
|
||||
export const CASE_DETAILS_TAGS = '[data-test-subj="case-tags"]';
|
||||
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types';
|
||||
import { Dispatch } from 'react';
|
||||
import { Case } from '../../containers/types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { Dispatch } from 'react';
|
||||
import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types';
|
||||
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { Case } from '../../containers/types';
|
||||
import { UpdateCase } from '../../containers/use_get_cases';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface GetActions {
|
||||
caseStatus: string;
|
||||
|
@ -29,7 +31,7 @@ export const getActions = ({
|
|||
type: 'icon',
|
||||
'data-test-subj': 'action-delete',
|
||||
},
|
||||
caseStatus === 'open'
|
||||
caseStatus === CaseStatuses.open
|
||||
? {
|
||||
description: i18n.CLOSE_CASE,
|
||||
icon: 'folderCheck',
|
||||
|
@ -37,7 +39,7 @@ export const getActions = ({
|
|||
onClick: (theCase: Case) =>
|
||||
dispatchUpdate({
|
||||
updateKey: 'status',
|
||||
updateValue: 'closed',
|
||||
updateValue: CaseStatuses.closed,
|
||||
caseId: theCase.id,
|
||||
version: theCase.version,
|
||||
}),
|
||||
|
@ -51,7 +53,7 @@ export const getActions = ({
|
|||
onClick: (theCase: Case) =>
|
||||
dispatchUpdate({
|
||||
updateKey: 'status',
|
||||
updateValue: 'open',
|
||||
updateValue: CaseStatuses.open,
|
||||
caseId: theCase.id,
|
||||
version: theCase.version,
|
||||
}),
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
EuiAvatar,
|
||||
|
@ -16,6 +17,8 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types';
|
||||
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { getEmptyTagValue } from '../../../common/components/empty_value';
|
||||
import { Case } from '../../containers/types';
|
||||
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
|
||||
|
@ -59,7 +62,7 @@ export const getCasesColumns = (
|
|||
) : (
|
||||
<span>{theCase.title}</span>
|
||||
);
|
||||
return theCase.status === 'open' ? (
|
||||
return theCase.status !== CaseStatuses.closed ? (
|
||||
caseDetailsLinkComponent
|
||||
) : (
|
||||
<>
|
||||
|
@ -127,7 +130,7 @@ export const getCasesColumns = (
|
|||
? renderStringField(`${totalComment}`, `case-table-column-commentCount`)
|
||||
: getEmptyTagValue(),
|
||||
},
|
||||
filterStatus === 'open'
|
||||
filterStatus === CaseStatuses.open
|
||||
? {
|
||||
field: 'createdAt',
|
||||
name: i18n.OPENED_ON,
|
||||
|
|
|
@ -14,6 +14,7 @@ import { TestProviders } from '../../../common/mock';
|
|||
import { useGetCasesMockState } from '../../containers/mock';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { getEmptyTagValue } from '../../../common/components/empty_value';
|
||||
import { useDeleteCases } from '../../containers/use_delete_cases';
|
||||
|
@ -159,7 +160,7 @@ describe('AllCases', () => {
|
|||
expect(column.find('span').text()).toEqual(emptyTag);
|
||||
};
|
||||
await waitFor(() => {
|
||||
getCasesColumns([], 'open', false).map(
|
||||
getCasesColumns([], CaseStatuses.open, false).map(
|
||||
(i, key) => i.name != null && checkIt(`${i.name}`, key)
|
||||
);
|
||||
});
|
||||
|
@ -175,7 +176,9 @@ describe('AllCases', () => {
|
|||
const checkIt = (columnName: string) => {
|
||||
expect(columnName).not.toEqual(i18n.ACTIONS);
|
||||
};
|
||||
getCasesColumns([], 'open', true).map((i, key) => i.name != null && checkIt(`${i.name}`));
|
||||
getCasesColumns([], CaseStatuses.open, true).map(
|
||||
(i, key) => i.name != null && checkIt(`${i.name}`)
|
||||
);
|
||||
expect(wrapper.find(`a[data-test-subj="case-details-link"]`).exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
@ -208,7 +211,7 @@ describe('AllCases', () => {
|
|||
expect(dispatchUpdateCaseProperty).toBeCalledWith({
|
||||
caseId: firstCase.id,
|
||||
updateKey: 'status',
|
||||
updateValue: 'closed',
|
||||
updateValue: CaseStatuses.closed,
|
||||
refetchCasesStatus: fetchCasesStatus,
|
||||
version: firstCase.version,
|
||||
});
|
||||
|
@ -217,7 +220,7 @@ describe('AllCases', () => {
|
|||
it('opens case when row action icon clicked', async () => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: 'closed' },
|
||||
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed },
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
|
@ -231,7 +234,7 @@ describe('AllCases', () => {
|
|||
expect(dispatchUpdateCaseProperty).toBeCalledWith({
|
||||
caseId: firstCase.id,
|
||||
updateKey: 'status',
|
||||
updateValue: 'open',
|
||||
updateValue: CaseStatuses.open,
|
||||
refetchCasesStatus: fetchCasesStatus,
|
||||
version: firstCase.version,
|
||||
});
|
||||
|
@ -288,7 +291,7 @@ describe('AllCases', () => {
|
|||
await waitFor(() => {
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().simulate('click');
|
||||
expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'closed');
|
||||
expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.closed);
|
||||
});
|
||||
});
|
||||
it('Bulk open status update', async () => {
|
||||
|
@ -297,7 +300,7 @@ describe('AllCases', () => {
|
|||
selectedCases: useGetCasesMockState.data.cases,
|
||||
filterOptions: {
|
||||
...defaultGetCases.filterOptions,
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -309,7 +312,7 @@ describe('AllCases', () => {
|
|||
await waitFor(() => {
|
||||
wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="cases-bulk-open-button"]').first().simulate('click');
|
||||
expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'open');
|
||||
expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.open);
|
||||
});
|
||||
});
|
||||
it('isDeleted is true, refetch', async () => {
|
||||
|
|
|
@ -19,6 +19,7 @@ import { isEmpty, memoize } from 'lodash/fp';
|
|||
import styled, { css } from 'styled-components';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { getCasesColumns } from './columns';
|
||||
import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../containers/types';
|
||||
import { useGetCases, UpdateCase } from '../../containers/use_get_cases';
|
||||
|
@ -37,7 +38,6 @@ import { getCreateCaseUrl, useFormatUrl } from '../../../common/components/link_
|
|||
import { getBulkItems } from '../bulk_actions';
|
||||
import { CaseHeaderPage } from '../case_header_page';
|
||||
import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
|
||||
import { OpenClosedStats } from '../open_closed_stats';
|
||||
import { getActions } from './actions';
|
||||
import { CasesTableFilters } from './table_filters';
|
||||
import { useUpdateCases } from '../../containers/use_bulk_update_case';
|
||||
|
@ -50,6 +50,7 @@ import { LinkButton } from '../../../common/components/links';
|
|||
import { SecurityPageName } from '../../../app/types';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { APP_ID } from '../../../../common/constants';
|
||||
import { Stats } from '../status';
|
||||
|
||||
const Div = styled.div`
|
||||
margin-top: ${({ theme }) => theme.eui.paddingSizes.m};
|
||||
|
@ -91,8 +92,9 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case);
|
||||
const { actionLicense } = useGetActionLicense();
|
||||
const {
|
||||
countClosedCases,
|
||||
countOpenCases,
|
||||
countInProgressCases,
|
||||
countClosedCases,
|
||||
isLoading: isCasesStatusLoading,
|
||||
fetchCasesStatus,
|
||||
} = useGetCasesStatus();
|
||||
|
@ -291,10 +293,15 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
|
||||
const onFilterChangedCallback = useCallback(
|
||||
(newFilterOptions: Partial<FilterOptions>) => {
|
||||
if (newFilterOptions.status && newFilterOptions.status === 'closed') {
|
||||
if (newFilterOptions.status && newFilterOptions.status === CaseStatuses.closed) {
|
||||
setQueryParams({ sortField: SortFieldCase.closedAt });
|
||||
} else if (newFilterOptions.status && newFilterOptions.status === 'open') {
|
||||
} else if (newFilterOptions.status && newFilterOptions.status === CaseStatuses.open) {
|
||||
setQueryParams({ sortField: SortFieldCase.createdAt });
|
||||
} else if (
|
||||
newFilterOptions.status &&
|
||||
newFilterOptions.status === CaseStatuses['in-progress']
|
||||
) {
|
||||
setQueryParams({ sortField: SortFieldCase.updatedAt });
|
||||
}
|
||||
setFilters(newFilterOptions);
|
||||
refreshCases(false);
|
||||
|
@ -375,18 +382,26 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
data-test-subj="all-cases-header"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<OpenClosedStats
|
||||
<Stats
|
||||
dataTestSubj="openStatsHeader"
|
||||
caseCount={countOpenCases}
|
||||
caseStatus={'open'}
|
||||
caseStatus={CaseStatuses.open}
|
||||
isLoading={isCasesStatusLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Stats
|
||||
dataTestSubj="inProgressStatsHeader"
|
||||
caseCount={countInProgressCases}
|
||||
caseStatus={CaseStatuses['in-progress']}
|
||||
isLoading={isCasesStatusLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<FlexItemDivider grow={false}>
|
||||
<OpenClosedStats
|
||||
<Stats
|
||||
dataTestSubj="closedStatsHeader"
|
||||
caseCount={countClosedCases}
|
||||
caseStatus={'closed'}
|
||||
caseStatus={CaseStatuses.closed}
|
||||
isLoading={isCasesStatusLoading}
|
||||
/>
|
||||
</FlexItemDivider>
|
||||
|
@ -422,6 +437,7 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
<CasesTableFilters
|
||||
countClosedCases={data.countClosedCases}
|
||||
countOpenCases={data.countOpenCases}
|
||||
countInProgressCases={data.countInProgressCases}
|
||||
onFilterChanged={onFilterChangedCallback}
|
||||
initial={{
|
||||
search: filterOptions.search,
|
||||
|
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiSuperSelect, EuiSuperSelectOption, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { Status, statuses } from '../status';
|
||||
|
||||
interface Props {
|
||||
stats: Record<CaseStatuses, number>;
|
||||
selectedStatus: CaseStatuses;
|
||||
onStatusChanged: (status: CaseStatuses) => void;
|
||||
}
|
||||
|
||||
const StatusFilterComponent: React.FC<Props> = ({ stats, selectedStatus, onStatusChanged }) => {
|
||||
const caseStatuses = Object.keys(statuses) as CaseStatuses[];
|
||||
const options: Array<EuiSuperSelectOption<CaseStatuses>> = caseStatuses.map((status) => ({
|
||||
value: status,
|
||||
inputDisplay: (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems={'center'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Status type={status} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{` (${stats[status]})`}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
'data-test-subj': `case-status-filter-${status}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
options={options}
|
||||
valueOfSelected={selectedStatus}
|
||||
onChange={onStatusChanged}
|
||||
data-test-subj="case-status-filter"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusFilter = memo(StatusFilterComponent);
|
|
@ -7,12 +7,13 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { CasesTableFilters } from './table_filters';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
import { useGetReporters } from '../../containers/use_get_reporters';
|
||||
import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases';
|
||||
|
||||
jest.mock('../../containers/use_get_reporters');
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
|
||||
|
@ -24,10 +25,12 @@ const setFilterRefetch = jest.fn();
|
|||
const props = {
|
||||
countClosedCases: 1234,
|
||||
countOpenCases: 99,
|
||||
countInProgressCases: 54,
|
||||
onFilterChanged,
|
||||
initial: DEFAULT_FILTER_OPTIONS,
|
||||
setFilterRefetch,
|
||||
};
|
||||
|
||||
describe('CasesTableFilters ', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
@ -40,19 +43,17 @@ describe('CasesTableFilters ', () => {
|
|||
fetchReporters,
|
||||
});
|
||||
});
|
||||
it('should render the initial case count', () => {
|
||||
|
||||
it('should render the case status filter dropdown', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<CasesTableFilters {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find(`[data-test-subj="open-case-count"]`).last().text()).toEqual(
|
||||
'Open cases (99)'
|
||||
);
|
||||
expect(wrapper.find(`[data-test-subj="closed-case-count"]`).last().text()).toEqual(
|
||||
'Closed cases (1234)'
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="case-status-filter"]`).first().exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call onFilterChange when selected tags change', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
@ -64,6 +65,7 @@ describe('CasesTableFilters ', () => {
|
|||
|
||||
expect(onFilterChanged).toBeCalledWith({ tags: ['coke'] });
|
||||
});
|
||||
|
||||
it('should call onFilterChange when selected reporters change', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
@ -79,6 +81,7 @@ describe('CasesTableFilters ', () => {
|
|||
|
||||
expect(onFilterChanged).toBeCalledWith({ reporters: [{ username: 'casetester' }] });
|
||||
});
|
||||
|
||||
it('should call onFilterChange when search changes', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
@ -92,16 +95,19 @@ describe('CasesTableFilters ', () => {
|
|||
.simulate('keyup', { key: 'Enter', target: { value: 'My search' } });
|
||||
expect(onFilterChanged).toBeCalledWith({ search: 'My search' });
|
||||
});
|
||||
it('should call onFilterChange when status toggled', () => {
|
||||
|
||||
it('should call onFilterChange when changing status', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<CasesTableFilters {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find(`[data-test-subj="closed-case-count"]`).last().simulate('click');
|
||||
|
||||
expect(onFilterChanged).toBeCalledWith({ status: 'closed' });
|
||||
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="case-status-filter-closed"]').simulate('click');
|
||||
expect(onFilterChanged).toBeCalledWith({ status: CaseStatuses.closed });
|
||||
});
|
||||
|
||||
it('should call on load setFilterRefetch', () => {
|
||||
mount(
|
||||
<TestProviders>
|
||||
|
@ -110,6 +116,7 @@ describe('CasesTableFilters ', () => {
|
|||
);
|
||||
expect(setFilterRefetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove tag from selected tags when tag no longer exists', () => {
|
||||
const ourProps = {
|
||||
...props,
|
||||
|
@ -125,6 +132,7 @@ describe('CasesTableFilters ', () => {
|
|||
);
|
||||
expect(onFilterChanged).toHaveBeenCalledWith({ tags: ['pepsi'] });
|
||||
});
|
||||
|
||||
it('should remove reporter from selected reporters when reporter no longer exists', () => {
|
||||
const ourProps = {
|
||||
...props,
|
||||
|
|
|
@ -4,24 +4,21 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiFilterGroup } from '@elastic/eui';
|
||||
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
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 { StatusFilter } from './status_filter';
|
||||
|
||||
import * as i18n from './translations';
|
||||
interface CasesTableFiltersProps {
|
||||
countClosedCases: number | null;
|
||||
countInProgressCases: number | null;
|
||||
countOpenCases: number | null;
|
||||
onFilterChanged: (filterOptions: Partial<FilterOptions>) => void;
|
||||
initial: FilterOptions;
|
||||
|
@ -35,11 +32,12 @@ interface CasesTableFiltersProps {
|
|||
* @param onFilterChanged change listener to be notified on filter changes
|
||||
*/
|
||||
|
||||
const defaultInitial = { search: '', reporters: [], status: 'open', tags: [] };
|
||||
const defaultInitial = { search: '', reporters: [], status: CaseStatuses.open, tags: [] };
|
||||
|
||||
const CasesTableFiltersComponent = ({
|
||||
countClosedCases,
|
||||
countOpenCases,
|
||||
countInProgressCases,
|
||||
onFilterChanged,
|
||||
initial = defaultInitial,
|
||||
setFilterRefetch,
|
||||
|
@ -49,18 +47,20 @@ const CasesTableFiltersComponent = ({
|
|||
);
|
||||
const [search, setSearch] = useState(initial.search);
|
||||
const [selectedTags, setSelectedTags] = useState(initial.tags);
|
||||
const [showOpenCases, setShowOpenCases] = useState(initial.status === 'open');
|
||||
const { tags, fetchTags } = useGetTags();
|
||||
const { reporters, respReporters, fetchReporters } = useGetReporters();
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
fetchTags();
|
||||
fetchReporters();
|
||||
}, [fetchReporters, fetchTags]);
|
||||
|
||||
useEffect(() => {
|
||||
if (setFilterRefetch != null) {
|
||||
setFilterRefetch(refetch);
|
||||
}
|
||||
}, [refetch, setFilterRefetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedReporters.length) {
|
||||
const newReporters = selectedReporters.filter((r) => reporters.includes(r));
|
||||
|
@ -68,6 +68,7 @@ const CasesTableFiltersComponent = ({
|
|||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [reporters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTags.length) {
|
||||
const newTags = selectedTags.filter((t) => tags.includes(t));
|
||||
|
@ -100,6 +101,7 @@ const CasesTableFiltersComponent = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[selectedTags]
|
||||
);
|
||||
|
||||
const handleOnSearch = useCallback(
|
||||
(newSearch) => {
|
||||
const trimSearch = newSearch.trim();
|
||||
|
@ -111,19 +113,26 @@ const CasesTableFiltersComponent = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[search]
|
||||
);
|
||||
const handleToggleFilter = useCallback(
|
||||
(showOpen) => {
|
||||
if (showOpen !== showOpenCases) {
|
||||
setShowOpenCases(showOpen);
|
||||
onFilterChanged({ status: showOpen ? 'open' : 'closed' });
|
||||
}
|
||||
|
||||
const onStatusChanged = useCallback(
|
||||
(status: CaseStatuses) => {
|
||||
onFilterChanged({ status });
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[showOpenCases]
|
||||
[onFilterChanged]
|
||||
);
|
||||
|
||||
const stats = useMemo(
|
||||
() => ({
|
||||
[CaseStatuses.open]: countOpenCases ?? 0,
|
||||
[CaseStatuses['in-progress']]: countInProgressCases ?? 0,
|
||||
[CaseStatuses.closed]: countClosedCases ?? 0,
|
||||
}),
|
||||
[countClosedCases, countInProgressCases, countOpenCases]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="m" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFlexItem grow={8}>
|
||||
<EuiFieldSearch
|
||||
aria-label={i18n.SEARCH_CASES}
|
||||
data-test-subj="search-cases"
|
||||
|
@ -133,26 +142,15 @@ const CasesTableFiltersComponent = ({
|
|||
onSearch={handleOnSearch}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={2}>
|
||||
<StatusFilter
|
||||
selectedStatus={initial.status}
|
||||
onStatusChanged={onStatusChanged}
|
||||
stats={stats}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFilterGroup>
|
||||
<EuiFilterButton
|
||||
data-test-subj="open-case-count"
|
||||
withNext
|
||||
hasActiveFilters={showOpenCases}
|
||||
onClick={handleToggleFilter.bind(null, true)}
|
||||
>
|
||||
{i18n.OPEN_CASES}
|
||||
{countOpenCases != null ? ` (${countOpenCases})` : ''}
|
||||
</EuiFilterButton>
|
||||
<EuiFilterButton
|
||||
data-test-subj="closed-case-count"
|
||||
hasActiveFilters={!showOpenCases}
|
||||
onClick={handleToggleFilter.bind(null, false)}
|
||||
>
|
||||
{i18n.CLOSED_CASES}
|
||||
{countClosedCases != null ? ` (${countClosedCases})` : ''}
|
||||
</EuiFilterButton>
|
||||
<FilterPopover
|
||||
buttonLabel={i18n.REPORTER}
|
||||
onSelectedOptionsChanged={handleSelectedReporters}
|
||||
|
|
|
@ -69,12 +69,6 @@ export const SEARCH_PLACEHOLDER = i18n.translate(
|
|||
defaultMessage: 'e.g. case name',
|
||||
}
|
||||
);
|
||||
export const OPEN_CASES = i18n.translate('xpack.securitySolution.case.caseTable.openCases', {
|
||||
defaultMessage: 'Open cases',
|
||||
});
|
||||
export const CLOSED_CASES = i18n.translate('xpack.securitySolution.case.caseTable.closedCases', {
|
||||
defaultMessage: 'Closed cases',
|
||||
});
|
||||
|
||||
export const CLOSED = i18n.translate('xpack.securitySolution.case.caseTable.closed', {
|
||||
defaultMessage: 'Closed',
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface GetBulkItems {
|
||||
|
@ -24,7 +26,7 @@ export const getBulkItems = ({
|
|||
updateCaseStatus,
|
||||
}: GetBulkItems) => {
|
||||
return [
|
||||
caseStatus === 'open' ? (
|
||||
caseStatus === CaseStatuses.open ? (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="cases-bulk-close-button"
|
||||
disabled={selectedCaseIds.length === 0}
|
||||
|
@ -32,7 +34,7 @@ export const getBulkItems = ({
|
|||
icon="folderCheck"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
updateCaseStatus('closed');
|
||||
updateCaseStatus(CaseStatuses.closed);
|
||||
}}
|
||||
>
|
||||
{i18n.BULK_ACTION_CLOSE_SELECTED}
|
||||
|
@ -45,7 +47,7 @@ export const getBulkItems = ({
|
|||
icon="folderExclamation"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
updateCaseStatus('open');
|
||||
updateCaseStatus(CaseStatuses.open);
|
||||
}}
|
||||
>
|
||||
{i18n.BULK_ACTION_OPEN_SELECTED}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { Case } from '../../containers/types';
|
||||
import { statuses } from '../status';
|
||||
|
||||
export const getStatusDate = (theCase: Case): string | null => {
|
||||
if (theCase.status === CaseStatuses.open) {
|
||||
return theCase.createdAt;
|
||||
} else if (theCase.status === CaseStatuses['in-progress']) {
|
||||
return theCase.updatedAt;
|
||||
} else if (theCase.status === CaseStatuses.closed) {
|
||||
return theCase.closedAt;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getStatusTitle = (status: CaseStatuses) => statuses[status].actionBar.title;
|
|
@ -4,11 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListDescription,
|
||||
|
@ -16,11 +14,14 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import * as i18n from '../case_view/translations';
|
||||
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
|
||||
import { CaseViewActions } from '../case_view/actions';
|
||||
import { Case } from '../../containers/types';
|
||||
import { CaseService } from '../../containers/use_get_case_user_actions';
|
||||
import { StatusContextMenu } from './status_context_menu';
|
||||
import { getStatusDate, getStatusTitle } from './helpers';
|
||||
|
||||
const MyDescriptionList = styled(EuiDescriptionList)`
|
||||
${({ theme }) => css`
|
||||
|
@ -31,58 +32,46 @@ const MyDescriptionList = styled(EuiDescriptionList)`
|
|||
`}
|
||||
`;
|
||||
|
||||
interface CaseStatusProps {
|
||||
'data-test-subj': string;
|
||||
badgeColor: string;
|
||||
buttonLabel: string;
|
||||
interface CaseActionBarProps {
|
||||
caseData: Case;
|
||||
currentExternalIncident: CaseService | null;
|
||||
disabled?: boolean;
|
||||
icon: string;
|
||||
isLoading: boolean;
|
||||
isSelected: boolean;
|
||||
onRefresh: () => void;
|
||||
status: string;
|
||||
title: string;
|
||||
toggleStatusCase: (status: boolean) => void;
|
||||
value: string | null;
|
||||
onStatusChanged: (status: CaseStatuses) => void;
|
||||
}
|
||||
const CaseStatusComp: React.FC<CaseStatusProps> = ({
|
||||
'data-test-subj': dataTestSubj,
|
||||
badgeColor,
|
||||
buttonLabel,
|
||||
const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
|
||||
caseData,
|
||||
currentExternalIncident,
|
||||
disabled = false,
|
||||
icon,
|
||||
isLoading,
|
||||
isSelected,
|
||||
onRefresh,
|
||||
status,
|
||||
title,
|
||||
toggleStatusCase,
|
||||
value,
|
||||
onStatusChanged,
|
||||
}) => {
|
||||
const handleToggleStatusCase = useCallback(() => {
|
||||
toggleStatusCase(!isSelected);
|
||||
}, [toggleStatusCase, isSelected]);
|
||||
const date = useMemo(() => getStatusDate(caseData), [caseData]);
|
||||
const title = useMemo(() => getStatusTitle(caseData.status), [caseData.status]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="l" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyDescriptionList compressed>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem grow={false} data-test-subj="case-view-status">
|
||||
<EuiDescriptionListTitle>{i18n.STATUS}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<EuiBadge color={badgeColor} data-test-subj="case-view-status">
|
||||
{status}
|
||||
</EuiBadge>
|
||||
<StatusContextMenu
|
||||
currentStatus={caseData.status}
|
||||
onStatusChanged={onStatusChanged}
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>{title}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<FormattedRelativePreferenceDate data-test-subj={dataTestSubj} value={value} />
|
||||
<FormattedRelativePreferenceDate
|
||||
data-test-subj={'case-action-bar-status-date'}
|
||||
value={date}
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -95,18 +84,6 @@ const CaseStatusComp: React.FC<CaseStatusProps> = ({
|
|||
{i18n.CASE_REFRESH}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
data-test-subj="toggle-case-status"
|
||||
isDisabled={disabled}
|
||||
iconType={icon}
|
||||
isLoading={isLoading}
|
||||
fill={isSelected}
|
||||
onClick={handleToggleStatusCase}
|
||||
>
|
||||
{buttonLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<CaseViewActions
|
||||
caseData={caseData}
|
||||
|
@ -120,4 +97,4 @@ const CaseStatusComp: React.FC<CaseStatusProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const CaseStatus = React.memo(CaseStatusComp);
|
||||
export const CaseActionBar = React.memo(CaseActionBarComponent);
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
currentStatus: CaseStatuses;
|
||||
onStatusChanged: (status: CaseStatuses) => void;
|
||||
}
|
||||
|
||||
const StatusContextMenuComponent: React.FC<Props> = ({ currentStatus, onStatusChanged }) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
const openPopover = useCallback(() => setIsPopoverOpen(true), []);
|
||||
const popOverButton = useMemo(
|
||||
() => <Status type={currentStatus} withArrow onClick={openPopover} />,
|
||||
[currentStatus, openPopover]
|
||||
);
|
||||
|
||||
const onContextMenuItemClick = useMemo(
|
||||
() =>
|
||||
memoize<(status: CaseStatuses) => () => void>((status) => () => {
|
||||
closePopover();
|
||||
onStatusChanged(status);
|
||||
}),
|
||||
[closePopover, onStatusChanged]
|
||||
);
|
||||
|
||||
const caseStatuses = Object.keys(statuses) as CaseStatuses[];
|
||||
const panelItems = caseStatuses.map((status: CaseStatuses) => (
|
||||
<EuiContextMenuItem
|
||||
key={status}
|
||||
icon={status === currentStatus ? 'check' : 'empty'}
|
||||
onClick={onContextMenuItemClick(status)}
|
||||
data-test-subj={`case-view-status-dropdown-${status}`}
|
||||
>
|
||||
<Status type={status} />
|
||||
</EuiContextMenuItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
id="caseStatusPopover"
|
||||
button={popOverButton}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="downLeft"
|
||||
data-test-subj="case-view-status-dropdown"
|
||||
>
|
||||
<EuiContextMenuPanel items={panelItems} />
|
||||
</EuiPopover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusContextMenu = memo(StatusContextMenuComponent);
|
|
@ -114,8 +114,8 @@ describe('CaseView ', () => {
|
|||
data.title
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="case-view-status"]`).first().text()).toEqual(
|
||||
data.status
|
||||
expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).first().text()).toEqual(
|
||||
'Open'
|
||||
);
|
||||
|
||||
expect(
|
||||
|
@ -136,11 +136,9 @@ describe('CaseView ', () => {
|
|||
data.createdBy.username
|
||||
);
|
||||
|
||||
expect(wrapper.contains(`[data-test-subj="case-view-closedAt"]`)).toBe(false);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="case-view-createdAt"]`).first().prop('value')).toEqual(
|
||||
data.createdAt
|
||||
);
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="case-action-bar-status-date"]`).first().prop('value')
|
||||
).toEqual(data.createdAt);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
|
@ -156,6 +154,7 @@ describe('CaseView ', () => {
|
|||
...defaultUpdateCaseState,
|
||||
caseData: basicCaseClosed,
|
||||
}));
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
|
@ -163,18 +162,18 @@ describe('CaseView ', () => {
|
|||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.contains(`[data-test-subj="case-view-createdAt"]`)).toBe(false);
|
||||
expect(wrapper.find(`[data-test-subj="case-view-closedAt"]`).first().prop('value')).toEqual(
|
||||
basicCaseClosed.closedAt
|
||||
);
|
||||
expect(wrapper.find(`[data-test-subj="case-view-status"]`).first().text()).toEqual(
|
||||
basicCaseClosed.status
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="case-action-bar-status-date"]`).first().prop('value')
|
||||
).toEqual(basicCaseClosed.closedAt);
|
||||
expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).first().text()).toEqual(
|
||||
'Closed'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch update state when button is toggled', async () => {
|
||||
it('should dispatch update state when status is changed', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
|
@ -182,8 +181,14 @@ describe('CaseView ', () => {
|
|||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.find('[data-test-subj="toggle-case-status"]').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="case-view-status-dropdown"] button').first().simulate('click');
|
||||
wrapper.update();
|
||||
wrapper
|
||||
.find('button[data-test-subj="case-view-status-dropdown-closed"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(updateCaseProperty).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -211,26 +216,6 @@ describe('CaseView ', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should display Toggle Status isLoading', async () => {
|
||||
useUpdateCaseMock.mockImplementation(() => ({
|
||||
...defaultUpdateCaseState,
|
||||
isLoading: true,
|
||||
updateKey: 'status',
|
||||
}));
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<CaseComponent {...caseProps} />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="toggle-case-status"]').first().prop('isLoading')
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display description isLoading', async () => {
|
||||
useUpdateCaseMock.mockImplementation(() => ({
|
||||
...defaultUpdateCaseState,
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingContent,
|
||||
|
@ -16,7 +15,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import styled from 'styled-components';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { Case, CaseConnector } from '../../containers/types';
|
||||
import { getCaseDetailsUrl, getCaseUrl, useFormatUrl } from '../../../common/components/link_to';
|
||||
import { gutterTimeline } from '../../../common/lib/helpers';
|
||||
|
@ -29,7 +28,7 @@ import { UserList } from '../user_list';
|
|||
import { useUpdateCase } from '../../containers/use_update_case';
|
||||
import { getTypedPayload } from '../../containers/utils';
|
||||
import { WhitePageWrapper, HeaderWrapper } from '../wrappers';
|
||||
import { CaseStatus } from '../case_status';
|
||||
import { CaseActionBar } from '../case_action_bar';
|
||||
import { SpyRoute } from '../../../common/utils/route/spy_routes';
|
||||
import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions';
|
||||
import { usePushToService } from '../use_push_to_service';
|
||||
|
@ -41,6 +40,9 @@ import {
|
|||
normalizeActionConnector,
|
||||
getNoneConnector,
|
||||
} from '../configure_cases/utils';
|
||||
import { StatusActionButton } from '../status/button';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
caseId: string;
|
||||
|
@ -159,7 +161,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
});
|
||||
break;
|
||||
case 'status':
|
||||
const statusUpdate = getTypedPayload<string>(value);
|
||||
const statusUpdate = getTypedPayload<CaseStatuses>(value);
|
||||
if (caseData.status !== value) {
|
||||
updateCaseProperty({
|
||||
fetchCaseUserActions,
|
||||
|
@ -241,11 +243,11 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
[onUpdateField]
|
||||
);
|
||||
|
||||
const toggleStatusCase = useCallback(
|
||||
(nextStatus) =>
|
||||
const changeStatus = useCallback(
|
||||
(status: CaseStatuses) =>
|
||||
onUpdateField({
|
||||
key: 'status',
|
||||
value: nextStatus ? 'closed' : 'open',
|
||||
value: status,
|
||||
}),
|
||||
[onUpdateField]
|
||||
);
|
||||
|
@ -257,32 +259,6 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
|
||||
const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]);
|
||||
|
||||
const caseStatusData = useMemo(
|
||||
() =>
|
||||
caseData.status === 'open'
|
||||
? {
|
||||
'data-test-subj': 'case-view-createdAt',
|
||||
value: caseData.createdAt,
|
||||
title: i18n.CASE_OPENED,
|
||||
buttonLabel: i18n.CLOSE_CASE,
|
||||
status: caseData.status,
|
||||
icon: 'folderCheck',
|
||||
badgeColor: 'secondary',
|
||||
isSelected: false,
|
||||
}
|
||||
: {
|
||||
'data-test-subj': 'case-view-closedAt',
|
||||
value: caseData.closedAt ?? '',
|
||||
title: i18n.CASE_CLOSED,
|
||||
buttonLabel: i18n.REOPEN_CASE,
|
||||
status: caseData.status,
|
||||
icon: 'folderExclamation',
|
||||
badgeColor: 'danger',
|
||||
isSelected: true,
|
||||
},
|
||||
[caseData.closedAt, caseData.createdAt, caseData.status]
|
||||
);
|
||||
|
||||
const emailContent = useMemo(
|
||||
() => ({
|
||||
subject: i18n.EMAIL_SUBJECT(caseData.title),
|
||||
|
@ -307,11 +283,6 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
[allCasesLink]
|
||||
);
|
||||
|
||||
const isSelected = useMemo(() => caseStatusData.isSelected, [caseStatusData]);
|
||||
const handleToggleStatusCase = useCallback(() => {
|
||||
toggleStatusCase(!isSelected);
|
||||
}, [toggleStatusCase, isSelected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderWrapper>
|
||||
|
@ -329,14 +300,13 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
}
|
||||
title={caseData.title}
|
||||
>
|
||||
<CaseStatus
|
||||
<CaseActionBar
|
||||
currentExternalIncident={currentExternalIncident}
|
||||
caseData={caseData}
|
||||
disabled={!userCanCrud}
|
||||
isLoading={isLoading && updateKey === 'status'}
|
||||
onRefresh={handleRefresh}
|
||||
toggleStatusCase={handleToggleStatusCase}
|
||||
{...caseStatusData}
|
||||
onStatusChanged={changeStatus}
|
||||
/>
|
||||
</HeaderPage>
|
||||
</HeaderWrapper>
|
||||
|
@ -363,16 +333,12 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
<MyEuiHorizontalRule margin="s" />
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj={caseStatusData['data-test-subj']}
|
||||
iconType={caseStatusData.icon}
|
||||
isDisabled={!userCanCrud}
|
||||
<StatusActionButton
|
||||
status={caseData.status}
|
||||
onStatusChanged={changeStatus}
|
||||
disabled={!userCanCrud}
|
||||
isLoading={isLoading && updateKey === 'status'}
|
||||
fill={caseStatusData.isSelected}
|
||||
onClick={handleToggleStatusCase}
|
||||
>
|
||||
{caseStatusData.buttonLabel}
|
||||
</EuiButton>
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{hasDataToPush && (
|
||||
<EuiFlexItem data-test-subj="has-data-to-push-button" grow={false}>
|
||||
|
|
|
@ -128,14 +128,6 @@ export const COMMENT = i18n.translate('xpack.securitySolution.case.caseView.comm
|
|||
defaultMessage: 'comment',
|
||||
});
|
||||
|
||||
export const CASE_OPENED = i18n.translate('xpack.securitySolution.case.caseView.caseOpened', {
|
||||
defaultMessage: 'Case opened',
|
||||
});
|
||||
|
||||
export const CASE_CLOSED = i18n.translate('xpack.securitySolution.case.caseView.caseClosed', {
|
||||
defaultMessage: 'Case closed',
|
||||
});
|
||||
|
||||
export const CASE_REFRESH = i18n.translate('xpack.securitySolution.case.caseView.caseRefresh', {
|
||||
defaultMessage: 'Refresh case',
|
||||
});
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiDescriptionList, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import * as i18n from '../all_cases/translations';
|
||||
|
||||
export interface Props {
|
||||
caseCount: number | null;
|
||||
caseStatus: 'open' | 'closed';
|
||||
isLoading: boolean;
|
||||
dataTestSubj?: string;
|
||||
}
|
||||
|
||||
export const OpenClosedStats = React.memo<Props>(
|
||||
({ caseCount, caseStatus, isLoading, dataTestSubj }) => {
|
||||
const openClosedStats = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: caseStatus === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES,
|
||||
description: isLoading ? <EuiLoadingSpinner /> : caseCount ?? 'N/A',
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[caseCount, caseStatus, isLoading, dataTestSubj]
|
||||
);
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
data-test-subj={dataTestSubj}
|
||||
textStyle="reverse"
|
||||
listItems={openClosedStats}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
OpenClosedStats.displayName = 'OpenClosedStats';
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
|
||||
import { CaseStatuses, caseStatuses } from '../../../../../case/common/api';
|
||||
import { statuses } from './config';
|
||||
|
||||
interface Props {
|
||||
status: CaseStatuses;
|
||||
disabled: boolean;
|
||||
isLoading: boolean;
|
||||
onStatusChanged: (status: CaseStatuses) => void;
|
||||
}
|
||||
|
||||
// Rotate over the statuses. open -> in-progress -> closes -> open...
|
||||
const getNextItem = (item: number) => (item + 1) % caseStatuses.length;
|
||||
|
||||
const StatusActionButtonComponent: React.FC<Props> = ({
|
||||
status,
|
||||
onStatusChanged,
|
||||
disabled,
|
||||
isLoading,
|
||||
}) => {
|
||||
const indexOfCurrentStatus = useMemo(
|
||||
() => caseStatuses.findIndex((caseStatus) => caseStatus === status),
|
||||
[status]
|
||||
);
|
||||
const nextStatusIndex = useMemo(() => getNextItem(indexOfCurrentStatus), [indexOfCurrentStatus]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
onStatusChanged(caseStatuses[nextStatusIndex]);
|
||||
}, [nextStatusIndex, onStatusChanged]);
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
data-test-subj={'case-view-status-action-button'}
|
||||
iconType={statuses[caseStatuses[nextStatusIndex]].button.icon}
|
||||
isDisabled={disabled}
|
||||
isLoading={isLoading}
|
||||
onClick={onClick}
|
||||
>
|
||||
{statuses[caseStatuses[nextStatusIndex]].button.label}
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
||||
export const StatusActionButton = memo(StatusActionButtonComponent);
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import * as i18n from './translations';
|
||||
|
||||
type Statuses = Record<
|
||||
CaseStatuses,
|
||||
{
|
||||
color: string;
|
||||
label: string;
|
||||
actionBar: {
|
||||
title: string;
|
||||
};
|
||||
button: {
|
||||
label: string;
|
||||
icon: string;
|
||||
};
|
||||
stats: {
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
export const statuses: Statuses = {
|
||||
[CaseStatuses.open]: {
|
||||
color: 'primary',
|
||||
label: i18n.OPEN,
|
||||
actionBar: {
|
||||
title: i18n.CASE_OPENED,
|
||||
},
|
||||
button: {
|
||||
label: i18n.REOPEN_CASE,
|
||||
icon: 'folderCheck',
|
||||
},
|
||||
stats: {
|
||||
title: i18n.OPEN_CASES,
|
||||
},
|
||||
},
|
||||
[CaseStatuses['in-progress']]: {
|
||||
color: 'warning',
|
||||
label: i18n.IN_PROGRESS,
|
||||
actionBar: {
|
||||
title: i18n.CASE_IN_PROGRESS,
|
||||
},
|
||||
button: {
|
||||
label: i18n.MARK_CASE_IN_PROGRESS,
|
||||
icon: 'folderExclamation',
|
||||
},
|
||||
stats: {
|
||||
title: i18n.IN_PROGRESS_CASES,
|
||||
},
|
||||
},
|
||||
[CaseStatuses.closed]: {
|
||||
color: 'default',
|
||||
label: i18n.CLOSED,
|
||||
actionBar: {
|
||||
title: i18n.CASE_CLOSED,
|
||||
},
|
||||
button: {
|
||||
label: i18n.CLOSE_CASE,
|
||||
icon: 'folderCheck',
|
||||
},
|
||||
stats: {
|
||||
title: i18n.CLOSED_CASES,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './status';
|
||||
export * from './config';
|
||||
export * from './stats';
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { EuiDescriptionList, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { statuses } from './config';
|
||||
|
||||
export interface Props {
|
||||
caseCount: number | null;
|
||||
caseStatus: CaseStatuses;
|
||||
isLoading: boolean;
|
||||
dataTestSubj?: string;
|
||||
}
|
||||
|
||||
const StatsComponent: React.FC<Props> = ({ caseCount, caseStatus, isLoading, dataTestSubj }) => {
|
||||
const statusStats = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: statuses[caseStatus].stats.title,
|
||||
description: isLoading ? <EuiLoadingSpinner /> : caseCount ?? 'N/A',
|
||||
},
|
||||
],
|
||||
[caseCount, caseStatus, isLoading]
|
||||
);
|
||||
return (
|
||||
<EuiDescriptionList data-test-subj={dataTestSubj} textStyle="reverse" listItems={statusStats} />
|
||||
);
|
||||
};
|
||||
|
||||
StatsComponent.displayName = 'StatsComponent';
|
||||
export const Stats = memo(StatsComponent);
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
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 * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
type: CaseStatuses;
|
||||
withArrow?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const StatusComponent: React.FC<Props> = ({ type, withArrow = false, onClick = noop }) => {
|
||||
const props = useMemo(
|
||||
() => ({
|
||||
color: statuses[type].color,
|
||||
...(withArrow ? { iconType: 'arrowDown', iconSide: 'right' as const } : {}),
|
||||
}),
|
||||
[withArrow, type]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiBadge
|
||||
{...props}
|
||||
iconOnClick={onClick}
|
||||
iconOnClickAriaLabel={i18n.STATUS_ICON_ARIA}
|
||||
data-test-subj={`status-badge-${type}`}
|
||||
>
|
||||
{statuses[type].label}
|
||||
</EuiBadge>
|
||||
);
|
||||
};
|
||||
|
||||
export const Status = memo(StatusComponent);
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
export * from '../../translations';
|
||||
|
||||
export const OPEN = i18n.translate('xpack.securitySolution.case.status.open', {
|
||||
defaultMessage: 'Open',
|
||||
});
|
||||
|
||||
export const IN_PROGRESS = i18n.translate('xpack.securitySolution.case.status.inProgress', {
|
||||
defaultMessage: 'In progress',
|
||||
});
|
||||
|
||||
export const CLOSED = i18n.translate('xpack.securitySolution.case.status.closed', {
|
||||
defaultMessage: 'Closed',
|
||||
});
|
||||
|
||||
export const STATUS_ICON_ARIA = i18n.translate('xpack.securitySolution.case.status.iconAria', {
|
||||
defaultMessage: 'Change status',
|
||||
});
|
||||
|
||||
export const CASE_OPENED = i18n.translate('xpack.securitySolution.case.caseView.caseOpened', {
|
||||
defaultMessage: 'Case opened',
|
||||
});
|
||||
|
||||
export const CASE_IN_PROGRESS = i18n.translate(
|
||||
'xpack.securitySolution.case.caseView.caseInProgress',
|
||||
{
|
||||
defaultMessage: 'Case in progress',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CLOSED = i18n.translate('xpack.securitySolution.case.caseView.caseClosed', {
|
||||
defaultMessage: 'Case closed',
|
||||
});
|
|
@ -11,6 +11,7 @@ import '../../../common/mock/match_media';
|
|||
import { usePushToService, ReturnUsePushToService, UsePushToService } from '.';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { usePostPushToService } from '../../containers/use_post_push_to_service';
|
||||
import { basicPush, actionLicenses } from '../../containers/mock';
|
||||
import { useGetActionLicense } from '../../containers/use_get_action_license';
|
||||
|
@ -61,7 +62,7 @@ describe('usePushToService', () => {
|
|||
},
|
||||
caseId,
|
||||
caseServices,
|
||||
caseStatus: 'open',
|
||||
caseStatus: CaseStatuses.open,
|
||||
connectors: connectorsMock,
|
||||
updateCase,
|
||||
userCanCrud: true,
|
||||
|
@ -252,7 +253,7 @@ describe('usePushToService', () => {
|
|||
() =>
|
||||
usePushToService({
|
||||
...defaultArgs,
|
||||
caseStatus: 'closed',
|
||||
caseStatus: CaseStatuses.closed,
|
||||
}),
|
||||
{
|
||||
wrapper: ({ children }) => <TestProviders> {children}</TestProviders>,
|
||||
|
|
|
@ -16,7 +16,7 @@ import { getConfigureCasesUrl, useFormatUrl } from '../../../common/components/l
|
|||
import { CaseCallOut } from '../callout';
|
||||
import { getLicenseError, getKibanaConfigError } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
import { CaseConnector, ActionConnector } from '../../../../../case/common/api';
|
||||
import { CaseConnector, ActionConnector, CaseStatuses } from '../../../../../case/common/api';
|
||||
import { CaseServices } from '../../containers/use_get_case_user_actions';
|
||||
import { LinkAnchor } from '../../../common/components/links';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
|
@ -133,7 +133,7 @@ export const usePushToService = ({
|
|||
},
|
||||
];
|
||||
}
|
||||
if (caseStatus === 'closed') {
|
||||
if (caseStatus === CaseStatuses.closed) {
|
||||
errors = [
|
||||
...errors,
|
||||
{
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { CaseStatuses } from '../../../../../case/common/api';
|
||||
import { basicPush, getUserAction } from '../../containers/mock';
|
||||
import { getLabelTitle, getPushedServiceLabelTitle, getConnectorLabelTitle } from './helpers';
|
||||
import * as i18n from '../case_view/translations';
|
||||
import { mount } from 'enzyme';
|
||||
import { connectorsMock } from '../../containers/configure/mock';
|
||||
import * as i18n from './translations';
|
||||
|
||||
describe('User action tree helpers', () => {
|
||||
const connectors = connectorsMock;
|
||||
|
@ -54,24 +56,24 @@ describe('User action tree helpers', () => {
|
|||
expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`);
|
||||
});
|
||||
|
||||
it('label title generated for update status to open', () => {
|
||||
const action = { ...getUserAction(['status'], 'update'), newValue: 'open' };
|
||||
it.skip('label title generated for update status to open', () => {
|
||||
const action = { ...getUserAction(['status'], 'update'), newValue: CaseStatuses.open };
|
||||
const result: string | JSX.Element = getLabelTitle({
|
||||
action,
|
||||
field: 'status',
|
||||
});
|
||||
|
||||
expect(result).toEqual(`${i18n.REOPENED_CASE.toLowerCase()} ${i18n.CASE}`);
|
||||
expect(result).toEqual(`${i18n.REOPEN_CASE.toLowerCase()} ${i18n.CASE}`);
|
||||
});
|
||||
|
||||
it('label title generated for update status to closed', () => {
|
||||
const action = { ...getUserAction(['status'], 'update'), newValue: 'closed' };
|
||||
it.skip('label title generated for update status to closed', () => {
|
||||
const action = { ...getUserAction(['status'], 'update'), newValue: CaseStatuses.closed };
|
||||
const result: string | JSX.Element = getLabelTitle({
|
||||
action,
|
||||
field: 'status',
|
||||
});
|
||||
|
||||
expect(result).toEqual(`${i18n.CLOSED_CASE.toLowerCase()} ${i18n.CASE}`);
|
||||
expect(result).toEqual(`${i18n.CLOSE_CASE.toLowerCase()} ${i18n.CASE}`);
|
||||
});
|
||||
|
||||
it('label title generated for update comment', () => {
|
||||
|
|
|
@ -7,22 +7,38 @@
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiCommentProps } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { CaseFullExternalService, ActionConnector } from '../../../../../case/common/api';
|
||||
import {
|
||||
CaseFullExternalService,
|
||||
ActionConnector,
|
||||
CaseStatuses,
|
||||
} from '../../../../../case/common/api';
|
||||
import { CaseUserActions } from '../../containers/types';
|
||||
import { CaseServices } from '../../containers/use_get_case_user_actions';
|
||||
import { parseString } from '../../containers/utils';
|
||||
import { Tags } from '../tag_list/tags';
|
||||
import * as i18n from '../case_view/translations';
|
||||
import { UserActionUsernameWithAvatar } from './user_action_username_with_avatar';
|
||||
import { UserActionTimestamp } from './user_action_timestamp';
|
||||
import { UserActionCopyLink } from './user_action_copy_link';
|
||||
import { UserActionMoveToReference } from './user_action_move_to_reference';
|
||||
import { Status, statuses } from '../status';
|
||||
import * as i18n from '../case_view/translations';
|
||||
|
||||
interface LabelTitle {
|
||||
action: CaseUserActions;
|
||||
field: string;
|
||||
}
|
||||
|
||||
const getStatusTitle = (status: CaseStatuses) => {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems={'center'}>
|
||||
<EuiFlexItem grow={false}>{i18n.MARKED_CASE_AS}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Status type={status} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const getLabelTitle = ({ action, field }: LabelTitle) => {
|
||||
if (field === 'tags') {
|
||||
return getTagsLabelTitle(action);
|
||||
|
@ -33,9 +49,12 @@ export const getLabelTitle = ({ action, field }: LabelTitle) => {
|
|||
} else if (field === 'description' && action.action === 'update') {
|
||||
return `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`;
|
||||
} else if (field === 'status' && action.action === 'update') {
|
||||
return `${
|
||||
action.newValue === 'open' ? i18n.REOPENED_CASE.toLowerCase() : i18n.CLOSED_CASE.toLowerCase()
|
||||
} ${i18n.CASE}`;
|
||||
if (!Object.prototype.hasOwnProperty.call(statuses, action.newValue ?? '')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// The above check ensures that the newValue is of type CaseStatuses.
|
||||
return getStatusTitle(action.newValue as CaseStatuses);
|
||||
} else if (field === 'comment' && action.action === 'update') {
|
||||
return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`;
|
||||
}
|
||||
|
@ -120,6 +139,16 @@ export const getPushInfo = (
|
|||
parsedConnectorName: 'none',
|
||||
};
|
||||
|
||||
const getUpdateActionIcon = (actionField: string): string => {
|
||||
if (actionField === 'tags') {
|
||||
return 'tag';
|
||||
} else if (actionField === 'status') {
|
||||
return 'folderClosed';
|
||||
}
|
||||
|
||||
return 'dot';
|
||||
};
|
||||
|
||||
export const getUpdateAction = ({
|
||||
action,
|
||||
label,
|
||||
|
@ -139,7 +168,7 @@ export const getUpdateAction = ({
|
|||
event: label,
|
||||
'data-test-subj': `${action.actionField[0]}-${action.action}-action-${action.actionId}`,
|
||||
timestamp: <UserActionTimestamp createdAt={action.actionAt} />,
|
||||
timelineIcon: action.action === 'add' || action.action === 'delete' ? 'tag' : 'dot',
|
||||
timelineIcon: getUpdateActionIcon(action.actionField[0]),
|
||||
actions: (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -380,10 +380,10 @@ export const UserActionTree = React.memo(
|
|||
];
|
||||
}
|
||||
|
||||
// title, description, comments, tags
|
||||
// title, description, comments, tags, status
|
||||
if (
|
||||
action.actionField.length === 1 &&
|
||||
['title', 'description', 'comment', 'tags'].includes(action.actionField[0])
|
||||
['title', 'description', 'comment', 'tags', 'status'].includes(action.actionField[0])
|
||||
) {
|
||||
const myField = action.actionField[0];
|
||||
const label: string | JSX.Element = getLabelTitle({
|
||||
|
|
|
@ -35,6 +35,7 @@ import {
|
|||
ServiceConnectorCaseParams,
|
||||
ServiceConnectorCaseResponse,
|
||||
User,
|
||||
CaseStatuses,
|
||||
} from '../../../../../case/common/api';
|
||||
|
||||
export const getCase = async (
|
||||
|
@ -62,7 +63,7 @@ export const getCases = async ({
|
|||
filterOptions = {
|
||||
search: '',
|
||||
reporters: [],
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: [],
|
||||
},
|
||||
queryParams = {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { KibanaServices } from '../../common/lib/kibana';
|
||||
|
||||
import { ConnectorTypes, CommentType, CaseStatuses } from '../../../../case/common/api';
|
||||
import { CASES_URL } from '../../../../case/common/constants';
|
||||
|
||||
import {
|
||||
|
@ -51,7 +52,6 @@ import {
|
|||
|
||||
import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases';
|
||||
import * as i18n from './translations';
|
||||
import { ConnectorTypes, CommentType } from '../../../../case/common/api';
|
||||
|
||||
const abortCtrl = new AbortController();
|
||||
const mockKibanaServices = KibanaServices.get as jest.Mock;
|
||||
|
@ -138,7 +138,7 @@ describe('Case Configuration API', () => {
|
|||
...DEFAULT_QUERY_PARAMS,
|
||||
reporters: [],
|
||||
tags: [],
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
@ -149,7 +149,7 @@ describe('Case Configuration API', () => {
|
|||
...DEFAULT_FILTER_OPTIONS,
|
||||
reporters: [...respReporters, { username: null, full_name: null, email: null }],
|
||||
tags,
|
||||
status: '',
|
||||
status: CaseStatuses.open,
|
||||
search: 'hello',
|
||||
},
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
|
@ -162,6 +162,7 @@ describe('Case Configuration API', () => {
|
|||
reporters,
|
||||
tags: ['"coke"', '"pepsi"'],
|
||||
search: 'hello',
|
||||
status: CaseStatuses.open,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
@ -174,7 +175,7 @@ describe('Case Configuration API', () => {
|
|||
...DEFAULT_FILTER_OPTIONS,
|
||||
reporters: [...respReporters, { username: null, full_name: null, email: null }],
|
||||
tags: weirdTags,
|
||||
status: '',
|
||||
status: CaseStatuses.open,
|
||||
search: 'hello',
|
||||
},
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
|
@ -187,6 +188,7 @@ describe('Case Configuration API', () => {
|
|||
reporters,
|
||||
tags: ['"("', '"\\"double\\""'],
|
||||
search: 'hello',
|
||||
status: CaseStatuses.open,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
@ -310,7 +312,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
const data = [
|
||||
{
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
id: basicCase.id,
|
||||
version: basicCase.version,
|
||||
},
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
ServiceConnectorCaseResponse,
|
||||
ActionTypeExecutorResult,
|
||||
CommentType,
|
||||
CaseStatuses,
|
||||
} from '../../../../case/common/api';
|
||||
|
||||
import {
|
||||
|
@ -120,7 +121,7 @@ export const getCases = async ({
|
|||
filterOptions = {
|
||||
search: '',
|
||||
reporters: [],
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: [],
|
||||
},
|
||||
queryParams = {
|
||||
|
@ -134,7 +135,7 @@ export const getCases = async ({
|
|||
const query = {
|
||||
reporters: filterOptions.reporters.map((r) => r.username ?? '').filter((r) => r !== ''),
|
||||
tags: filterOptions.tags.map((t) => `"${t.replace(/"/g, '\\"')}"`),
|
||||
...(filterOptions.status !== '' ? { status: filterOptions.status } : {}),
|
||||
status: filterOptions.status,
|
||||
...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}),
|
||||
...queryParams,
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ import { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment }
|
|||
import {
|
||||
CommentResponse,
|
||||
ServiceConnectorCaseResponse,
|
||||
Status,
|
||||
CaseStatuses,
|
||||
UserAction,
|
||||
UserActionField,
|
||||
CaseResponse,
|
||||
|
@ -69,7 +69,7 @@ export const basicCase: Case = {
|
|||
},
|
||||
description: 'Security banana Issue',
|
||||
externalService: null,
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags,
|
||||
title: 'Another horrible breach!!',
|
||||
totalComment: 1,
|
||||
|
@ -98,8 +98,9 @@ export const basicCaseCommentPatch = {
|
|||
};
|
||||
|
||||
export const casesStatus: CasesStatus = {
|
||||
countClosedCases: 130,
|
||||
countOpenCases: 20,
|
||||
countInProgressCases: 40,
|
||||
countClosedCases: 130,
|
||||
};
|
||||
|
||||
export const basicPush = {
|
||||
|
@ -203,7 +204,7 @@ export const basicCommentSnake: CommentResponse = {
|
|||
|
||||
export const basicCaseSnake: CaseResponse = {
|
||||
...basicCase,
|
||||
status: 'open' as Status,
|
||||
status: CaseStatuses.open,
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
comments: [basicCommentSnake],
|
||||
|
@ -222,6 +223,7 @@ export const basicCaseSnake: CaseResponse = {
|
|||
|
||||
export const casesStatusSnake: CasesStatusResponse = {
|
||||
count_closed_cases: 130,
|
||||
count_in_progress_cases: 40,
|
||||
count_open_cases: 20,
|
||||
};
|
||||
|
||||
|
@ -325,5 +327,5 @@ export const basicCaseClosed: Case = {
|
|||
...basicCase,
|
||||
closedAt: '2020-02-25T23:06:33.798Z',
|
||||
closedBy: elasticUser,
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
UserAction,
|
||||
CaseConnector,
|
||||
CommentType,
|
||||
CaseStatuses,
|
||||
} from '../../../../case/common/api';
|
||||
|
||||
export { CaseConnector, ActionConnector } from '../../../../case/common/api';
|
||||
|
@ -57,7 +58,7 @@ export interface Case {
|
|||
createdBy: ElasticUser;
|
||||
description: string;
|
||||
externalService: CaseExternalService | null;
|
||||
status: string;
|
||||
status: CaseStatuses;
|
||||
tags: string[];
|
||||
title: string;
|
||||
totalComment: number;
|
||||
|
@ -75,7 +76,7 @@ export interface QueryParams {
|
|||
|
||||
export interface FilterOptions {
|
||||
search: string;
|
||||
status: string;
|
||||
status: CaseStatuses;
|
||||
tags: string[];
|
||||
reporters: User[];
|
||||
}
|
||||
|
@ -83,6 +84,7 @@ export interface FilterOptions {
|
|||
export interface CasesStatus {
|
||||
countClosedCases: number | null;
|
||||
countOpenCases: number | null;
|
||||
countInProgressCases: number | null;
|
||||
}
|
||||
|
||||
export interface AllCases extends CasesStatus {
|
||||
|
@ -95,6 +97,7 @@ export interface AllCases extends CasesStatus {
|
|||
export enum SortFieldCase {
|
||||
createdAt = 'createdAt',
|
||||
closedAt = 'closedAt',
|
||||
updatedAt = 'updatedAt',
|
||||
}
|
||||
|
||||
export interface ElasticUser {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { CaseStatuses } from '../../../../case/common/api';
|
||||
import { useUpdateCases, UseUpdateCases } from './use_bulk_update_case';
|
||||
import { basicCase } from './mock';
|
||||
import * as api from './api';
|
||||
|
@ -43,12 +44,12 @@ describe('useUpdateCases', () => {
|
|||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.updateBulkStatus([basicCase], 'closed');
|
||||
result.current.updateBulkStatus([basicCase], CaseStatuses.closed);
|
||||
await waitForNextUpdate();
|
||||
expect(spyOnPatchCases).toBeCalledWith(
|
||||
[
|
||||
{
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
id: basicCase.id,
|
||||
version: basicCase.version,
|
||||
},
|
||||
|
@ -64,7 +65,7 @@ describe('useUpdateCases', () => {
|
|||
useUpdateCases()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
result.current.updateBulkStatus([basicCase], 'closed');
|
||||
result.current.updateBulkStatus([basicCase], CaseStatuses.closed);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
isUpdated: true,
|
||||
|
@ -82,7 +83,7 @@ describe('useUpdateCases', () => {
|
|||
useUpdateCases()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
result.current.updateBulkStatus([basicCase], 'closed');
|
||||
result.current.updateBulkStatus([basicCase], CaseStatuses.closed);
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
});
|
||||
|
@ -95,7 +96,7 @@ describe('useUpdateCases', () => {
|
|||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
result.current.updateBulkStatus([basicCase], 'closed');
|
||||
result.current.updateBulkStatus([basicCase], CaseStatuses.closed);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current.isUpdated).toBeTruthy();
|
||||
result.current.dispatchResetIsUpdated();
|
||||
|
@ -114,7 +115,7 @@ describe('useUpdateCases', () => {
|
|||
useUpdateCases()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
result.current.updateBulkStatus([basicCase], 'closed');
|
||||
result.current.updateBulkStatus([basicCase], CaseStatuses.closed);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
isUpdated: false,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { useCallback, useReducer } from 'react';
|
||||
import { CaseStatuses } from '../../../../case/common/api';
|
||||
import {
|
||||
displaySuccessToast,
|
||||
errorToToaster,
|
||||
|
@ -86,7 +87,7 @@ export const useUpdateCases = (): UseUpdateCases => {
|
|||
caseTitle: resultCount === 1 ? firstTitle : '',
|
||||
};
|
||||
const message =
|
||||
resultCount && patchResponse[0].status === 'open'
|
||||
resultCount && patchResponse[0].status === CaseStatuses.open
|
||||
? i18n.REOPENED_CASES(messageArgs)
|
||||
: i18n.CLOSED_CASES(messageArgs);
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { useEffect, useReducer, useCallback } from 'react';
|
||||
import { CaseStatuses } from '../../../../case/common/api';
|
||||
|
||||
import { Case } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
@ -66,7 +67,7 @@ export const initialData: Case = {
|
|||
},
|
||||
description: '',
|
||||
externalService: null,
|
||||
status: '',
|
||||
status: CaseStatuses.open,
|
||||
tags: [],
|
||||
title: '',
|
||||
totalComment: 0,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { CaseStatuses } from '../../../../case/common/api';
|
||||
import {
|
||||
DEFAULT_FILTER_OPTIONS,
|
||||
DEFAULT_QUERY_PARAMS,
|
||||
|
@ -157,7 +158,7 @@ describe('useGetCases', () => {
|
|||
const newFilters = {
|
||||
search: 'new',
|
||||
tags: ['new'],
|
||||
status: 'closed',
|
||||
status: CaseStatuses.closed,
|
||||
};
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases());
|
||||
await waitForNextUpdate();
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { useCallback, useEffect, useReducer } from 'react';
|
||||
import { CaseStatuses } from '../../../../case/common/api';
|
||||
import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants';
|
||||
import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case } from './types';
|
||||
import { errorToToaster, useStateToaster } from '../../common/components/toasters';
|
||||
|
@ -94,7 +95,7 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS
|
|||
export const DEFAULT_FILTER_OPTIONS: FilterOptions = {
|
||||
search: '',
|
||||
reporters: [],
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
|
@ -108,6 +109,7 @@ export const DEFAULT_QUERY_PARAMS: QueryParams = {
|
|||
export const initialData: AllCases = {
|
||||
cases: [],
|
||||
countClosedCases: null,
|
||||
countInProgressCases: null,
|
||||
countOpenCases: null,
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
|
|
|
@ -27,6 +27,7 @@ describe('useGetCasesStatus', () => {
|
|||
expect(result.current).toEqual({
|
||||
countClosedCases: null,
|
||||
countOpenCases: null,
|
||||
countInProgressCases: null,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
fetchCasesStatus: result.current.fetchCasesStatus,
|
||||
|
@ -56,6 +57,7 @@ describe('useGetCasesStatus', () => {
|
|||
expect(result.current).toEqual({
|
||||
countClosedCases: casesStatus.countClosedCases,
|
||||
countOpenCases: casesStatus.countOpenCases,
|
||||
countInProgressCases: casesStatus.countInProgressCases,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
fetchCasesStatus: result.current.fetchCasesStatus,
|
||||
|
@ -79,6 +81,7 @@ describe('useGetCasesStatus', () => {
|
|||
expect(result.current).toEqual({
|
||||
countClosedCases: 0,
|
||||
countOpenCases: 0,
|
||||
countInProgressCases: 0,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
fetchCasesStatus: result.current.fetchCasesStatus,
|
||||
|
|
|
@ -18,6 +18,7 @@ interface CasesStatusState extends CasesStatus {
|
|||
|
||||
const initialData: CasesStatusState = {
|
||||
countClosedCases: null,
|
||||
countInProgressCases: null,
|
||||
countOpenCases: null,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
|
@ -57,6 +58,7 @@ export const useGetCasesStatus = (): UseGetCasesStatus => {
|
|||
});
|
||||
setCasesStatusState({
|
||||
countClosedCases: 0,
|
||||
countInProgressCases: 0,
|
||||
countOpenCases: 0,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
|
|
|
@ -65,8 +65,9 @@ export const convertToCamelCase = <T, U extends {}>(snakeCase: T): U =>
|
|||
|
||||
export const convertAllCasesToCamel = (snakeCases: CasesFindResponse): AllCases => ({
|
||||
cases: snakeCases.cases.map((snakeCase) => convertToCamelCase<CaseResponse, Case>(snakeCase)),
|
||||
countClosedCases: snakeCases.count_closed_cases,
|
||||
countOpenCases: snakeCases.count_open_cases,
|
||||
countInProgressCases: snakeCases.count_in_progress_cases,
|
||||
countClosedCases: snakeCases.count_closed_cases,
|
||||
page: snakeCases.page,
|
||||
perPage: snakeCases.per_page,
|
||||
total: snakeCases.total,
|
||||
|
|
|
@ -115,10 +115,6 @@ export const CREATE_CASE = i18n.translate('xpack.securitySolution.case.caseView.
|
|||
defaultMessage: 'Create case',
|
||||
});
|
||||
|
||||
export const CLOSED_CASE = i18n.translate('xpack.securitySolution.case.caseView.closedCase', {
|
||||
defaultMessage: 'Closed case',
|
||||
});
|
||||
|
||||
export const CLOSE_CASE = i18n.translate('xpack.securitySolution.case.caseView.closeCase', {
|
||||
defaultMessage: 'Close case',
|
||||
});
|
||||
|
@ -127,10 +123,6 @@ export const REOPEN_CASE = i18n.translate('xpack.securitySolution.case.caseView.
|
|||
defaultMessage: 'Reopen case',
|
||||
});
|
||||
|
||||
export const REOPENED_CASE = i18n.translate('xpack.securitySolution.case.caseView.reopenedCase', {
|
||||
defaultMessage: 'Reopened case',
|
||||
});
|
||||
|
||||
export const CASE_NAME = i18n.translate('xpack.securitySolution.case.caseView.caseName', {
|
||||
defaultMessage: 'Case name',
|
||||
});
|
||||
|
|
|
@ -115,22 +115,21 @@ export const CREATE_CASE = i18n.translate('xpack.securitySolution.case.caseView.
|
|||
defaultMessage: 'Create case',
|
||||
});
|
||||
|
||||
export const CLOSED_CASE = i18n.translate('xpack.securitySolution.case.caseView.closedCase', {
|
||||
defaultMessage: 'Closed case',
|
||||
});
|
||||
|
||||
export const CLOSE_CASE = i18n.translate('xpack.securitySolution.case.caseView.closeCase', {
|
||||
defaultMessage: 'Close case',
|
||||
});
|
||||
|
||||
export const MARK_CASE_IN_PROGRESS = i18n.translate(
|
||||
'xpack.securitySolution.case.caseView.markInProgress',
|
||||
{
|
||||
defaultMessage: 'Mark in progress',
|
||||
}
|
||||
);
|
||||
|
||||
export const REOPEN_CASE = i18n.translate('xpack.securitySolution.case.caseView.reopenCase', {
|
||||
defaultMessage: 'Reopen case',
|
||||
});
|
||||
|
||||
export const REOPENED_CASE = i18n.translate('xpack.securitySolution.case.caseView.reopenedCase', {
|
||||
defaultMessage: 'Reopened case',
|
||||
});
|
||||
|
||||
export const CASE_NAME = i18n.translate('xpack.securitySolution.case.caseView.caseName', {
|
||||
defaultMessage: 'Case name',
|
||||
});
|
||||
|
@ -238,3 +237,22 @@ export const NO_CONNECTOR = i18n.translate('xpack.securitySolution.case.common.n
|
|||
export const UNKNOWN = i18n.translate('xpack.securitySolution.case.caseView.unknown', {
|
||||
defaultMessage: 'Unknown',
|
||||
});
|
||||
|
||||
export const MARKED_CASE_AS = i18n.translate('xpack.securitySolution.case.caseView.markedCaseAs', {
|
||||
defaultMessage: 'marked case as',
|
||||
});
|
||||
|
||||
export const OPEN_CASES = i18n.translate('xpack.securitySolution.case.caseTable.openCases', {
|
||||
defaultMessage: 'Open cases',
|
||||
});
|
||||
|
||||
export const CLOSED_CASES = i18n.translate('xpack.securitySolution.case.caseTable.closedCases', {
|
||||
defaultMessage: 'Closed cases',
|
||||
});
|
||||
|
||||
export const IN_PROGRESS_CASES = i18n.translate(
|
||||
'xpack.securitySolution.case.caseTable.inProgressCases',
|
||||
{
|
||||
defaultMessage: 'In progress cases',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -16387,7 +16387,6 @@
|
|||
"xpack.securitySolution.case.caseView.caseOpened": "ケースを開きました",
|
||||
"xpack.securitySolution.case.caseView.caseRefresh": "ケースを更新",
|
||||
"xpack.securitySolution.case.caseView.closeCase": "ケースを閉じる",
|
||||
"xpack.securitySolution.case.caseView.closedCase": "閉じたケース",
|
||||
"xpack.securitySolution.case.caseView.closedOn": "終了日",
|
||||
"xpack.securitySolution.case.caseView.cloudDeploymentLink": "クラウド展開",
|
||||
"xpack.securitySolution.case.caseView.comment": "コメント",
|
||||
|
@ -16435,7 +16434,6 @@
|
|||
"xpack.securitySolution.case.caseView.pushToServiceDisableByNoConfigTitle": "外部コネクターを構成",
|
||||
"xpack.securitySolution.case.caseView.pushToServiceDisableByNoConnectors": "外部システムでケースを開いて更新するには、{link}を設定する必要があります。",
|
||||
"xpack.securitySolution.case.caseView.reopenCase": "ケースを再開",
|
||||
"xpack.securitySolution.case.caseView.reopenedCase": "ケースを再開する",
|
||||
"xpack.securitySolution.case.caseView.reporterLabel": "報告者",
|
||||
"xpack.securitySolution.case.caseView.requiredUpdateToExternalService": "{ externalService }インシデントの更新が必要です",
|
||||
"xpack.securitySolution.case.caseView.sendEmalLinkAria": "クリックすると、{user}に電子メールを送信します",
|
||||
|
|
|
@ -16405,7 +16405,6 @@
|
|||
"xpack.securitySolution.case.caseView.caseOpened": "案例已打开",
|
||||
"xpack.securitySolution.case.caseView.caseRefresh": "刷新案例",
|
||||
"xpack.securitySolution.case.caseView.closeCase": "关闭案例",
|
||||
"xpack.securitySolution.case.caseView.closedCase": "已关闭案例",
|
||||
"xpack.securitySolution.case.caseView.closedOn": "关闭于",
|
||||
"xpack.securitySolution.case.caseView.cloudDeploymentLink": "云部署",
|
||||
"xpack.securitySolution.case.caseView.comment": "注释",
|
||||
|
@ -16453,7 +16452,6 @@
|
|||
"xpack.securitySolution.case.caseView.pushToServiceDisableByNoConfigTitle": "配置外部连接器",
|
||||
"xpack.securitySolution.case.caseView.pushToServiceDisableByNoConnectors": "要在外部系统上打开和更新案例,必须配置{link}。",
|
||||
"xpack.securitySolution.case.caseView.reopenCase": "重新打开案例",
|
||||
"xpack.securitySolution.case.caseView.reopenedCase": "重新打开的案例",
|
||||
"xpack.securitySolution.case.caseView.reporterLabel": "报告者",
|
||||
"xpack.securitySolution.case.caseView.requiredUpdateToExternalService": "需要更新 { externalService } 事件",
|
||||
"xpack.securitySolution.case.caseView.sendEmalLinkAria": "单击可向 {user} 发送电子邮件",
|
||||
|
|
|
@ -87,6 +87,67 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
});
|
||||
});
|
||||
|
||||
it('filters by status', async () => {
|
||||
const { body: openCase } = await supertest
|
||||
.post(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCaseReq);
|
||||
|
||||
const { body: toCloseCase } = await supertest
|
||||
.post(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCaseReq);
|
||||
|
||||
await supertest
|
||||
.patch(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
cases: [
|
||||
{
|
||||
id: toCloseCase.id,
|
||||
version: toCloseCase.version,
|
||||
status: 'closed',
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${CASES_URL}/_find?sortOrder=asc&status=open`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
...findCasesResp,
|
||||
total: 1,
|
||||
cases: [openCase],
|
||||
count_open_cases: 1,
|
||||
count_closed_cases: 1,
|
||||
count_in_progress_cases: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters by reporters', async () => {
|
||||
const { body: postedCase } = await supertest
|
||||
.post(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCaseReq);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${CASES_URL}/_find?sortOrder=asc&reporters=elastic`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
...findCasesResp,
|
||||
total: 1,
|
||||
cases: [postedCase],
|
||||
count_open_cases: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly counts comments', async () => {
|
||||
const { body: postedCase } = await supertest
|
||||
.post(CASES_URL)
|
||||
|
@ -127,8 +188,14 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
});
|
||||
});
|
||||
|
||||
it('correctly counts open/closed', async () => {
|
||||
it('correctly counts open/closed/in-progress', async () => {
|
||||
await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq);
|
||||
|
||||
const { body: inProgreeCase } = await supertest
|
||||
.post(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCaseReq);
|
||||
|
||||
const { body: postedCase } = await supertest
|
||||
.post(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
|
@ -149,6 +216,20 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
await supertest
|
||||
.patch(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
cases: [
|
||||
{
|
||||
id: inProgreeCase.id,
|
||||
version: inProgreeCase.version,
|
||||
status: 'in-progress',
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${CASES_URL}/_find?sortOrder=asc`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
|
@ -157,7 +238,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
expect(body.count_open_cases).to.eql(1);
|
||||
expect(body.count_closed_cases).to.eql(1);
|
||||
expect(body.count_in_progress_cases).to.eql(1);
|
||||
});
|
||||
|
||||
it('unhappy path - 400s when bad query supplied', async () => {
|
||||
await supertest
|
||||
.get(`${CASES_URL}/_find?perPage=true`)
|
||||
|
|
|
@ -156,6 +156,28 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
.expect(400);
|
||||
});
|
||||
|
||||
it('unhappy path - 400s when unsupported status sent', async () => {
|
||||
const { body: postedCase } = await supertest
|
||||
.post(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCaseReq)
|
||||
.expect(200);
|
||||
|
||||
await supertest
|
||||
.patch(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
cases: [
|
||||
{
|
||||
id: postedCase.id,
|
||||
version: postedCase.version,
|
||||
status: 'not-supported',
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('unhappy path - 400s when bad connector type sent', async () => {
|
||||
const { body: postedCase } = await supertest
|
||||
.post(CASES_URL)
|
||||
|
|
|
@ -23,6 +23,12 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
it('should return case statuses', async () => {
|
||||
await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq);
|
||||
|
||||
const { body: inProgressCase } = await supertest
|
||||
.post(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(postCaseReq);
|
||||
|
||||
const { body: postedCase } = await supertest
|
||||
.post(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
|
@ -43,6 +49,20 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
await supertest
|
||||
.patch(CASES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
cases: [
|
||||
{
|
||||
id: inProgressCase.id,
|
||||
version: inProgressCase.version,
|
||||
status: 'in-progress',
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(CASE_STATUS_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
|
@ -52,6 +72,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(body).to.eql({
|
||||
count_open_cases: 1,
|
||||
count_closed_cases: 1,
|
||||
count_in_progress_cases: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
CommentRequestUserType,
|
||||
CommentRequestAlertType,
|
||||
CommentType,
|
||||
CaseStatuses,
|
||||
} from '../../../../plugins/case/common/api';
|
||||
export const defaultUser = { email: null, full_name: null, username: 'elastic' };
|
||||
export const postCaseReq: CasePostRequest = {
|
||||
|
@ -49,7 +50,7 @@ export const postCaseResp = (
|
|||
closed_by: null,
|
||||
created_by: defaultUser,
|
||||
external_service: null,
|
||||
status: 'open',
|
||||
status: CaseStatuses.open,
|
||||
updated_by: null,
|
||||
});
|
||||
|
||||
|
@ -78,4 +79,5 @@ export const findCasesResp: CasesFindResponse = {
|
|||
cases: [],
|
||||
count_open_cases: 0,
|
||||
count_closed_cases: 0,
|
||||
count_in_progress_cases: 0,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue