diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
index db2dea0d7a172f..291896b4525e10 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
@@ -351,6 +351,7 @@ export const ConfigureCases: React.FC = React.memo(() => {
connectorId,
fields,
customFields: templateCustomFields,
+ syncAlerts = false,
...otherCaseFields
} = caseFields ?? {};
const transformedCustomFields = templateCustomFields
@@ -368,6 +369,7 @@ export const ConfigureCases: React.FC = React.memo(() => {
...otherCaseFields,
connector: transformedConnector,
customFields: transformedCustomFields,
+ settings: { syncAlerts },
},
};
const updatedTemplates = addOrReplaceField(templates, transformedData);
diff --git a/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx b/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx
index 29782f391168c9..d7d756249605b6 100644
--- a/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx
+++ b/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx
@@ -6,22 +6,26 @@
*/
import React, { memo } from 'react';
-import { getUseField, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
-import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components';
+import { UseField, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
+import { ToggleField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import * as i18n from './translations';
-const CommonUseField = getUseField({ component: Field });
-
interface Props {
isLoading: boolean;
path?: string;
}
const SyncAlertsToggleComponent: React.FC
= ({ isLoading, path }) => {
- const [{ syncAlerts }] = useFormData({ watch: ['syncAlerts'] });
+ const [formData] = useFormData();
+
+ const syncAlerts =
+ path !== '' ? Boolean(formData?.caseFields?.syncAlerts) : Boolean(formData?.syncAlerts);
+
return (
- = {
},
syncAlerts: {
helpText: i18n.SYNC_ALERTS_HELP,
- defaultValue: true,
labelAppend: OptionalFieldLabel,
},
},
From 41c025df8c51071366be3b519a263f36a2fef61c Mon Sep 17 00:00:00 2001
From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com>
Date: Tue, 21 May 2024 15:36:36 +0200
Subject: [PATCH 05/28] add custom fields component
---
.../case_form_fields/custom_fields.tsx | 76 ++++++++++
.../case_form_fields/index.test.tsx | 102 ++++++++++++++
.../components/case_form_fields/index.tsx | 76 +++++-----
.../case_form_fields/translations.ts | 14 ++
.../category/category_form_field.tsx | 2 +-
.../components/configure_cases/flyout.tsx | 11 +-
.../components/configure_cases/index.test.tsx | 24 +++-
.../components/configure_cases/index.tsx | 1 +
.../components/create/custom_fields.tsx | 6 +-
.../components/custom_fields/flyout.tsx | 2 -
.../public/components/templates/connector.tsx | 12 +-
.../public/components/templates/form.tsx | 6 +-
.../components/templates/form_fields.test.tsx | 130 ++++++++++++++++++
.../components/templates/form_fields.tsx | 24 +++-
.../components/templates/templates_list.tsx | 34 +----
.../components/templates/translations.ts | 4 -
.../use_persist_configuration.test.tsx | 59 +++++++-
.../apps/cases/group1/create_case_form.ts | 2 +-
18 files changed, 475 insertions(+), 110 deletions(-)
create mode 100644 x-pack/plugins/cases/public/components/case_form_fields/custom_fields.tsx
create mode 100644 x-pack/plugins/cases/public/components/case_form_fields/index.test.tsx
create mode 100644 x-pack/plugins/cases/public/components/case_form_fields/translations.ts
create mode 100644 x-pack/plugins/cases/public/components/templates/form_fields.test.tsx
diff --git a/x-pack/plugins/cases/public/components/case_form_fields/custom_fields.tsx b/x-pack/plugins/cases/public/components/case_form_fields/custom_fields.tsx
new file mode 100644
index 00000000000000..d66c125ada42b0
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/case_form_fields/custom_fields.tsx
@@ -0,0 +1,76 @@
+/*
+ * 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;
+ path?: string;
+ setAsOptional?: boolean;
+ configurationCustomFields: CasesConfigurationUI['customFields'];
+}
+
+const CustomFieldsComponent: React.FC = ({
+ isLoading,
+ path,
+ 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 (
+
+ );
+ }
+ );
+
+ if (!configurationCustomFields.length) {
+ return null;
+ }
+
+ return (
+
+
+ {i18n.ADDITIONAL_FIELDS}
+
+
+ {customFieldsComponents}
+
+ );
+};
+
+CustomFieldsComponent.displayName = 'CustomFields';
+
+export const CustomFields = React.memo(CustomFieldsComponent);
+
+const sortCustomFieldsByLabel = (configCustomFields: CasesConfigurationUI['customFields']) => {
+ return sortBy(configCustomFields, (configCustomField) => {
+ return configCustomField.label;
+ });
+};
diff --git a/x-pack/plugins/cases/public/components/case_form_fields/index.test.tsx b/x-pack/plugins/cases/public/components/case_form_fields/index.test.tsx
new file mode 100644
index 00000000000000..e67047c2168893
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/case_form_fields/index.test.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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 } from '@testing-library/react';
+import type { AppMockRenderer } from '../../common/mock';
+import { createAppMockRenderer } from '../../common/mock';
+import { CaseFormFields } from '.';
+import { FormTestComponent } from '../../common/test_utils';
+import { customFieldsConfigurationMock } from '../../containers/mock';
+import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
+
+describe('CaseFormFields', () => {
+ let appMock: AppMockRenderer;
+ const onSubmit = jest.fn();
+ const defaultProps = {
+ configurationCustomFields: [],
+ draftStorageKey: '',
+ };
+
+ beforeEach(() => {
+ appMock = createAppMockRenderer();
+ jest.clearAllMocks();
+ });
+
+ it('renders correctly', async () => {
+ appMock.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('case-form-fields')).toBeInTheDocument();
+ });
+
+ it('renders case fields correctly', async () => {
+ appMock.render(
+
+
+
+ );
+
+ 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();
+ });
+
+ it('does not render customFields when empty', () => {
+ appMock.render(
+
+
+
+ );
+
+ expect(screen.queryByTestId('caseCustomFields')).not.toBeInTheDocument();
+ });
+
+ it('renders customFields when not empty', async () => {
+ appMock.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('caseCustomFields')).toBeInTheDocument();
+ });
+
+ it('does not render assignees when no platinum license', () => {
+ appMock.render(
+
+
+
+ );
+
+ 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(
+
+
+
+ );
+
+ expect(await screen.findByTestId('createCaseAssigneesComboBox')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/case_form_fields/index.tsx b/x-pack/plugins/cases/public/components/case_form_fields/index.tsx
index ba2768597d6f86..2109c011a325ca 100644
--- a/x-pack/plugins/cases/public/components/case_form_fields/index.tsx
+++ b/x-pack/plugins/cases/public/components/case_form_fields/index.tsx
@@ -7,9 +7,7 @@
import React, { memo } from 'react';
import { useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
-import type { EuiThemeComputed } from '@elastic/eui';
-import { logicalCSS, useEuiTheme } from '@elastic/eui';
-import { css } from '@emotion/react';
+import { EuiFlexGroup } from '@elastic/eui';
import { Title } from '../create/title';
import { Tags } from '../create/tags';
import { Category } from '../create/category';
@@ -17,57 +15,51 @@ 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 '../create/custom_fields';
+import { CustomFields } from './custom_fields';
import { SyncAlertsToggle } from '../create/sync_alerts_toggle';
+import type { CasesConfigurationUI } from '../../containers/types';
-const containerCss = (euiTheme: EuiThemeComputed<{}>, big?: boolean) =>
- big
- ? css`
- ${logicalCSS('margin-top', euiTheme.size.xl)};
- `
- : css`
- ${logicalCSS('margin-top', euiTheme.size.base)};
- `;
+interface Props {
+ configurationCustomFields: CasesConfigurationUI['customFields'];
+ draftStorageKey: string;
+}
-const CaseFormFieldsComponent: React.FC = () => {
+const CaseFormFieldsComponent: React.FC = ({
+ configurationCustomFields,
+ draftStorageKey,
+}) => {
const { isSubmitting } = useFormContext();
const { caseAssignmentAuthorized, isSyncAlertsEnabled } = useCasesFeatures();
- const { euiTheme } = useEuiTheme();
return (
- <>
+
{caseAssignmentAuthorized ? (
-
+
) : null}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
{isSyncAlertsEnabled ? (
-
-
-
+
) : null}
-
-
-
-
- >
+
+
+
);
};
diff --git a/x-pack/plugins/cases/public/components/case_form_fields/translations.ts b/x-pack/plugins/cases/public/components/case_form_fields/translations.ts
new file mode 100644
index 00000000000000..b8359958025b32
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/case_form_fields/translations.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+export * from '../../common/translations';
+
+export const ADDITIONAL_FIELDS = i18n.translate('xpack.cases.additionalFields', {
+ defaultMessage: 'Additional fields',
+});
diff --git a/x-pack/plugins/cases/public/components/category/category_form_field.tsx b/x-pack/plugins/cases/public/components/category/category_form_field.tsx
index 2f44948bbf2241..f74946c4a0c68c 100644
--- a/x-pack/plugins/cases/public/components/category/category_form_field.tsx
+++ b/x-pack/plugins/cases/public/components/category/category_form_field.tsx
@@ -81,7 +81,7 @@ const CategoryFormFieldComponent: React.FC = ({
label={CATEGORY}
error={errorMessage}
isInvalid={isInvalid}
- data-test-subj="case-create-form-category"
+ data-test-subj="caseCategory"
fullWidth
>
void;
data: CustomFieldConfiguration | TemplateConfiguration | null;
type: 'customField' | 'template';
- connectors: ActionConnector[];
- configurationConnector: CasesConfigurationUI['connector'];
+ connectors?: ActionConnector[];
+ configurationConnector?: CasesConfigurationUI['connector'];
+ configurationCustomFields?: CasesConfigurationUI['customFields'];
}
const FlyoutComponent: React.FC = ({
@@ -51,6 +52,7 @@ const FlyoutComponent: React.FC = ({
type,
connectors,
configurationConnector,
+ configurationCustomFields,
}) => {
const dataTestSubj = `${type}Flyout`;
@@ -92,8 +94,9 @@ const FlyoutComponent: React.FC = ({
) : null}
diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
index 2a62ee87badb31..bf9545724bd581 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
@@ -425,6 +425,7 @@ describe('ConfigureCases', () => {
},
closureType: 'close-by-user',
customFields: [],
+ templates: [],
id: '',
version: '',
});
@@ -521,6 +522,7 @@ describe('ConfigureCases', () => {
},
closureType: 'close-by-pushing',
customFields: [],
+ templates: [],
id: '',
version: '',
});
@@ -706,6 +708,7 @@ describe('ConfigureCases', () => {
{ ...customFieldsConfigurationMock[2] },
{ ...customFieldsConfigurationMock[3] },
],
+ templates: [],
id: '',
version: '',
});
@@ -824,10 +827,29 @@ describe('ConfigureCases', () => {
});
});
- describe('rendering with license limitations', () => {
+ describe('templates', () => {
let appMockRender: AppMockRenderer;
let persistCaseConfigure: jest.Mock;
+ beforeEach(() => {
+ jest.clearAllMocks();
+ appMockRender = createAppMockRenderer();
+ persistCaseConfigure = jest.fn();
+ usePersistConfigurationMock.mockImplementation(() => ({
+ ...usePersistConfigurationMockResponse,
+ mutate: persistCaseConfigure,
+ }));
+ });
+
+ it('should render template section', async () => {
+ appMockRender.render();
+ expect(await screen.findByTestId('templates-form-group')).toBeInTheDocument();
+ });
+ });
+
+ describe('rendering with license limitations', () => {
+ let appMockRender: AppMockRenderer;
+ let persistCaseConfigure: jest.Mock;
beforeEach(() => {
// Default setup
jest.clearAllMocks();
diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
index 291896b4525e10..db43eb951f20e4 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
@@ -414,6 +414,7 @@ export const ConfigureCases: React.FC = React.memo(() => {
data={flyoutType === 'template' ? templateToEdit : customFieldToEdit}
connectors={connectors ?? []}
configurationConnector={connector}
+ configurationCustomFields={customFields}
onCloseFlyout={onCloseAddFieldFlyout}
onSaveField={onFlyoutSave}
/>
diff --git a/x-pack/plugins/cases/public/components/create/custom_fields.tsx b/x-pack/plugins/cases/public/components/create/custom_fields.tsx
index d87d3cbfdc10a4..28cebde65db27e 100644
--- a/x-pack/plugins/cases/public/components/create/custom_fields.tsx
+++ b/x-pack/plugins/cases/public/components/create/custom_fields.tsx
@@ -19,11 +19,9 @@ import { getConfigurationByOwner } from '../../containers/configure/utils';
interface Props {
isLoading: boolean;
- path?: string;
- setAsOptional?: boolean;
}
-const CustomFieldsComponent: React.FC = ({ isLoading, path, setAsOptional }) => {
+const CustomFieldsComponent: React.FC = ({ isLoading }) => {
const { owner } = useCasesContext();
const [{ selectedOwner }] = useFormData<{ selectedOwner: string }>({ watch: ['selectedOwner'] });
const { data: configurations, isLoading: isLoadingCaseConfiguration } =
@@ -56,8 +54,6 @@ const CustomFieldsComponent: React.FC = ({ isLoading, path, setAsOptional
isLoading={isLoading || isLoadingCaseConfiguration}
customFieldConfiguration={customField}
key={customField.key}
- path={path}
- setAsOptional={setAsOptional}
/>
);
}
diff --git a/x-pack/plugins/cases/public/components/custom_fields/flyout.tsx b/x-pack/plugins/cases/public/components/custom_fields/flyout.tsx
index 8d11589d851af1..e8fb5a937da9ad 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/flyout.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/flyout.tsx
@@ -20,10 +20,8 @@ import {
import type { CustomFieldFormState } from './form';
import { CustomFieldsForm } from './form';
import type { CustomFieldConfiguration } from '../../../common/types/domain';
-import { CustomFieldTypes } from '../../../common/types/domain';
import * as i18n from './translations';
-import { isEmpty } from 'lodash';
export interface CustomFieldFlyoutProps {
disabled: boolean;
diff --git a/x-pack/plugins/cases/public/components/templates/connector.tsx b/x-pack/plugins/cases/public/components/templates/connector.tsx
index 3368f71f068a62..982d6c35bd3d52 100644
--- a/x-pack/plugins/cases/public/components/templates/connector.tsx
+++ b/x-pack/plugins/cases/public/components/templates/connector.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { memo, useMemo } from 'react';
+import React, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import type { FieldConfig } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
@@ -24,7 +24,7 @@ interface Props {
connectors: ActionConnector[];
isLoading: boolean;
path?: string;
- configurationConnector: CasesConfigurationUI['connector'];
+ configurationConnector: CasesConfigurationUI['connector'] | null;
}
const ConnectorComponent: React.FC = ({
@@ -40,12 +40,6 @@ const ConnectorComponent: React.FC = ({
const { permissions } = useCasesContext();
const hasReadPermissions = permissions.connectors && actions.read;
- const defaultConnectorId = useMemo(() => {
- return connectors.some((c) => c.id === configurationConnector.id)
- ? configurationConnector.id
- : 'none';
- }, [configurationConnector.id, connectors]);
-
const connectorIdConfig = getConnectorsFormValidators({
config: schema.caseFields?.connectorId as FieldConfig,
connectors,
@@ -66,7 +60,7 @@ const ConnectorComponent: React.FC = ({
path={path ?? 'connectorId'}
config={connectorIdConfig}
component={ConnectorSelector}
- defaultValue={defaultConnectorId}
+ defaultValue={configurationConnector !== null ? configurationConnector.id : ''}
componentProps={{
connectors,
dataTestSubj: 'caseConnectors',
diff --git a/x-pack/plugins/cases/public/components/templates/form.tsx b/x-pack/plugins/cases/public/components/templates/form.tsx
index c1cef0ce017582..7173ba0c6afd64 100644
--- a/x-pack/plugins/cases/public/components/templates/form.tsx
+++ b/x-pack/plugins/cases/public/components/templates/form.tsx
@@ -25,7 +25,8 @@ interface Props {
onChange: (state: TemplateFormState) => void;
initialValue: TemplateFormProps | null;
connectors: ActionConnector[];
- configurationConnector: CasesConfigurationUI['connector'];
+ configurationConnector: CasesConfigurationUI['connector'] | null;
+ configurationCustomFields: CasesConfigurationUI['customFields'];
}
const FormComponent: React.FC = ({
@@ -33,6 +34,7 @@ const FormComponent: React.FC = ({
initialValue,
connectors,
configurationConnector,
+ configurationCustomFields,
}) => {
const keyDefaultValue = useMemo(() => uuidv4(), []);
@@ -60,9 +62,9 @@ const FormComponent: React.FC = ({
);
diff --git a/x-pack/plugins/cases/public/components/templates/form_fields.test.tsx b/x-pack/plugins/cases/public/components/templates/form_fields.test.tsx
new file mode 100644
index 00000000000000..d83c3cdc097206
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/templates/form_fields.test.tsx
@@ -0,0 +1,130 @@
+/*
+ * 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 } from '@testing-library/react';
+import type { AppMockRenderer } from '../../common/mock';
+import { createAppMockRenderer } from '../../common/mock';
+import { FormFields } from './form_fields';
+import { FormTestComponent } from '../../common/test_utils';
+import { connectorsMock, customFieldsConfigurationMock } from '../../containers/mock';
+import { ConnectorTypes } from '../../../common/types/domain';
+import { TEMPLATE_FIELDS, CASE_FIELDS, CONNECTOR_FIELDS } from './translations';
+
+describe('form fields', () => {
+ let appMockRenderer: AppMockRenderer;
+ const onSubmit = jest.fn();
+ const defaultProps = {
+ connectors: connectorsMock,
+ configurationConnector: {
+ id: 'none',
+ type: ConnectorTypes.none,
+ fields: null,
+ name: 'My Connector',
+ },
+ configurationCustomFields: [],
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ appMockRenderer = createAppMockRenderer();
+ });
+
+ it('renders correctly', async () => {
+ appMockRenderer.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('template-creation-form-steps')).toBeInTheDocument();
+ });
+
+ it('renders all steps', async () => {
+ appMockRenderer.render(
+
+
+
+ );
+
+ expect(await screen.findByText(TEMPLATE_FIELDS)).toBeInTheDocument();
+ expect(await screen.findByText(CASE_FIELDS)).toBeInTheDocument();
+ expect(await screen.findByText(CONNECTOR_FIELDS)).toBeInTheDocument();
+ });
+
+ it('renders template fields correctly', async () => {
+ appMockRenderer.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('template-name-input')).toBeInTheDocument();
+ expect(await screen.findByTestId('template-description-input')).toBeInTheDocument();
+ });
+
+ it('renders case fields', async () => {
+ appMockRenderer.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('case-form-fields')).toBeInTheDocument();
+ 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();
+ });
+
+ it('renders custom fields correctly', async () => {
+ const newProps = {
+ ...defaultProps,
+ configurationCustomFields: customFieldsConfigurationMock,
+ };
+ appMockRenderer.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('caseCustomFields')).toBeInTheDocument();
+ });
+
+ it('renders default connector correctly', async () => {
+ appMockRenderer.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('caseConnectors')).toBeInTheDocument();
+ });
+
+ it('renders connector and its fields correctly', async () => {
+ const newProps = {
+ ...defaultProps,
+ configurationConnector: {
+ id: 'servicenow-1',
+ name: 'my_service_now_connector',
+ type: ConnectorTypes.serviceNowITSM,
+ fields: null,
+ },
+ };
+
+ appMockRenderer.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('caseConnectors')).toBeInTheDocument();
+ expect(await screen.findByTestId('connector-fields')).toBeInTheDocument();
+ expect(await screen.findByTestId('connector-fields-sn-itsm')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/templates/form_fields.tsx b/x-pack/plugins/cases/public/components/templates/form_fields.tsx
index 46ece1412abbc9..79695fde79ae7d 100644
--- a/x-pack/plugins/cases/public/components/templates/form_fields.tsx
+++ b/x-pack/plugins/cases/public/components/templates/form_fields.tsx
@@ -14,20 +14,29 @@ import * as i18n from './translations';
import { Connector } from './connector';
import type { ActionConnector } from '../../containers/configure/types';
import type { CasesConfigurationUI } from '../../containers/types';
+import { getMarkdownEditorStorageKey } from '../markdown_editor/utils';
+import { useCasesContext } from '../cases_context/use_cases_context';
interface FormFieldsProps {
isSubmitting?: boolean;
- isEditMode?: boolean;
connectors: ActionConnector[];
- configurationConnector: CasesConfigurationUI['connector'];
+ configurationConnector: CasesConfigurationUI['connector'] | null;
+ configurationCustomFields: CasesConfigurationUI['customFields'];
}
const FormFieldsComponent: React.FC = ({
isSubmitting = false,
- isEditMode,
connectors,
configurationConnector,
+ configurationCustomFields,
}) => {
+ const { owner } = useCasesContext();
+ const draftStorageKey = getMarkdownEditorStorageKey({
+ appId: owner[0],
+ caseId: 'createCaseTemplate',
+ commentId: 'description',
+ });
+
const firstStep = useMemo(
() => ({
title: i18n.TEMPLATE_FIELDS,
@@ -66,9 +75,14 @@ const FormFieldsComponent: React.FC = ({
const secondStep = useMemo(
() => ({
title: i18n.CASE_FIELDS,
- children: ,
+ children: (
+
+ ),
}),
- []
+ [configurationCustomFields, draftStorageKey]
);
const thirdStep = useMemo(
diff --git a/x-pack/plugins/cases/public/components/templates/templates_list.tsx b/x-pack/plugins/cases/public/components/templates/templates_list.tsx
index dbadac1f4aed9f..c18fbfc1795736 100644
--- a/x-pack/plugins/cases/public/components/templates/templates_list.tsx
+++ b/x-pack/plugins/cases/public/components/templates/templates_list.tsx
@@ -5,19 +5,16 @@
* 2.0.
*/
-import React, { useState } from 'react';
+import React from 'react';
import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
import type { CasesConfigurationUITemplate } from '../../../common/ui';
export interface Props {
templates: CasesConfigurationUITemplate[];
- // onDeleteCustomField: (key: string) => void;
- // onEditCustomField: (key: string) => void;
}
const TemplatesListComponent: React.FC = (props) => {
const { templates } = props;
- const [selectedItem, setSelectedItem] = useState(null);
return templates.length ? (
<>
@@ -41,41 +38,12 @@ const TemplatesListComponent: React.FC = (props) => {
- {/*
-
-
- onEditCustomField(customField.key)}
- />
-
-
- setSelectedItem(customField)}
- />
-
-
- */}
))}
- {/* {showModal && selectedItem ? (
-
- ) : null} */}
>
) : null;
diff --git a/x-pack/plugins/cases/public/components/templates/translations.ts b/x-pack/plugins/cases/public/components/templates/translations.ts
index 675f8f9fe72f08..e127f248fd1ba4 100644
--- a/x-pack/plugins/cases/public/components/templates/translations.ts
+++ b/x-pack/plugins/cases/public/components/templates/translations.ts
@@ -54,7 +54,3 @@ export const CASE_FIELDS = i18n.translate('xpack.cases.templates.caseFields', {
export const CONNECTOR_FIELDS = i18n.translate('xpack.cases.templates.connectorFields', {
defaultMessage: 'External Connector Fields',
});
-
-// export const SAVE = i18n.translate('xpack.cases.templates.saveTemplate', {
-// defaultMessage: 'Save',
-// });
diff --git a/x-pack/plugins/cases/public/containers/configure/use_persist_configuration.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_persist_configuration.test.tsx
index 47816321d684ae..c1839bfbac6819 100644
--- a/x-pack/plugins/cases/public/containers/configure/use_persist_configuration.test.tsx
+++ b/x-pack/plugins/cases/public/containers/configure/use_persist_configuration.test.tsx
@@ -14,13 +14,14 @@ import type { AppMockRenderer } from '../../common/mock';
import { createAppMockRenderer } from '../../common/mock';
import { ConnectorTypes } from '../../../common';
import { casesQueriesKeys } from '../constants';
+import { customFieldsConfigurationMock, templatesConfigurationMock } from '../mock';
jest.mock('./api');
jest.mock('../../common/lib/kibana');
const useToastMock = useToasts as jest.Mock;
-describe('useCreateAttachments', () => {
+describe('usePersistConfiguration', () => {
const addError = jest.fn();
const addSuccess = jest.fn();
@@ -98,6 +99,34 @@ describe('useCreateAttachments', () => {
});
});
+ it('calls postCaseConfigure with correct data', async () => {
+ const spyPost = jest.spyOn(api, 'postCaseConfigure');
+
+ const { waitForNextUpdate, result } = renderHook(() => usePersistConfiguration(), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ const newRequest = {
+ ...request,
+ customFields: customFieldsConfigurationMock,
+ templates: templatesConfigurationMock,
+ };
+
+ act(() => {
+ result.current.mutate({ ...newRequest, id: 'test-id' });
+ });
+
+ await waitForNextUpdate();
+
+ expect(spyPost).toHaveBeenCalledWith({
+ closure_type: 'close-by-user',
+ connector: { fields: null, id: 'none', name: 'none', type: '.none' },
+ customFields: customFieldsConfigurationMock,
+ templates: templatesConfigurationMock,
+ owner: 'securitySolution',
+ });
+ });
+
it('calls patchCaseConfigure when the id and the version are not empty', async () => {
const spyPost = jest.spyOn(api, 'postCaseConfigure');
const spyPatch = jest.spyOn(api, 'patchCaseConfigure');
@@ -122,6 +151,34 @@ describe('useCreateAttachments', () => {
});
});
+ it('calls patchCaseConfigure with correct data', async () => {
+ const spyPatch = jest.spyOn(api, 'patchCaseConfigure');
+
+ const { waitForNextUpdate, result } = renderHook(() => usePersistConfiguration(), {
+ wrapper: appMockRender.AppWrapper,
+ });
+
+ const newRequest = {
+ ...request,
+ customFields: customFieldsConfigurationMock,
+ templates: templatesConfigurationMock,
+ };
+
+ act(() => {
+ result.current.mutate({ ...newRequest, id: 'test-id', version: 'test-version' });
+ });
+
+ await waitForNextUpdate();
+
+ expect(spyPatch).toHaveBeenCalledWith('test-id', {
+ closure_type: 'close-by-user',
+ connector: { fields: null, id: 'none', name: 'none', type: '.none' },
+ customFields: customFieldsConfigurationMock,
+ templates: templatesConfigurationMock,
+ version: 'test-version',
+ });
+ });
+
it('invalidates the queries correctly', async () => {
const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries');
const { waitForNextUpdate, result } = renderHook(() => usePersistConfiguration(), {
diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts
index fcb1e23d6f9bb6..cbd2537179040c 100644
--- a/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts
@@ -93,7 +93,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => {
'The length of the tag is too long. The maximum length is 256 characters.'
);
- const category = await testSubjects.find('case-create-form-category');
+ const category = await testSubjects.find('caseCategory');
expect(await category.getVisibleText()).contain(
'The length of the category is too long. The maximum length is 50 characters.'
);
From 5de5bd24a9d0c2839b59db1cc771fe34cfab636f Mon Sep 17 00:00:00 2001
From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com>
Date: Thu, 23 May 2024 11:29:27 +0200
Subject: [PATCH 06/28] add tags field, update default value of connector
---
.../components/configure_cases/flyout.tsx | 6 +-
.../components/configure_cases/index.tsx | 2 +-
.../cases/public/components/create/tags.tsx | 7 +-
.../public/components/templates/connector.tsx | 10 +--
.../public/components/templates/form.tsx | 6 +-
.../components/templates/form_fields.tsx | 10 ++-
.../public/components/templates/schema.tsx | 79 ++++++++++++-------
.../components/templates/translations.ts | 5 ++
.../public/components/templates/utils.ts | 56 +++++++++++++
9 files changed, 132 insertions(+), 49 deletions(-)
diff --git a/x-pack/plugins/cases/public/components/configure_cases/flyout.tsx b/x-pack/plugins/cases/public/components/configure_cases/flyout.tsx
index de050e08b20636..bece367e77ab4e 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/flyout.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/flyout.tsx
@@ -39,7 +39,7 @@ export interface FlyoutProps {
data: CustomFieldConfiguration | TemplateConfiguration | null;
type: 'customField' | 'template';
connectors?: ActionConnector[];
- configurationConnector?: CasesConfigurationUI['connector'];
+ configurationConnectorId?: string;
configurationCustomFields?: CasesConfigurationUI['customFields'];
}
@@ -51,7 +51,7 @@ const FlyoutComponent: React.FC = ({
data: initialValue,
type,
connectors,
- configurationConnector,
+ configurationConnectorId,
configurationCustomFields,
}) => {
const dataTestSubj = `${type}Flyout`;
@@ -95,7 +95,7 @@ const FlyoutComponent: React.FC = ({
onChange={setFormState}
initialValue={initialValue as TemplateFormProps}
connectors={connectors ?? []}
- configurationConnector={configurationConnector ?? null}
+ configurationConnectorId={configurationConnectorId ?? ''}
configurationCustomFields={configurationCustomFields ?? []}
/>
) : null}
diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
index db43eb951f20e4..4496adb18fb77a 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
@@ -413,7 +413,7 @@ export const ConfigureCases: React.FC = React.memo(() => {
type={flyoutType}
data={flyoutType === 'template' ? templateToEdit : customFieldToEdit}
connectors={connectors ?? []}
- configurationConnector={connector}
+ configurationConnectorId={connector.id}
configurationCustomFields={customFields}
onCloseFlyout={onCloseAddFieldFlyout}
onSaveField={onFlyoutSave}
diff --git a/x-pack/plugins/cases/public/components/create/tags.tsx b/x-pack/plugins/cases/public/components/create/tags.tsx
index 9744e198fbac3f..211a7d3219b7b1 100644
--- a/x-pack/plugins/cases/public/components/create/tags.tsx
+++ b/x-pack/plugins/cases/public/components/create/tags.tsx
@@ -14,9 +14,10 @@ import * as i18n from './translations';
interface Props {
isLoading: boolean;
path?: string;
+ dataTestSubject?: string;
}
-const TagsComponent: React.FC = ({ isLoading, path }) => {
+const TagsComponent: React.FC = ({ isLoading, path, dataTestSubject }) => {
const { data: tagOptions = [], isLoading: isLoadingTags } = useGetTags();
const options = useMemo(
() =>
@@ -32,8 +33,8 @@ const TagsComponent: React.FC = ({ isLoading, path }) => {
component={ComboBoxField}
defaultValue={[]}
componentProps={{
- idAria: 'caseTags',
- 'data-test-subj': 'caseTags',
+ idAria: dataTestSubject ?? 'caseTags',
+ 'data-test-subj': dataTestSubject ?? 'caseTags',
euiFieldProps: {
fullWidth: true,
placeholder: '',
diff --git a/x-pack/plugins/cases/public/components/templates/connector.tsx b/x-pack/plugins/cases/public/components/templates/connector.tsx
index 982d6c35bd3d52..7a7972a34d94a5 100644
--- a/x-pack/plugins/cases/public/components/templates/connector.tsx
+++ b/x-pack/plugins/cases/public/components/templates/connector.tsx
@@ -17,21 +17,20 @@ import { getConnectorById, getConnectorsFormValidators } from '../utils';
import { useApplicationCapabilities } from '../../common/lib/kibana';
import * as i18n from '../../common/translations';
import { useCasesContext } from '../cases_context/use_cases_context';
-import type { CasesConfigurationUI } from '../../containers/types';
import { schema } from './schema';
interface Props {
connectors: ActionConnector[];
isLoading: boolean;
path?: string;
- configurationConnector: CasesConfigurationUI['connector'] | null;
+ configurationConnectorId: string;
}
const ConnectorComponent: React.FC = ({
connectors,
isLoading,
path,
- configurationConnector,
+ configurationConnectorId,
}) => {
const [{ caseFields }] = useFormData({ watch: ['caseFields.connectorId'] });
const connector = getConnectorById(caseFields?.connectorId, connectors) ?? null;
@@ -39,9 +38,10 @@ const ConnectorComponent: React.FC = ({
const { actions } = useApplicationCapabilities();
const { permissions } = useCasesContext();
const hasReadPermissions = permissions.connectors && actions.read;
+ const connectorId = schema.caseFields?.connectorId ?? '';
const connectorIdConfig = getConnectorsFormValidators({
- config: schema.caseFields?.connectorId as FieldConfig,
+ config: connectorId as FieldConfig,
connectors,
});
@@ -60,7 +60,7 @@ const ConnectorComponent: React.FC = ({
path={path ?? 'connectorId'}
config={connectorIdConfig}
component={ConnectorSelector}
- defaultValue={configurationConnector !== null ? configurationConnector.id : ''}
+ defaultValue={configurationConnectorId}
componentProps={{
connectors,
dataTestSubj: 'caseConnectors',
diff --git a/x-pack/plugins/cases/public/components/templates/form.tsx b/x-pack/plugins/cases/public/components/templates/form.tsx
index 7173ba0c6afd64..9bca4ae3b82c05 100644
--- a/x-pack/plugins/cases/public/components/templates/form.tsx
+++ b/x-pack/plugins/cases/public/components/templates/form.tsx
@@ -25,7 +25,7 @@ interface Props {
onChange: (state: TemplateFormState) => void;
initialValue: TemplateFormProps | null;
connectors: ActionConnector[];
- configurationConnector: CasesConfigurationUI['connector'] | null;
+ configurationConnectorId: string;
configurationCustomFields: CasesConfigurationUI['customFields'];
}
@@ -33,7 +33,7 @@ const FormComponent: React.FC = ({
onChange,
initialValue,
connectors,
- configurationConnector,
+ configurationConnectorId,
configurationCustomFields,
}) => {
const keyDefaultValue = useMemo(() => uuidv4(), []);
@@ -63,7 +63,7 @@ const FormComponent: React.FC = ({
diff --git a/x-pack/plugins/cases/public/components/templates/form_fields.tsx b/x-pack/plugins/cases/public/components/templates/form_fields.tsx
index 79695fde79ae7d..29d42ac1da9fb2 100644
--- a/x-pack/plugins/cases/public/components/templates/form_fields.tsx
+++ b/x-pack/plugins/cases/public/components/templates/form_fields.tsx
@@ -16,18 +16,19 @@ import type { ActionConnector } from '../../containers/configure/types';
import type { CasesConfigurationUI } from '../../containers/types';
import { getMarkdownEditorStorageKey } from '../markdown_editor/utils';
import { useCasesContext } from '../cases_context/use_cases_context';
+import { Tags } from '../create/tags';
interface FormFieldsProps {
isSubmitting?: boolean;
connectors: ActionConnector[];
- configurationConnector: CasesConfigurationUI['connector'] | null;
+ configurationConnectorId: string;
configurationCustomFields: CasesConfigurationUI['customFields'];
}
const FormFieldsComponent: React.FC = ({
isSubmitting = false,
connectors,
- configurationConnector,
+ configurationConnectorId,
configurationCustomFields,
}) => {
const { owner } = useCasesContext();
@@ -54,6 +55,7 @@ const FormFieldsComponent: React.FC = ({
},
}}
/>
+
= ({
),
}),
- [connectors, configurationConnector, isSubmitting]
+ [connectors, configurationConnectorId, isSubmitting]
);
const allSteps = useMemo(
diff --git a/x-pack/plugins/cases/public/components/templates/schema.tsx b/x-pack/plugins/cases/public/components/templates/schema.tsx
index 3c6069dd6e4af4..45eebf48ae8578 100644
--- a/x-pack/plugins/cases/public/components/templates/schema.tsx
+++ b/x-pack/plugins/cases/public/components/templates/schema.tsx
@@ -12,17 +12,17 @@ import {
MAX_DESCRIPTION_LENGTH,
MAX_LENGTH_PER_TAG,
MAX_TAGS_PER_CASE,
+ MAX_TAGS_PER_TEMPLATE,
+ MAX_TEMPLATE_TAG_LENGTH,
MAX_TITLE_LENGTH,
} from '../../../common/constants';
import { OptionalFieldLabel } from '../create/optional_field_label';
import { SEVERITY_TITLE } from '../severity/translations';
import * as i18n from './translations';
import type { TemplateFormProps } from './types';
+import { validateEmptyTags, validateMaxLength, validateMaxTagsLength } from './utils';
const { emptyField, maxLengthField } = fieldValidators;
-const isInvalidTag = (value: string) => value.trim() === '';
-
-const isTagCharactersInLimit = (value: string) => value.trim().length > MAX_LENGTH_PER_TAG;
export const schema: FormSchema