Skip to content

Commit

Permalink
[ResponseOps][Cases] Allow users to create templates (#184104)
Browse files Browse the repository at this point in the history
## Summary
Merging into feature branch.

Implements create functionality
#181864

Add template from case settings page.

<img width="1728" alt="image"
src="https://github.com/elastic/kibana/assets/117571355/4a4565e5-3ca8-4a7c-a5c9-0ac15b72a0d0">

**How to test**
- Go to Cases > Settings
- Click on add templates
- Fill the form
- Save

Scenarios: 
- Create template with different custom fields
- Create template  with connector

Flaky test runner
[here](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6213)

### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
js-jankisalvi and kibanamachine committed Jun 7, 2024
1 parent 5986cc9 commit c511009
Show file tree
Hide file tree
Showing 67 changed files with 4,820 additions and 726 deletions.
12 changes: 12 additions & 0 deletions x-pack/plugins/cases/common/types/api/configure/v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,18 @@ describe('configure', () => {
});
});

it('does not throw when there is no description or tags', () => {
const newRequest = {
key: 'template_key_1',
name: 'Template 1',
caseFields: null,
};

expect(PathReporter.report(TemplateConfigurationRt.decode({ ...newRequest }))).toContain(
'No errors!'
);
});

it('limits name to 50 characters', () => {
const longName = 'x'.repeat(MAX_TEMPLATE_NAME_LENGTH + 1);

Expand Down
16 changes: 8 additions & 8 deletions x-pack/plugins/cases/common/types/api/configure/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,21 @@ export const TemplateConfigurationRt = rt.intersection([
* name of template
*/
name: limitedStringSchema({ fieldName: 'name', min: 1, max: MAX_TEMPLATE_NAME_LENGTH }),
/**
* description of templates
*/
description: limitedStringSchema({
fieldName: 'description',
min: 1,
max: MAX_TEMPLATE_DESCRIPTION_LENGTH,
}),
/**
* case fields
*/
caseFields: rt.union([rt.null, CaseBaseOptionalFieldsRequestRt]),
}),
rt.exact(
rt.partial({
/**
* description of templates
*/
description: limitedStringSchema({
fieldName: 'description',
min: 0,
max: MAX_TEMPLATE_DESCRIPTION_LENGTH,
}),
/**
* tags of templates
*/
Expand Down
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>;
2 changes: 0 additions & 2 deletions x-pack/plugins/cases/common/types/domain/configure/v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ describe('configure', () => {
const templateWithFewCaseFields = {
key: 'template_sample_2',
name: 'Sample template 2',
description: 'this is second sample template',
tags: [],
caseFields: {
title: 'Case with sample template 2',
Expand All @@ -92,7 +91,6 @@ describe('configure', () => {
const templateWithNoCaseFields = {
key: 'template_sample_3',
name: 'Sample template 3',
description: 'this is third sample template',
caseFields: null,
};

Expand Down
8 changes: 4 additions & 4 deletions x-pack/plugins/cases/common/types/domain/configure/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,17 @@ export const TemplateConfigurationRt = rt.intersection([
* name of template
*/
name: rt.string,
/**
* description of template
*/
description: rt.string,
/**
* case fields of template
*/
caseFields: rt.union([rt.null, CaseBaseOptionalFieldsRt]),
}),
rt.exact(
rt.partial({
/**
* description of template
*/
description: rt.string,
/**
* tags of template
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* 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 } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import type { AppMockRenderer } from '../../common/mock';
import { createAppMockRenderer } from '../../common/mock';
import { FormTestComponent } from '../../common/test_utils';
import { customFieldsConfigurationMock } from '../../containers/mock';
import { CustomFields } from './custom_fields';
import * as i18n from './translations';

describe('CustomFields', () => {
let appMockRender: AppMockRenderer;
const onSubmit = jest.fn();

const defaultProps = {
configurationCustomFields: customFieldsConfigurationMock,
isLoading: false,
setCustomFieldsOptional: false,
};

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

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

expect(await screen.findByText(i18n.ADDITIONAL_FIELDS)).toBeInTheDocument();
expect(await screen.findByTestId('caseCustomFields')).toBeInTheDocument();

for (const item of customFieldsConfigurationMock) {
expect(
await screen.findByTestId(`${item.key}-${item.type}-create-custom-field`)
).toBeInTheDocument();
}
});

it('should not show the custom fields if the configuration is empty', async () => {
appMockRender.render(
<FormTestComponent onSubmit={onSubmit}>
<CustomFields
isLoading={false}
setCustomFieldsOptional={false}
configurationCustomFields={[]}
/>
</FormTestComponent>
);

expect(screen.queryByText(i18n.ADDITIONAL_FIELDS)).not.toBeInTheDocument();
expect(screen.queryAllByTestId('create-custom-field', { exact: false }).length).toEqual(0);
});

it('should render as optional fields for text custom fields', async () => {
appMockRender.render(
<FormTestComponent onSubmit={onSubmit}>
<CustomFields
isLoading={false}
configurationCustomFields={customFieldsConfigurationMock}
setCustomFieldsOptional={true}
/>
</FormTestComponent>
);

expect(screen.getAllByTestId('form-optional-field-label')).toHaveLength(2);
});

it('should sort the custom fields correctly', async () => {
const reversedCustomFieldsConfiguration = [...customFieldsConfigurationMock].reverse();

appMockRender.render(
<FormTestComponent onSubmit={onSubmit}>
<CustomFields
isLoading={false}
setCustomFieldsOptional={false}
configurationCustomFields={reversedCustomFieldsConfiguration}
/>
</FormTestComponent>
);

const customFieldsWrapper = await screen.findByTestId('caseCustomFields');

const customFields = customFieldsWrapper.querySelectorAll('.euiFormRow');

expect(customFields).toHaveLength(4);

expect(customFields[0]).toHaveTextContent('My test label 1');
expect(customFields[1]).toHaveTextContent('My test label 2');
expect(customFields[2]).toHaveTextContent('My test label 3');
expect(customFields[3]).toHaveTextContent('My test label 4');
});

it('should update the custom fields', async () => {
appMockRender.render(
<FormTestComponent onSubmit={onSubmit}>
<CustomFields {...defaultProps} />
</FormTestComponent>
);

const textField = customFieldsConfigurationMock[2];
const toggleField = customFieldsConfigurationMock[3];

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

userEvent.click(await screen.findByText('Submit'));

await waitFor(() => {
// data, isValid
expect(onSubmit).toHaveBeenCalledWith(
{
customFields: {
[customFieldsConfigurationMock[0].key]: customFieldsConfigurationMock[0].defaultValue,
[customFieldsConfigurationMock[1].key]: customFieldsConfigurationMock[1].defaultValue,
[textField.key]: 'hello',
[toggleField.key]: true,
},
},
true
);
});
});
});
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;
setCustomFieldsOptional: boolean;
configurationCustomFields: CasesConfigurationUI['customFields'];
}

const CustomFieldsComponent: React.FC<Props> = ({
isLoading,
setCustomFieldsOptional,
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={setCustomFieldsOptional}
/>
);
}
);

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;
});
};
Loading

0 comments on commit c511009

Please sign in to comment.