Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ResponseOps][Cases] Allow users to create templates #184104

Merged
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
37ac85f
initial commit
js-jankisalvi May 14, 2024
9be6c68
add connector field
js-jankisalvi May 16, 2024
edfc999
map connector fields
js-jankisalvi May 17, 2024
6641ec8
syncAlerts added
js-jankisalvi May 17, 2024
e1b6d3a
Merge remote-tracking branch 'upstream/feat/case_templates' into case…
js-jankisalvi May 17, 2024
6860034
Merge remote-tracking branch 'upstream/feat/case_templates' into case…
js-jankisalvi May 21, 2024
41c025d
add custom fields component
js-jankisalvi May 21, 2024
21436db
Merge remote-tracking branch 'upstream/feat/case_templates' into case…
js-jankisalvi May 23, 2024
5de5bd2
add tags field, update default value of connector
js-jankisalvi May 23, 2024
027ad22
add unit tests
js-jankisalvi May 23, 2024
e64508c
Remove custom field flyout, add tests
js-jankisalvi May 24, 2024
2024212
add template section description
js-jankisalvi May 27, 2024
81f3d0b
remove path from case fields
js-jankisalvi May 27, 2024
95d3c2f
add max length validations, tests
js-jankisalvi May 28, 2024
03d8f07
add utils, flyout and configure case tests
js-jankisalvi May 30, 2024
448ea94
add e2e test for templates
js-jankisalvi May 30, 2024
c2d1a35
Merge remote-tracking branch 'upstream/feat/case_templates' into case…
js-jankisalvi May 30, 2024
db4b1a5
Clean up
js-jankisalvi May 30, 2024
83f3a6d
set autoFocus
js-jankisalvi May 30, 2024
1999ca0
remove caseFields path
js-jankisalvi May 30, 2024
49220d9
add e2e tests to serverless
js-jankisalvi May 31, 2024
75f24e2
fix type error
js-jankisalvi May 31, 2024
82cd6af
lint fix title
js-jankisalvi May 31, 2024
a468d94
use description text area, remove session storage key for description…
js-jankisalvi May 31, 2024
ef4dc10
fix flaky functional test
js-jankisalvi Jun 3, 2024
d298b5a
add retry to wait for templates-list to exist, add optional label to …
js-jankisalvi Jun 3, 2024
9c29363
fix function name
js-jankisalvi Jun 3, 2024
a833425
Merge branch 'feat/case_templates' into case-templates-ui
kibanamachine Jun 3, 2024
4f3f91f
add custom_fields tests, remove unnecessary args from renderBody of f…
js-jankisalvi Jun 5, 2024
a28298a
Merge branch 'case-templates-ui' of https://github.com/js-jankisalvi/…
js-jankisalvi Jun 5, 2024
8379679
PR feedback 1
js-jankisalvi Jun 6, 2024
e4aa6ad
PR feedback 2
js-jankisalvi Jun 7, 2024
95a77b4
typo fixed
js-jankisalvi Jun 7, 2024
dc64cc8
test update
js-jankisalvi Jun 7, 2024
d5b7f24
Merge branch 'feat/case_templates' into case-templates-ui
js-jankisalvi Jun 7, 2024
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
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/types/domain/case/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,4 @@ export type CaseAttributes = rt.TypeOf<typeof CaseAttributesRt>;
export type CaseSettings = rt.TypeOf<typeof CaseSettingsRt>;
export type RelatedCase = rt.TypeOf<typeof RelatedCaseRt>;
export type AttachmentTotals = rt.TypeOf<typeof AttachmentTotalsRt>;
export type CaseBaseOptionalFields = rt.TypeOf<typeof CaseBaseOptionalFieldsRt>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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, { useMemo } from 'react';
import { sortBy } from 'lodash';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';

import type { CasesConfigurationUI } from '../../../common/ui';
import { builderMap as customFieldsBuilderMap } from '../custom_fields/builder';
import * as i18n from './translations';

interface Props {
isLoading: boolean;
setAsOptional?: boolean;
configurationCustomFields: CasesConfigurationUI['customFields'];
}

const CustomFieldsComponent: React.FC<Props> = ({
isLoading,
setAsOptional,
configurationCustomFields,
}) => {
const sortedCustomFields = useMemo(
() => sortCustomFieldsByLabel(configurationCustomFields),
[configurationCustomFields]
);

const customFieldsComponents = sortedCustomFields.map(
(customField: CasesConfigurationUI['customFields'][number]) => {
const customFieldFactory = customFieldsBuilderMap[customField.type];
const customFieldType = customFieldFactory().build();

const CreateComponent = customFieldType.Create;

return (
<CreateComponent
isLoading={isLoading}
customFieldConfiguration={customField}
key={customField.key}
setAsOptional={setAsOptional}
/>
);
}
);

if (!configurationCustomFields.length) {
return null;
}

return (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiText size="m">
<h3>{i18n.ADDITIONAL_FIELDS}</h3>
</EuiText>
<EuiSpacer size="xs" />
<EuiFlexItem data-test-subj="caseCustomFields">{customFieldsComponents}</EuiFlexItem>
</EuiFlexGroup>
);
};

CustomFieldsComponent.displayName = 'CustomFields';

export const CustomFields = React.memo(CustomFieldsComponent);

const sortCustomFieldsByLabel = (configCustomFields: CasesConfigurationUI['customFields']) => {
return sortBy(configCustomFields, (configCustomField) => {
return configCustomField.label;
});
};
255 changes: 255 additions & 0 deletions x-pack/plugins/cases/public/components/case_form_fields/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/*
* 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 { screen, waitFor, within } from '@testing-library/react';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';

import type { AppMockRenderer } from '../../common/mock';
import { createAppMockRenderer } from '../../common/mock';
import { FormTestComponent } from '../../common/test_utils';
import { customFieldsConfigurationMock } from '../../containers/mock';
import { userProfiles } from '../../containers/user_profiles/api.mock';

import { CaseFormFields } from '.';
import userEvent from '@testing-library/user-event';
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';

jest.mock('../../containers/user_profiles/api');

const appId = 'securitySolution';
const draftKey = `cases.${appId}.createCaseTemplate.description.markdownEditor`;

describe('CaseFormFields', () => {
let appMock: AppMockRenderer;
const onSubmit = jest.fn();
const defaultProps = {
isLoading: false,
configurationCustomFields: [],
draftStorageKey: '',
};

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

afterEach(() => {
sessionStorage.removeItem(draftKey);
});

it('renders correctly', async () => {
appMock.render(
<FormTestComponent onSubmit={onSubmit}>
<CaseFormFields {...defaultProps} />
</FormTestComponent>
);

expect(await screen.findByTestId('case-form-fields')).toBeInTheDocument();
});

it('renders case fields correctly', async () => {
appMock.render(
<FormTestComponent onSubmit={onSubmit}>
<CaseFormFields {...defaultProps} />
</FormTestComponent>
);

expect(await screen.findByTestId('caseTitle')).toBeInTheDocument();
expect(await screen.findByTestId('caseTags')).toBeInTheDocument();
expect(await screen.findByTestId('caseCategory')).toBeInTheDocument();
expect(await screen.findByTestId('caseSeverity')).toBeInTheDocument();
expect(await screen.findByTestId('caseDescription')).toBeInTheDocument();
expect(await screen.findByTestId('caseSyncAlerts')).toBeInTheDocument();
});

it('does not render customFields when empty', () => {
appMock.render(
<FormTestComponent onSubmit={onSubmit}>
<CaseFormFields {...defaultProps} />
</FormTestComponent>
);

expect(screen.queryByTestId('caseCustomFields')).not.toBeInTheDocument();
});

it('renders customFields when not empty', async () => {
appMock.render(
<FormTestComponent onSubmit={onSubmit}>
<CaseFormFields
isLoading={false}
configurationCustomFields={customFieldsConfigurationMock}
draftStorageKey=""
/>
</FormTestComponent>
);

expect(await screen.findByTestId('caseCustomFields')).toBeInTheDocument();
});

it('does not render assignees when no platinum license', () => {
appMock.render(
<FormTestComponent onSubmit={onSubmit}>
<CaseFormFields {...defaultProps} />
</FormTestComponent>
);

expect(screen.queryByTestId('createCaseAssigneesComboBox')).not.toBeInTheDocument();
});

it('renders assignees when platinum license', async () => {
const license = licensingMock.createLicense({
license: { type: 'platinum' },
});

appMock = createAppMockRenderer({ license });

appMock.render(
<FormTestComponent onSubmit={onSubmit}>
<CaseFormFields {...defaultProps} />
</FormTestComponent>
);

expect(await screen.findByTestId('createCaseAssigneesComboBox')).toBeInTheDocument();
});

it('does not render syncAlerts when feature is not enabled', () => {
appMock = createAppMockRenderer({
features: { alerts: { sync: false, enabled: true } },
});

appMock.render(
<FormTestComponent onSubmit={onSubmit}>
<CaseFormFields {...defaultProps} />
</FormTestComponent>
);

expect(screen.queryByTestId('caseSyncAlerts')).not.toBeInTheDocument();
});

it('calls onSubmit with case fields', async () => {
appMock.render(
<FormTestComponent onSubmit={onSubmit}>
<CaseFormFields {...defaultProps} />
</FormTestComponent>
);

const caseTitle = await screen.findByTestId('caseTitle');
userEvent.paste(within(caseTitle).getByTestId('input'), 'Case with Template 1');

const caseDescription = await screen.findByTestId('caseDescription');
userEvent.paste(
within(caseDescription).getByTestId('euiMarkdownEditorTextArea'),
'This is a case description'
);

const caseTags = await screen.findByTestId('caseTags');
userEvent.paste(within(caseTags).getByRole('combobox'), 'template-1');
userEvent.keyboard('{enter}');

const caseCategory = await screen.findByTestId('caseCategory');
userEvent.type(within(caseCategory).getByRole('combobox'), 'new {enter}');

userEvent.click(screen.getByText('Submit'));

await waitFor(() => {
expect(onSubmit).toBeCalledWith(
{
category: 'new',
tags: ['template-1'],
description: 'This is a case description',
title: 'Case with Template 1',
syncAlerts: true,
},
true
);
});
});

it('calls onSubmit with custom fields', async () => {
const newProps = {
...defaultProps,
configurationCustomFields: customFieldsConfigurationMock,
};

appMock.render(
<FormTestComponent onSubmit={onSubmit}>
<CaseFormFields {...newProps} />
</FormTestComponent>
);

expect(await screen.findByTestId('caseCustomFields')).toBeInTheDocument();

const textField = customFieldsConfigurationMock[0];
const toggleField = customFieldsConfigurationMock[1];

const textCustomField = await screen.findByTestId(
`${textField.key}-${textField.type}-create-custom-field`
);

userEvent.clear(textCustomField);
userEvent.paste(textCustomField, 'My text test value 1');

userEvent.click(
await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
);

userEvent.click(screen.getByText('Submit'));
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved

await waitFor(() => {
expect(onSubmit).toBeCalledWith(
{
category: null,
tags: [],
syncAlerts: true,
customFields: {
test_key_1: 'My text test value 1',
test_key_2: false,
test_key_4: false,
},
},
true
);
});
});

it('calls onSubmit with assignees', async () => {
const license = licensingMock.createLicense({
license: { type: 'platinum' },
});

appMock = createAppMockRenderer({ license });

appMock.render(
<FormTestComponent onSubmit={onSubmit}>
<CaseFormFields {...defaultProps} />
</FormTestComponent>
);

const assigneesComboBox = await screen.findByTestId('createCaseAssigneesComboBox');

userEvent.click(await within(assigneesComboBox).findByTestId('comboBoxToggleListButton'));

await waitForEuiPopoverOpen();

userEvent.click(screen.getByText(`${userProfiles[0].user.full_name}`));

userEvent.click(screen.getByText('Submit'));

await waitFor(() => {
expect(onSubmit).toBeCalledWith(
{
category: null,
tags: [],
syncAlerts: true,
assignees: [{ uid: userProfiles[0].uid }],
},
true
);
});
});
});
59 changes: 59 additions & 0 deletions x-pack/plugins/cases/public/components/case_form_fields/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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, { memo } from 'react';
import { EuiFlexGroup } from '@elastic/eui';
import { Title } from '../create/title';
import { Tags } from '../create/tags';
import { Category } from '../create/category';
import { Severity } from '../create/severity';
import { Description } from '../create/description';
import { useCasesFeatures } from '../../common/use_cases_features';
import { Assignees } from '../create/assignees';
import { CustomFields } from './custom_fields';
import { SyncAlertsToggle } from '../create/sync_alerts_toggle';
import type { CasesConfigurationUI } from '../../containers/types';

interface Props {
isLoading: boolean;
configurationCustomFields: CasesConfigurationUI['customFields'];
draftStorageKey: string;
}

const CaseFormFieldsComponent: React.FC<Props> = ({
isLoading,
configurationCustomFields,
draftStorageKey,
}) => {
const { caseAssignmentAuthorized, isSyncAlertsEnabled } = useCasesFeatures();

return (
<EuiFlexGroup data-test-subj="case-form-fields" direction="column">
<Title isLoading={isLoading} />
{caseAssignmentAuthorized ? <Assignees isLoading={isLoading} /> : null}
<Tags isLoading={isLoading} />

<Category isLoading={isLoading} />

<Severity isLoading={isLoading} />

<Description isLoading={isLoading} draftStorageKey={draftStorageKey} />

{isSyncAlertsEnabled ? <SyncAlertsToggle isLoading={isLoading} /> : null}
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved

<CustomFields
isLoading={isLoading}
setAsOptional={true}
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved
configurationCustomFields={configurationCustomFields}
/>
</EuiFlexGroup>
);
};

CaseFormFieldsComponent.displayName = 'CaseFormFields';

export const CaseFormFields = memo(CaseFormFieldsComponent);
Loading