Skip to content
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