Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const UserActionTypes = {
delete_case: 'delete_case',
category: 'category',
customFields: 'customFields',
observables: 'observables',
} as const;

type UserActionActionTypeKeys = keyof typeof UserActionTypes;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { UserActionTypes } from '../action/v1';
import { ObservablesUserActionPayloadRt, ObservablesUserActionRt } from './v1';

describe('Observables', () => {
describe('ObservablesUserActionPayloadRt', () => {
const defaultRequest = {
observables: {
count: 1,
actionType: 'add',
},
};

it('has expected attributes in request', () => {
const query = ObservablesUserActionPayloadRt.decode(defaultRequest);

expect(query).toStrictEqual({
_tag: 'Right',
right: defaultRequest,
});
});

it('removes foo:bar attributes from request', () => {
const query = ObservablesUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' });

expect(query).toStrictEqual({
_tag: 'Right',
right: defaultRequest,
});
});

it('removes foo:bar attributes from observables', () => {
const query = ObservablesUserActionPayloadRt.decode({
observables: { ...defaultRequest.observables, foo: 'bar' },
});

expect(query).toStrictEqual({
_tag: 'Right',
right: defaultRequest,
});
});
});
describe('ObservablesUserActionRt', () => {
const defaultRequest = {
type: UserActionTypes.observables,
payload: {
observables: {
count: 1,
actionType: 'add',
},
},
};

it('has expected attributes in request', () => {
const query = ObservablesUserActionRt.decode(defaultRequest);

expect(query).toStrictEqual({
_tag: 'Right',
right: defaultRequest,
});
});

it('removes foo:bar attributes from request', () => {
const query = ObservablesUserActionRt.decode({ ...defaultRequest, foo: 'bar' });

expect(query).toStrictEqual({
_tag: 'Right',
right: defaultRequest,
});
});

it('removes foo:bar attributes from payload', () => {
const query = ObservablesUserActionRt.decode({
...defaultRequest,
payload: { ...defaultRequest.payload, foo: 'bar' },
});

expect(query).toStrictEqual({
_tag: 'Right',
right: defaultRequest,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import * as rt from 'io-ts';
import { UserActionTypes } from '../action/v1';

const ObservablesActionTypeRt = rt.union([
rt.literal('add'),
rt.literal('delete'),
rt.literal('update'),
]);

export const ObservablePayloadRt = rt.strict({
count: rt.number,
actionType: ObservablesActionTypeRt,
});

export const ObservablesUserActionPayloadRt = rt.strict({ observables: ObservablePayloadRt });

export const ObservablesUserActionRt = rt.strict({
type: rt.literal(UserActionTypes.observables),
payload: ObservablesUserActionPayloadRt,
});

export type ObservablesActionType = rt.TypeOf<typeof ObservablesActionTypeRt>;
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { StatusUserActionRt } from './status/v1';
import { TagsUserActionRt } from './tags/v1';
import { TitleUserActionRt } from './title/v1';
import { CustomFieldsUserActionRt } from './custom_fields/v1';

import { ObservablesUserActionRt } from './observables/v1';
export { UserActionTypes, UserActionActions } from './action/v1';
export { StatusUserActionRt } from './status/v1';

Expand Down Expand Up @@ -61,6 +61,7 @@ const BasicUserActionsRt = rt.union([
DeleteCaseUserActionRt,
CategoryUserActionRt,
CustomFieldsUserActionRt,
ObservablesUserActionRt,
]);

const CommonUserActionsWithIdsRt = rt.union([BasicUserActionsRt, CommentUserActionRt]);
Expand Down Expand Up @@ -154,3 +155,4 @@ export type CreateCaseUserActionWithoutConnectorId = UserActionWithAttributes<
rt.TypeOf<typeof CreateCaseUserActionWithoutConnectorIdRt>
>;
export type CustomFieldsUserAction = UserAction<rt.TypeOf<typeof CustomFieldsUserActionRt>>;
export type ObservablesUserAction = UserAction<rt.TypeOf<typeof ObservablesUserActionRt>>;
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,24 @@ export const TOTAL_USERS_ASSIGNED = (total: number) =>
defaultMessage: '{total} assigned',
values: { total },
});

export const ADDED_OBSERVABLES = (totalObservables: number): string =>
i18n.translate('xpack.cases.caseView.observables.addedObservables', {
values: { totalObservables },
defaultMessage:
'added {totalObservables, plural, =1 {an} other {{totalObservables}}} {totalObservables, plural, =1 {observable} other {observables}}',
});

export const DELETED_OBSERVABLES = (totalObservables: number): string =>
i18n.translate('xpack.cases.caseView.observables.deletedObservables', {
values: { totalObservables },
defaultMessage:
'deleted {totalObservables, plural, =1 {an} other {{totalObservables}}} {totalObservables, plural, =1 {observable} other {observables}}',
});

export const UPDATED_OBSERVABLES = (totalObservables: number): string =>
i18n.translate('xpack.cases.caseView.observables.updatedObservables', {
values: { totalObservables },
defaultMessage:
'updated {totalObservables, plural, =1 {an} other {{totalObservables}}} {totalObservables, plural, =1 {observable} other {observables}}',
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { createCaseUserActionBuilder } from './create_case';
import type { UserActionBuilderMap } from './types';
import { createCategoryUserActionBuilder } from './category';
import { createCustomFieldsUserActionBuilder } from './custom_fields/custom_fields';
import { createObservablesUserActionBuilder } from './observables';

export const builderMap: UserActionBuilderMap = {
create_case: createCaseUserActionBuilder,
Expand All @@ -34,4 +35,5 @@ export const builderMap: UserActionBuilderMap = {
assignees: createAssigneesUserActionBuilder,
category: createCategoryUserActionBuilder,
customFields: createCustomFieldsUserActionBuilder,
observables: createObservablesUserActionBuilder,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { EuiCommentList } from '@elastic/eui';
import { screen } from '@testing-library/react';
import { UserActionActions } from '../../../common/types/domain';

import { renderWithTestingProviders } from '../../common/mock';
import { getUserAction } from '../../containers/mock';
import { getMockBuilderArgs } from './mock';
import { createObservablesUserActionBuilder } from './observables';
import type { ObservablesActionType } from '../../../common/types/domain/user_action/observables/v1';

jest.mock('../../common/lib/kibana');
jest.mock('../../common/navigation/hooks');

describe('createObservablesUserActionBuilder ', () => {
const builderArgs = getMockBuilderArgs();

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

const tests: [number, ObservablesActionType, string][] = [
[1, 'add', 'added an observable'],
[1, 'delete', 'deleted an observable'],
[1, 'update', 'updated an observable'],
[10, 'add', 'added 10 observables'],
];

it.each(tests)(
'renders correctly when changed observables to %s',
async (count, actionType, label) => {
const userAction = getUserAction('observables', UserActionActions.update, {
payload: { observables: { count, actionType } },
});
const builder = createObservablesUserActionBuilder({
...builderArgs,
userAction,
});

const createdUserAction = builder.build();
renderWithTestingProviders(<EuiCommentList comments={createdUserAction} />);

expect(screen.getByTestId(`observables-${actionType}-action`)).toHaveTextContent(label);
}
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { type ReactNode } from 'react';
import { EuiText } from '@elastic/eui';
import type { SnakeToCamelCase } from '../../../common/types';
import type { ObservablesUserAction } from '../../../common/types/domain';
import type { UserActionBuilder } from './types';

import { createCommonUpdateUserActionBuilder } from './common';
import { ADDED_OBSERVABLES, DELETED_OBSERVABLES, UPDATED_OBSERVABLES } from './translations';
import type { ObservablesActionType } from '../../../common/types/domain/user_action/observables/v1';

const getLabel: (actionType: ObservablesActionType, count: number) => ReactNode = (
actionType,
count
) => {
let label = '';
switch (actionType) {
case 'add':
label = ADDED_OBSERVABLES(count);
break;
case 'delete':
label = DELETED_OBSERVABLES(count);
break;
case 'update':
label = UPDATED_OBSERVABLES(count);
break;
}
return (
<EuiText size="s" data-test-subj={`observables-${actionType}-action`}>
{label}
</EuiText>
);
};
export const createObservablesUserActionBuilder: UserActionBuilder = ({
userAction,
userProfiles,
handleOutlineComment,
}) => ({
build: () => {
const action = userAction as SnakeToCamelCase<ObservablesUserAction>;
const { count, actionType } = action?.payload?.observables;
const label = getLabel(actionType, count);

if (count > 0) {
const commonBuilder = createCommonUpdateUserActionBuilder({
userProfiles,
userAction,
handleOutlineComment,
label,
icon: 'dot',
});

return commonBuilder.build();
}
return [];
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { createSettingsUserActionBuilder } from './settings';
jest.mock('../../common/lib/kibana');
jest.mock('../../common/navigation/hooks');

describe('createStatusUserActionBuilder ', () => {
describe('createSettingsUserActionBuilder ', () => {
const builderArgs = getMockBuilderArgs();

beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ export const CUSTOM_FIELDS = i18n.translate('xpack.cases.caseView.userActions.cu
defaultMessage: 'Custom Fields',
});

export const OBSERVABLES = i18n.translate('xpack.cases.caseView.userActions.observables', {
defaultMessage: 'Observables',
});

export const USER_ACTION_EDITED = (type: string) =>
i18n.translate('xpack.cases.caseView.userActions.edited', {
values: { type },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const getUserActionAriaLabel = (type: keyof typeof UserActionTypes) => {
delete_case: i18n.CASE_DELETED,
category: i18n.CATEGORY,
customFields: i18n.CUSTOM_FIELDS,
observables: i18n.OBSERVABLES,
};

switch (type) {
Expand Down
Loading