-
+
diff --git a/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx b/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx
index 27df975ac5864..3193777389c7f 100644
--- a/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx
+++ b/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useState, memo } from 'react';
+import React, { useState, memo, useRef } from 'react';
import type { EuiComboBoxOptionOption } from '@elastic/eui';
import { EuiComboBox, EuiFormRow } from '@elastic/eui';
@@ -17,17 +17,20 @@ import { useKibana } from '../../../common/lib/kibana';
import type { ActionConnector } from '../../../../common/types/domain';
import { useGetIssues } from './use_get_issues';
import * as i18n from './translations';
+import { useGetIssue } from './use_get_issue';
interface Props {
actionConnector?: ActionConnector;
+ currentParent: string | null;
}
-const SearchIssuesComponent: React.FC
= ({ actionConnector }) => {
+const SearchIssuesComponent: React.FC = ({ actionConnector, currentParent }) => {
const [query, setQuery] = useState(null);
const [selectedOptions, setSelectedOptions] = useState>>(
[]
);
const { http } = useKibana().services;
+ const isFirstRender = useRef(true);
const { isFetching: isLoadingIssues, data: issuesData } = useGetIssues({
http,
@@ -35,10 +38,27 @@ const SearchIssuesComponent: React.FC = ({ actionConnector }) => {
query,
});
+ const { isFetching: isLoadingIssue, data: issueData } = useGetIssue({
+ http,
+ actionConnector,
+ id: currentParent ?? '',
+ });
+
const issues = issuesData?.data ?? [];
const options = issues.map((issue) => ({ label: issue.title, value: issue.key }));
+ const issue = issueData?.data ?? null;
+
+ if (
+ isFirstRender.current &&
+ !isLoadingIssue &&
+ issue &&
+ !selectedOptions.find((option) => option.value === issue.key)
+ ) {
+ setSelectedOptions([{ label: issue.title, value: issue.key }]);
+ }
+
return (
{(field) => {
@@ -50,7 +70,8 @@ const SearchIssuesComponent: React.FC = ({ actionConnector }) => {
const onChangeComboBox = (changedOptions: Array>) => {
setSelectedOptions(changedOptions);
- field.setValue(changedOptions[0].value ?? '');
+ field.setValue(changedOptions.length ? changedOptions[0].value : '');
+ isFirstRender.current = false;
};
return (
diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue.test.tsx
new file mode 100644
index 0000000000000..876738025e6a8
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue.test.tsx
@@ -0,0 +1,131 @@
+/*
+ * 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 { renderHook } from '@testing-library/react-hooks';
+
+import { useKibana, useToasts } from '../../../common/lib/kibana';
+import { connector as actionConnector } from '../mock';
+import { useGetIssue } from './use_get_issue';
+import * as api from './api';
+import type { AppMockRenderer } from '../../../common/mock';
+import { createAppMockRenderer } from '../../../common/mock';
+
+jest.mock('../../../common/lib/kibana');
+jest.mock('./api');
+
+const useKibanaMock = useKibana as jest.Mocked;
+
+describe('useGetIssue', () => {
+ const { http } = useKibanaMock().services;
+ let appMockRender: AppMockRenderer;
+
+ beforeEach(() => {
+ appMockRender = createAppMockRenderer();
+ jest.clearAllMocks();
+ });
+
+ it('calls the api when invoked with the correct parameters', async () => {
+ const spy = jest.spyOn(api, 'getIssue');
+ const { result, waitFor } = renderHook(
+ () =>
+ useGetIssue({
+ http,
+ actionConnector,
+ id: 'RJ-107',
+ }),
+ { wrapper: appMockRender.AppWrapper }
+ );
+
+ await waitFor(() => result.current.isSuccess);
+
+ expect(spy).toHaveBeenCalledWith({
+ http,
+ signal: expect.anything(),
+ connectorId: actionConnector.id,
+ id: 'RJ-107',
+ });
+ });
+
+ it('does not call the api when the connector is missing', async () => {
+ const spy = jest.spyOn(api, 'getIssue');
+ renderHook(
+ () =>
+ useGetIssue({
+ http,
+ id: 'RJ-107',
+ }),
+ { wrapper: appMockRender.AppWrapper }
+ );
+
+ expect(spy).not.toHaveBeenCalledWith();
+ });
+
+ it('does not call the api when the id is missing', async () => {
+ const spy = jest.spyOn(api, 'getIssue');
+ renderHook(
+ () =>
+ useGetIssue({
+ http,
+ actionConnector,
+ id: '',
+ }),
+ { wrapper: appMockRender.AppWrapper }
+ );
+
+ expect(spy).not.toHaveBeenCalledWith();
+ });
+
+ it('calls addError when the getIssue api throws an error', async () => {
+ const spyOnGetCases = jest.spyOn(api, 'getIssue');
+ spyOnGetCases.mockImplementation(() => {
+ throw new Error('Something went wrong');
+ });
+
+ const addError = jest.fn();
+ (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError });
+
+ const { result, waitFor } = renderHook(
+ () =>
+ useGetIssue({
+ http,
+ actionConnector,
+ id: 'RJ-107',
+ }),
+ { wrapper: appMockRender.AppWrapper }
+ );
+
+ await waitFor(() => result.current.isError);
+
+ expect(addError).toHaveBeenCalled();
+ });
+
+ it('calls addError when the getIssue api returns successfully but contains an error', async () => {
+ const spyOnGetCases = jest.spyOn(api, 'getIssue');
+ spyOnGetCases.mockResolvedValue({
+ status: 'error',
+ message: 'Error message',
+ actionId: 'test',
+ });
+
+ const addError = jest.fn();
+ (useToasts as jest.Mock).mockReturnValue({ addSuccess: jest.fn(), addError });
+
+ const { result, waitFor } = renderHook(
+ () =>
+ useGetIssue({
+ http,
+ actionConnector,
+ id: 'RJ-107',
+ }),
+ { wrapper: appMockRender.AppWrapper }
+ );
+
+ await waitFor(() => result.current.isSuccess);
+
+ expect(addError).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue.tsx
new file mode 100644
index 0000000000000..ed3bfcf61f2f8
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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 type { HttpSetup } from '@kbn/core/public';
+import type { ActionTypeExecutorResult } from '@kbn/actions-plugin/common';
+import { useQuery } from '@tanstack/react-query';
+import { isEmpty } from 'lodash';
+import type { ActionConnector } from '../../../../common/types/domain';
+import { getIssue } from './api';
+import type { Issue } from './types';
+import * as i18n from './translations';
+import { useCasesToast } from '../../../common/use_cases_toast';
+import type { ServerError } from '../../../types';
+import { connectorsQueriesKeys } from '../constants';
+
+interface Props {
+ http: HttpSetup;
+ id: string;
+ actionConnector?: ActionConnector;
+}
+
+export const useGetIssue = ({ http, actionConnector, id }: Props) => {
+ const { showErrorToast } = useCasesToast();
+ return useQuery, ServerError>(
+ connectorsQueriesKeys.jiraGetIssue(actionConnector?.id ?? '', id),
+ ({ signal }) => {
+ return getIssue({
+ http,
+ signal,
+ connectorId: actionConnector?.id ?? '',
+ id,
+ });
+ },
+ {
+ enabled: Boolean(actionConnector && !isEmpty(id)),
+ staleTime: 60 * 1000, // one minute
+ onSuccess: (res) => {
+ if (res.status && res.status === 'error') {
+ showErrorToast(new Error(i18n.GET_ISSUE_API_ERROR(id)), {
+ title: i18n.GET_ISSUE_API_ERROR(id),
+ toastMessage: `${res.serviceMessage ?? res.message}`,
+ });
+ }
+ },
+ onError: (error: ServerError) => {
+ showErrorToast(error, { title: i18n.GET_ISSUE_API_ERROR(id) });
+ },
+ }
+ );
+};
+
+export type UseGetIssueTypes = ReturnType;
diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx
index e8260a69a3301..ee7538543ec41 100644
--- a/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx
+++ b/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx
@@ -77,12 +77,15 @@ const ResilientFieldsComponent: React.FunctionComponent =
field.setValue(changedOptions.map((option) => option.value as string));
};
- const selectedOptions = (field.value ?? []).map((incidentType) => ({
- value: incidentType,
- label:
- (allIncidentTypes ?? []).find((type) => incidentType === type.id.toString())?.name ??
- '',
- }));
+ const selectedOptions =
+ field.value && allIncidentTypes?.length
+ ? field.value.map((incidentType) => ({
+ value: incidentType,
+ label:
+ allIncidentTypes.find((type) => incidentType === type.id.toString())?.name ??
+ '',
+ }))
+ : [];
return (
{
let appMockRender: AppMockRenderer;
const MockHookWrapperComponent: FC> = ({ children }) => {
- const { form } = useForm();
+ const { form } = useForm();
globalForm = form;
return ;
diff --git a/x-pack/plugins/cases/public/components/create/assignees.tsx b/x-pack/plugins/cases/public/components/create/assignees.tsx
index 1e8464dc1a2ed..7ac543e3a6fda 100644
--- a/x-pack/plugins/cases/public/components/create/assignees.tsx
+++ b/x-pack/plugins/cases/public/components/create/assignees.tsx
@@ -40,14 +40,14 @@ interface Props {
isLoading: boolean;
}
+type UserProfileComboBoxOption = EuiComboBoxOptionOption & UserProfileWithAvatar;
+
interface FieldProps {
- field: FieldHook;
- options: EuiComboBoxOptionOption[];
+ field: FieldHook;
+ options: UserProfileComboBoxOption[];
isLoading: boolean;
isDisabled: boolean;
currentUserProfile?: UserProfile;
- selectedOptions: EuiComboBoxOptionOption[];
- setSelectedOptions: React.Dispatch>;
onSearchComboChange: (value: string) => void;
}
@@ -73,28 +73,32 @@ const userProfileToComboBoxOption = (userProfile: UserProfileWithAvatar) => ({
data: userProfile.data,
});
-const comboBoxOptionToAssignee = (option: EuiComboBoxOptionOption) => ({ uid: option.value });
+const comboBoxOptionToAssignee = (option: EuiComboBoxOptionOption) => ({
+ uid: option.value ?? '',
+});
const AssigneesFieldComponent: React.FC = React.memo(
- ({
- field,
- isLoading,
- isDisabled,
- options,
- currentUserProfile,
- selectedOptions,
- setSelectedOptions,
- onSearchComboChange,
- }) => {
- const { setValue } = field;
+ ({ field, isLoading, isDisabled, options, currentUserProfile, onSearchComboChange }) => {
+ const { setValue, value: selectedAssignees } = field;
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
+ const selectedOptions: UserProfileComboBoxOption[] = selectedAssignees
+ .map(({ uid }) => {
+ const selectedUserProfile = options.find((userProfile) => userProfile.key === uid);
+
+ if (selectedUserProfile) {
+ return selectedUserProfile;
+ }
+
+ return null;
+ })
+ .filter((value): value is UserProfileComboBoxOption => value != null);
+
const onComboChange = useCallback(
- (currentOptions: EuiComboBoxOptionOption[]) => {
- setSelectedOptions(currentOptions);
+ (currentOptions: Array>) => {
setValue(currentOptions.map((option) => comboBoxOptionToAssignee(option)));
},
- [setSelectedOptions, setValue]
+ [setValue]
);
const onSelfAssign = useCallback(() => {
@@ -102,62 +106,51 @@ const AssigneesFieldComponent: React.FC = React.memo(
return;
}
- setSelectedOptions((prev) => [
- ...(prev ?? []),
- userProfileToComboBoxOption(currentUserProfile),
- ]);
-
- setValue([
- ...(selectedOptions?.map((option) => comboBoxOptionToAssignee(option)) ?? []),
- { uid: currentUserProfile.uid },
- ]);
- }, [currentUserProfile, selectedOptions, setSelectedOptions, setValue]);
+ setValue([...selectedAssignees, { uid: currentUserProfile.uid }]);
+ }, [currentUserProfile, selectedAssignees, setValue]);
- const renderOption = useCallback(
- (option: EuiComboBoxOptionOption, searchValue: string, contentClassName: string) => {
- const { user, data } = option as EuiComboBoxOptionOption & UserProfileWithAvatar;
+ const renderOption = useCallback((option, searchValue: string, contentClassName: string) => {
+ const { user, data } = option as UserProfileComboBoxOption;
- const displayName = getUserDisplayName(user);
+ const displayName = getUserDisplayName(user);
- return (
+ return (
+
+
+
+
-
-
+
+
+ {displayName}
+
-
-
-
- {displayName}
-
+ {user.email && user.email !== displayName ? (
+
+
+
+ {user.email}
+
+
- {user.email && user.email !== displayName ? (
-
-
-
- {user.email}
-
-
-
- ) : null}
-
+ ) : null}
- );
- },
- []
- );
+
+ );
+ }, []);
const isCurrentUserSelected = Boolean(
- selectedOptions?.find((option) => option.value === currentUserProfile?.uid)
+ selectedAssignees?.find((assignee) => assignee.uid === currentUserProfile?.uid)
);
return (
@@ -204,7 +197,6 @@ const AssigneesComponent: React.FC = ({ isLoading: isLoadingForm }) => {
const { owner: owners } = useCasesContext();
const availableOwners = useAvailableCasesOwners(getAllPermissionsExceptFrom('delete'));
const [searchTerm, setSearchTerm] = useState('');
- const [selectedOptions, setSelectedOptions] = useState();
const { isUserTyping, onContentChange, onDebounce } = useIsUserTyping();
const hasOwners = owners.length > 0;
@@ -251,8 +243,6 @@ const AssigneesComponent: React.FC = ({ isLoading: isLoadingForm }) => {
componentProps={{
isLoading,
isDisabled,
- selectedOptions,
- setSelectedOptions,
options,
onSearchComboChange,
currentUserProfile,
diff --git a/x-pack/plugins/cases/public/components/create/tags.tsx b/x-pack/plugins/cases/public/components/create/tags.tsx
index e08f9ace8dde0..77c4cf7a7ba00 100644
--- a/x-pack/plugins/cases/public/components/create/tags.tsx
+++ b/x-pack/plugins/cases/public/components/create/tags.tsx
@@ -29,7 +29,6 @@ const TagsComponent: React.FC = ({ isLoading }) => {
{
)
);
- expect(await screen.findByTestId('confirm-delete-custom-field-modal')).toBeInTheDocument();
+ expect(await screen.findByTestId('confirm-delete-modal')).toBeInTheDocument();
});
it('calls onDeleteCustomField when confirm', async () => {
@@ -113,12 +113,12 @@ describe('CustomFieldsList', () => {
)
);
- expect(await screen.findByTestId('confirm-delete-custom-field-modal')).toBeInTheDocument();
+ expect(await screen.findByTestId('confirm-delete-modal')).toBeInTheDocument();
userEvent.click(await screen.findByText('Delete'));
await waitFor(() => {
- expect(screen.queryByTestId('confirm-delete-custom-field-modal')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('confirm-delete-modal')).not.toBeInTheDocument();
expect(props.onDeleteCustomField).toHaveBeenCalledWith(
customFieldsConfigurationMock[0].key
);
@@ -136,12 +136,12 @@ describe('CustomFieldsList', () => {
)
);
- expect(await screen.findByTestId('confirm-delete-custom-field-modal')).toBeInTheDocument();
+ expect(await screen.findByTestId('confirm-delete-modal')).toBeInTheDocument();
userEvent.click(await screen.findByText('Cancel'));
await waitFor(() => {
- expect(screen.queryByTestId('confirm-delete-custom-field-modal')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('confirm-delete-modal')).not.toBeInTheDocument();
expect(props.onDeleteCustomField).not.toHaveBeenCalledWith();
});
});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/custom_fields_list/index.tsx b/x-pack/plugins/cases/public/components/custom_fields/custom_fields_list/index.tsx
index cfccb53e48db3..f8475a90b94ad 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/custom_fields_list/index.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/custom_fields_list/index.tsx
@@ -20,7 +20,7 @@ import * as i18n from '../translations';
import type { CustomFieldTypes, CustomFieldsConfiguration } from '../../../../common/types/domain';
import { builderMap } from '../builder';
-import { DeleteConfirmationModal } from '../delete_confirmation_modal';
+import { DeleteConfirmationModal } from '../../configure_cases/delete_confirmation_modal';
export interface Props {
customFields: CustomFieldsConfiguration;
@@ -111,7 +111,8 @@ const CustomFieldsListComponent: React.FC = (props) => {
{showModal && selectedItem ? (
diff --git a/x-pack/plugins/cases/public/components/custom_fields/text/create.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/text/create.test.tsx
index 9db8541993057..0b62466fa6858 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/text/create.test.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/text/create.test.tsx
@@ -53,6 +53,22 @@ describe('Create ', () => {
);
});
+ it('does not render default value when setDefaultValue is false', async () => {
+ render(
+
+
+
+ );
+
+ expect(
+ await screen.findByTestId(`${customFieldConfiguration.key}-text-create-custom-field`)
+ ).toHaveValue('');
+ });
+
it('renders loading state correctly', async () => {
render(
diff --git a/x-pack/plugins/cases/public/components/custom_fields/text/create.tsx b/x-pack/plugins/cases/public/components/custom_fields/text/create.tsx
index 4fae9d7b4816d..3a2c54286cd62 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/text/create.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/text/create.tsx
@@ -17,12 +17,13 @@ const CreateComponent: CustomFieldType['Create'] = ({
customFieldConfiguration,
isLoading,
setAsOptional,
+ setDefaultValue = true,
}) => {
const { key, label, required, defaultValue } = customFieldConfiguration;
const config = getTextFieldConfig({
required: setAsOptional ? false : required,
label,
- ...(defaultValue && { defaultValue: String(defaultValue) }),
+ ...(defaultValue && setDefaultValue && { defaultValue: String(defaultValue) }),
});
return (
diff --git a/x-pack/plugins/cases/public/components/custom_fields/toggle/create.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/toggle/create.test.tsx
index 9672b3c8bb6be..8eb7c50300840 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/toggle/create.test.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/toggle/create.test.tsx
@@ -36,6 +36,20 @@ describe('Create ', () => {
expect(await screen.findByRole('switch')).toBeChecked(); // defaultValue true
});
+ it('does not render default value when setDefaultValue is false', async () => {
+ render(
+
+
+
+ );
+
+ expect(await screen.findByRole('switch')).not.toBeChecked();
+ });
+
it('updates the value correctly', async () => {
render(
diff --git a/x-pack/plugins/cases/public/components/custom_fields/toggle/create.tsx b/x-pack/plugins/cases/public/components/custom_fields/toggle/create.tsx
index 2d3f51bc4f678..eb3ad2b114e57 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/toggle/create.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/toggle/create.tsx
@@ -14,6 +14,7 @@ import type { CustomFieldType } from '../types';
const CreateComponent: CustomFieldType['Create'] = ({
customFieldConfiguration,
isLoading,
+ setDefaultValue = true,
}) => {
const { key, label, defaultValue } = customFieldConfiguration;
@@ -21,7 +22,7 @@ const CreateComponent: CustomFieldType['Create'] = ({
{
customFieldConfiguration: CasesConfigurationUICustomField;
isLoading: boolean;
setAsOptional?: boolean;
+ setDefaultValue?: boolean;
}>;
}
diff --git a/x-pack/plugins/cases/public/components/templates/connector.test.tsx b/x-pack/plugins/cases/public/components/templates/connector.test.tsx
index cc053f52a34f1..3222363d6afa4 100644
--- a/x-pack/plugins/cases/public/components/templates/connector.test.tsx
+++ b/x-pack/plugins/cases/public/components/templates/connector.test.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { screen } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { AppMockRenderer } from '../../common/mock';
import { connectorsMock } from '../../containers/mock';
@@ -96,6 +96,53 @@ describe('Connector', () => {
expect(await screen.findByTestId('connector-fields-jira')).toBeInTheDocument();
});
+ it('renders existing connector correctly in edit mode', async () => {
+ appMockRender.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('caseConnectors')).toBeInTheDocument();
+ expect(await screen.findByText('My Connector SIR')).toBeInTheDocument();
+
+ expect(await screen.findByTestId('connector-fields-sn-sir')).toBeInTheDocument();
+ });
+
+ it('calls on submit with existing connector over configuration connector in edit mode', async () => {
+ const onSubmit = jest.fn();
+
+ appMockRender.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('caseConnectors')).toBeInTheDocument();
+ expect(await screen.findByText('My Resilient connector')).toBeInTheDocument();
+
+ expect(screen.queryByTestId('connector-fields-jira')).not.toBeInTheDocument();
+
+ userEvent.click(await screen.findByText('Submit'));
+
+ await waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledWith(
+ {
+ connectorId: 'resilient-2',
+ fields: {
+ incidentTypes: [],
+ },
+ },
+ true
+ );
+ });
+ });
+
it('shows all connectors in dropdown', async () => {
appMockRender.render(
diff --git a/x-pack/plugins/cases/public/components/templates/connector.tsx b/x-pack/plugins/cases/public/components/templates/connector.tsx
index 2886da2663333..4194ee6ed5860 100644
--- a/x-pack/plugins/cases/public/components/templates/connector.tsx
+++ b/x-pack/plugins/cases/public/components/templates/connector.tsx
@@ -23,16 +23,16 @@ interface Props {
connectors: ActionConnector[];
isLoading: boolean;
configurationConnectorId: string;
+ isEditMode?: boolean;
}
const ConnectorComponent: React.FC = ({
connectors,
isLoading,
configurationConnectorId,
+ isEditMode = false,
}) => {
const [{ connectorId }] = useFormData({ watch: ['connectorId'] });
- const connector = getConnectorById(connectorId, connectors) ?? null;
-
const { actions } = useApplicationCapabilities();
const { permissions } = useCasesContext();
const hasReadPermissions = permissions.connectors && actions.read;
@@ -42,6 +42,8 @@ const ConnectorComponent: React.FC = ({
connectors,
});
+ const connector = getConnectorById(connectorId, connectors) ?? null;
+
if (!hasReadPermissions) {
return (
@@ -57,7 +59,7 @@ const ConnectorComponent: React.FC = ({
path="connectorId"
config={connectorIdConfig}
component={ConnectorSelector}
- defaultValue={configurationConnectorId}
+ defaultValue={isEditMode ? connectorId : configurationConnectorId}
componentProps={{
connectors,
dataTestSubj: 'caseConnectors',
diff --git a/x-pack/plugins/cases/public/components/templates/form.test.tsx b/x-pack/plugins/cases/public/components/templates/form.test.tsx
index 9b30f89f24713..4754675b6abe4 100644
--- a/x-pack/plugins/cases/public/components/templates/form.test.tsx
+++ b/x-pack/plugins/cases/public/components/templates/form.test.tsx
@@ -17,7 +17,11 @@ import {
MAX_TEMPLATE_TAG_LENGTH,
} from '../../../common/constants';
import { ConnectorTypes, CustomFieldTypes } from '../../../common/types/domain';
-import { connectorsMock, customFieldsConfigurationMock } from '../../containers/mock';
+import {
+ connectorsMock,
+ customFieldsConfigurationMock,
+ templatesConfigurationMock,
+} from '../../containers/mock';
import { useGetChoices } from '../connectors/servicenow/use_get_choices';
import { useGetChoicesResponse } from '../create/mock';
import type { FormState } from '../configure_cases/flyout';
@@ -83,7 +87,7 @@ describe('TemplateForm', () => {
initialValue: {
key: 'template_key_1',
name: 'Template 1',
- templateDescription: 'Sample description',
+ description: 'Sample description',
},
};
appMockRenderer.render();
@@ -107,9 +111,11 @@ describe('TemplateForm', () => {
initialValue: {
key: 'template_key_1',
name: 'Template 1',
- templateDescription: 'Sample description',
- title: 'Case with template 1',
- description: 'case description',
+ description: 'Sample description',
+ caseFields: {
+ title: 'Case with template 1',
+ description: 'case description',
+ },
},
};
appMockRenderer.render();
@@ -173,31 +179,76 @@ describe('TemplateForm', () => {
name: 'Template 1',
templateDescription: 'this is a first template',
templateTags: ['foo', 'bar'],
+ title: '',
+ description: '',
+ severity: '',
+ tags: [],
connectorId: 'none',
syncAlerts: true,
- fields: null,
+ category: null,
});
});
});
- it('serializes the case field data correctly', async () => {
+ it('serializes the template field data correctly with existing fields', async () => {
let formState: FormState;
const onChangeState = (state: FormState) => (formState = state);
- appMockRenderer.render();
+ const newProps = {
+ ...defaultProps,
+ initialValue: { ...templatesConfigurationMock[0], tags: ['foo', 'bar'] },
+ connectors: [],
+ onChange: onChangeState,
+ isEditMode: true,
+ };
+
+ appMockRenderer.render();
await waitFor(() => {
expect(formState).not.toBeUndefined();
});
- userEvent.paste(await screen.findByTestId('template-name-input'), 'Template 1');
+ await act(async () => {
+ const { data, isValid } = await formState!.submit();
- userEvent.paste(
- await screen.findByTestId('template-description-input'),
- 'this is a first template'
+ expect(isValid).toBe(true);
+
+ expect(data).toEqual({
+ key: expect.anything(),
+ name: 'First test template',
+ title: '',
+ description: '',
+ templateDescription: 'This is a first test template',
+ tags: [],
+ connectorId: 'none',
+ severity: '',
+ syncAlerts: true,
+ category: null,
+ templateTags: ['foo', 'bar'],
+ });
+ });
+ });
+
+ it('serializes the case field data correctly', async () => {
+ let formState: FormState;
+
+ const onChangeState = (state: FormState) => (formState = state);
+
+ appMockRenderer.render(
+
);
+ await waitFor(() => {
+ expect(formState).not.toBeUndefined();
+ });
+
const caseTitle = await screen.findByTestId('caseTitle');
userEvent.paste(within(caseTitle).getByTestId('input'), 'Case with Template 1');
@@ -222,14 +273,55 @@ describe('TemplateForm', () => {
expect(data).toEqual({
key: expect.anything(),
name: 'Template 1',
- templateDescription: 'this is a first template',
+ templateDescription: '',
+ templateTags: [],
title: 'Case with Template 1',
description: 'This is a case description',
tags: ['template-1'],
+ severity: '',
category: 'new',
connectorId: 'none',
syncAlerts: true,
- fields: null,
+ });
+ });
+ });
+
+ it('serializes the case field data correctly with existing fields', async () => {
+ let formState: FormState;
+
+ const onChangeState = (state: FormState) => (formState = state);
+
+ const newProps = {
+ ...defaultProps,
+ initialValue: templatesConfigurationMock[3],
+ connectors: [],
+ onChange: onChangeState,
+ isEditMode: true,
+ };
+
+ appMockRenderer.render();
+
+ await waitFor(() => {
+ expect(formState).not.toBeUndefined();
+ });
+
+ await act(async () => {
+ const { data, isValid } = await formState!.submit();
+
+ expect(isValid).toBe(true);
+
+ expect(data).toEqual({
+ key: expect.anything(),
+ name: 'Fourth test template',
+ title: 'Case with sample template 4',
+ description: 'case desc',
+ templateDescription: 'This is a fourth test template',
+ tags: ['sample-4'],
+ connectorId: 'none',
+ severity: 'low',
+ syncAlerts: true,
+ category: null,
+ templateTags: ['foo', 'bar'],
});
});
});
@@ -243,6 +335,7 @@ describe('TemplateForm', () => {
{
expect(formState).not.toBeUndefined();
});
- userEvent.paste(await screen.findByTestId('template-name-input'), 'Template 1');
-
- userEvent.paste(
- await screen.findByTestId('template-description-input'),
- 'this is a first template'
- );
-
expect(await screen.findByTestId('connector-fields-sn-itsm')).toBeInTheDocument();
userEvent.selectOptions(await screen.findByTestId('urgencySelect'), '1');
@@ -286,13 +372,90 @@ describe('TemplateForm', () => {
expect(data).toEqual({
key: expect.anything(),
name: 'Template 1',
- templateDescription: 'this is a first template',
+ tags: [],
+ templateDescription: '',
+ templateTags: [],
+ title: '',
+ description: '',
+ category: null,
+ severity: '',
connectorId: 'servicenow-1',
fields: {
category: 'software',
urgency: '1',
- impact: null,
- severity: null,
+ impact: '',
+ severity: '',
+ subcategory: null,
+ },
+ syncAlerts: true,
+ });
+ });
+ });
+
+ it('serializes the connector fields data correctly with existing connector', async () => {
+ let formState: FormState;
+
+ const onChangeState = (state: FormState) => (formState = state);
+
+ const newProps = {
+ ...defaultProps,
+ initialValue: {
+ key: 'template_1_key',
+ name: 'Template 1',
+ caseFields: {
+ connector: {
+ id: 'servicenow-1',
+ type: ConnectorTypes.serviceNowITSM,
+ name: 'my-SN-connector',
+ fields: null,
+ },
+ },
+ },
+ connectors: connectorsMock,
+ currentConfiguration: {
+ ...defaultProps.currentConfiguration,
+ connector: {
+ id: 'resilient-2',
+ name: 'My Resilient connector',
+ type: ConnectorTypes.resilient,
+ fields: null,
+ },
+ },
+ onChange: onChangeState,
+ isEditMode: true,
+ };
+
+ appMockRenderer.render();
+
+ await waitFor(() => {
+ expect(formState).not.toBeUndefined();
+ });
+
+ expect(await screen.findByTestId('connector-fields-sn-itsm')).toBeInTheDocument();
+
+ userEvent.selectOptions(await screen.findByTestId('categorySelect'), ['Denial of Service']);
+
+ await act(async () => {
+ const { data, isValid } = await formState!.submit();
+
+ expect(isValid).toBe(true);
+
+ expect(data).toEqual({
+ key: expect.anything(),
+ name: 'Template 1',
+ tags: [],
+ templateDescription: '',
+ templateTags: [],
+ title: '',
+ description: '',
+ category: null,
+ severity: '',
+ connectorId: 'servicenow-1',
+ fields: {
+ category: 'Denial of Service',
+ urgency: '',
+ impact: '',
+ severity: '',
subcategory: null,
},
syncAlerts: true,
@@ -309,6 +472,10 @@ describe('TemplateForm', () => {
{
expect(formState).not.toBeUndefined();
});
- userEvent.paste(await screen.findByTestId('template-name-input'), 'Template 1');
-
- userEvent.paste(
- await screen.findByTestId('template-description-input'),
- 'this is a first template'
- );
-
const customFieldsElement = await screen.findByTestId('caseCustomFields');
expect(
@@ -360,15 +520,91 @@ describe('TemplateForm', () => {
expect(data).toEqual({
key: expect.anything(),
name: 'Template 1',
- templateDescription: 'this is a first template',
+ tags: [],
+ templateDescription: '',
+ templateTags: [],
+ title: '',
+ description: '',
+ severity: '',
+ category: null,
connectorId: 'none',
syncAlerts: true,
customFields: {
test_key_1: 'My text test value 1',
test_key_2: true,
+ test_key_3: '',
test_key_4: true,
},
- fields: null,
+ });
+ });
+ });
+
+ it('serializes the custom fields data correctly with existing custom fields', async () => {
+ let formState: FormState;
+
+ const onChangeState = (state: FormState) => (formState = state);
+
+ const newProps = {
+ ...defaultProps,
+ initialValue: {
+ key: 'template_1_key',
+ name: 'Template 1',
+ caseFields: {
+ customFields: [
+ {
+ type: CustomFieldTypes.TEXT,
+ key: 'test_key_1',
+ value: 'this is my first custom field value',
+ },
+ {
+ type: CustomFieldTypes.TOGGLE,
+ key: 'test_key_2',
+ value: false,
+ },
+ ],
+ },
+ },
+ onChange: onChangeState,
+ currentConfiguration: {
+ ...defaultProps.currentConfiguration,
+ customFields: customFieldsConfigurationMock,
+ },
+ };
+ appMockRenderer.render();
+
+ await waitFor(() => {
+ expect(formState).not.toBeUndefined();
+ });
+
+ const toggleField = customFieldsConfigurationMock[1];
+
+ userEvent.click(
+ await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
+ );
+
+ await act(async () => {
+ const { data, isValid } = await formState!.submit();
+
+ expect(isValid).toBe(true);
+
+ expect(data).toEqual({
+ key: expect.anything(),
+ name: 'Template 1',
+ tags: [],
+ templateDescription: '',
+ templateTags: [],
+ title: '',
+ description: '',
+ severity: '',
+ category: null,
+ connectorId: 'none',
+ syncAlerts: true,
+ customFields: {
+ test_key_1: 'this is my first custom field value',
+ test_key_2: true,
+ test_key_3: '',
+ test_key_4: false,
+ },
});
});
});
diff --git a/x-pack/plugins/cases/public/components/templates/form.tsx b/x-pack/plugins/cases/public/components/templates/form.tsx
index e7ca2451bb179..2e5149aa76efe 100644
--- a/x-pack/plugins/cases/public/components/templates/form.tsx
+++ b/x-pack/plugins/cases/public/components/templates/form.tsx
@@ -9,18 +9,19 @@ import { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_l
import React, { useEffect, useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import type { ActionConnector } from '../../../common/types/domain';
+import type { FormState } from '../configure_cases/flyout';
import { schema } from './schema';
import { FormFields } from './form_fields';
-import { templateSerializer } from './utils';
+import { templateDeserializer } from './utils';
import type { TemplateFormProps } from './types';
import type { CasesConfigurationUI } from '../../containers/types';
-import type { FormState } from '../configure_cases/flyout';
interface Props {
onChange: (state: FormState) => void;
initialValue: TemplateFormProps | null;
connectors: ActionConnector[];
currentConfiguration: CasesConfigurationUI;
+ isEditMode?: boolean;
}
const FormComponent: React.FC = ({
@@ -28,6 +29,7 @@ const FormComponent: React.FC = ({
initialValue,
connectors,
currentConfiguration,
+ isEditMode = false,
}) => {
const keyDefaultValue = useMemo(() => uuidv4(), []);
@@ -35,12 +37,13 @@ const FormComponent: React.FC = ({
defaultValue: initialValue ?? {
key: keyDefaultValue,
name: '',
- templateDescription: '',
- templateTags: [],
+ description: '',
+ tags: [],
+ caseFields: null,
},
options: { stripEmptyFields: false },
schema,
- serializer: templateSerializer,
+ deserializer: templateDeserializer,
});
const { submit, isValid, isSubmitting } = form;
@@ -57,6 +60,7 @@ const FormComponent: React.FC = ({
isSubmitting={isSubmitting}
connectors={connectors}
currentConfiguration={currentConfiguration}
+ isEditMode={isEditMode}
/>
);
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
index 103dcb9135b39..61c709dd7a027 100644
--- a/x-pack/plugins/cases/public/components/templates/form_fields.test.tsx
+++ b/x-pack/plugins/cases/public/components/templates/form_fields.test.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { AppMockRenderer } from '../../common/mock';
-import { ConnectorTypes } from '../../../common/types/domain';
+import { CaseSeverity, ConnectorTypes } from '../../../common/types/domain';
import { createAppMockRenderer, mockedTestProvidersOwner } from '../../common/mock';
import { FormTestComponent } from '../../common/test_utils';
import { useGetChoices } from '../connectors/servicenow/use_get_choices';
@@ -25,6 +25,7 @@ const useGetChoicesMock = useGetChoices as jest.Mock;
describe('form fields', () => {
let appMockRenderer: AppMockRenderer;
const onSubmit = jest.fn();
+ const formDefaultValue = { tags: [], templateTags: [] };
const defaultProps = {
connectors: connectorsMock,
currentConfiguration: {
@@ -52,7 +53,7 @@ describe('form fields', () => {
it('renders correctly', async () => {
appMockRenderer.render(
-
+
);
@@ -62,7 +63,7 @@ describe('form fields', () => {
it('renders all steps', async () => {
appMockRenderer.render(
-
+
);
@@ -75,7 +76,7 @@ describe('form fields', () => {
it('renders template fields correctly', async () => {
appMockRenderer.render(
-
+
);
@@ -88,7 +89,7 @@ describe('form fields', () => {
it('renders case fields', async () => {
appMockRenderer.render(
-
+
);
@@ -101,9 +102,40 @@ describe('form fields', () => {
expect(await screen.findByTestId('caseDescription')).toBeInTheDocument();
});
+ it('renders case fields with existing value', async () => {
+ appMockRenderer.render(
+
+
+
+ );
+
+ expect(await within(await screen.findByTestId('caseTitle')).findByTestId('input')).toHaveValue(
+ 'Case title'
+ );
+
+ const caseTags = await screen.findByTestId('caseTags');
+ expect(await within(caseTags).findByTestId('comboBoxInput')).toHaveTextContent('case-1');
+ expect(await within(caseTags).findByTestId('comboBoxInput')).toHaveTextContent('case-2');
+
+ const category = await screen.findByTestId('caseCategory');
+ expect(await within(category).findByTestId('comboBoxSearchInput')).toHaveValue('new');
+ expect(await screen.findByTestId('case-severity-selection-medium')).toBeInTheDocument();
+ expect(await screen.findByTestId('caseDescription')).toHaveTextContent('case description');
+ });
+
it('renders sync alerts correctly', async () => {
appMockRenderer.render(
-
+
);
@@ -121,7 +153,7 @@ describe('form fields', () => {
};
appMockRenderer.render(
-
+
);
@@ -131,7 +163,7 @@ describe('form fields', () => {
it('renders default connector correctly', async () => {
appMockRenderer.render(
-
+
);
@@ -154,7 +186,7 @@ describe('form fields', () => {
};
appMockRenderer.render(
-
+
);
@@ -170,7 +202,7 @@ describe('form fields', () => {
});
appMockRenderer.render(
-
+
);
@@ -180,7 +212,7 @@ describe('form fields', () => {
it('calls onSubmit with template fields', async () => {
appMockRenderer.render(
-
+
);
@@ -217,7 +249,7 @@ describe('form fields', () => {
it('calls onSubmit with case fields', async () => {
appMockRenderer.render(
-
+
);
@@ -266,7 +298,7 @@ describe('form fields', () => {
};
appMockRenderer.render(
-
+
);
@@ -323,7 +355,7 @@ describe('form fields', () => {
};
appMockRenderer.render(
-
+
);
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 6a063f5710a17..1f47407d241bb 100644
--- a/x-pack/plugins/cases/public/components/templates/form_fields.tsx
+++ b/x-pack/plugins/cases/public/components/templates/form_fields.tsx
@@ -22,12 +22,14 @@ interface FormFieldsProps {
isSubmitting?: boolean;
connectors: ActionConnector[];
currentConfiguration: CasesConfigurationUI;
+ isEditMode?: boolean;
}
const FormFieldsComponent: React.FC = ({
isSubmitting = false,
connectors,
currentConfiguration,
+ isEditMode,
}) => {
const { isSyncAlertsEnabled } = useCasesFeatures();
const { customFields: configurationCustomFields, connector, templates } = currentConfiguration;
@@ -56,10 +58,11 @@ const FormFieldsComponent: React.FC = ({
configurationCustomFields={configurationCustomFields}
isLoading={isSubmitting}
setCustomFieldsOptional={true}
+ isEditMode={isEditMode}
/>
),
}),
- [isSubmitting, configurationCustomFields]
+ [isSubmitting, configurationCustomFields, isEditMode]
);
const thirdStep = useMemo(
@@ -74,16 +77,15 @@ const FormFieldsComponent: React.FC = ({
() => ({
title: i18n.CONNECTOR_FIELDS,
children: (
-
-
-
+
),
}),
- [connectors, connector, isSubmitting]
+ [connectors, connector, isSubmitting, isEditMode]
);
const allSteps = useMemo(
diff --git a/x-pack/plugins/cases/public/components/templates/index.test.tsx b/x-pack/plugins/cases/public/components/templates/index.test.tsx
index 075a4d2a2bd62..2745741fb87a7 100644
--- a/x-pack/plugins/cases/public/components/templates/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/templates/index.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
-import { screen } from '@testing-library/react';
+import { screen, waitFor, within } from '@testing-library/react';
import type { AppMockRenderer } from '../../common/mock';
import { createAppMockRenderer } from '../../common/mock';
@@ -25,6 +25,8 @@ describe('Templates', () => {
isLoading: false,
templates: [],
onAddTemplate: jest.fn(),
+ onEditTemplate: jest.fn(),
+ onDeleteTemplate: jest.fn(),
};
beforeEach(() => {
@@ -74,6 +76,40 @@ describe('Templates', () => {
expect(props.onAddTemplate).toBeCalled();
});
+ it('calls onEditTemplate correctly', async () => {
+ appMockRender.render();
+
+ const list = await screen.findByTestId('templates-list');
+
+ expect(list).toBeInTheDocument();
+
+ userEvent.click(
+ await within(list).findByTestId(`${templatesConfigurationMock[0].key}-template-edit`)
+ );
+
+ await waitFor(() => {
+ expect(props.onEditTemplate).toHaveBeenCalledWith(templatesConfigurationMock[0].key);
+ });
+ });
+
+ it('calls onDeleteTemplate correctly', async () => {
+ appMockRender.render();
+
+ const list = await screen.findByTestId('templates-list');
+
+ userEvent.click(
+ await within(list).findByTestId(`${templatesConfigurationMock[0].key}-template-delete`)
+ );
+
+ expect(await screen.findByTestId('confirm-delete-modal')).toBeInTheDocument();
+
+ userEvent.click(await screen.findByText('Delete'));
+
+ await waitFor(() => {
+ expect(props.onDeleteTemplate).toHaveBeenCalledWith(templatesConfigurationMock[0].key);
+ });
+ });
+
it('shows the experimental badge', async () => {
appMockRender.render();
diff --git a/x-pack/plugins/cases/public/components/templates/index.tsx b/x-pack/plugins/cases/public/components/templates/index.tsx
index f86037ce97813..9671b9aee8556 100644
--- a/x-pack/plugins/cases/public/components/templates/index.tsx
+++ b/x-pack/plugins/cases/public/components/templates/index.tsx
@@ -27,9 +27,18 @@ interface Props {
isLoading: boolean;
templates: CasesConfigurationUITemplate[];
onAddTemplate: () => void;
+ onEditTemplate: (key: string) => void;
+ onDeleteTemplate: (key: string) => void;
}
-const TemplatesComponent: React.FC = ({ disabled, isLoading, templates, onAddTemplate }) => {
+const TemplatesComponent: React.FC = ({
+ disabled,
+ isLoading,
+ templates,
+ onAddTemplate,
+ onEditTemplate,
+ onDeleteTemplate,
+}) => {
const { permissions } = useCasesContext();
const canAddTemplates = permissions.create && permissions.update;
const [error, setError] = useState(false);
@@ -44,6 +53,22 @@ const TemplatesComponent: React.FC = ({ disabled, isLoading, templates, o
setError(false);
}, [onAddTemplate, error, templates]);
+ const handleEditTemplate = useCallback(
+ (key: string) => {
+ setError(false);
+ onEditTemplate(key);
+ },
+ [setError, onEditTemplate]
+ );
+
+ const handleDeleteTemplate = useCallback(
+ (key: string) => {
+ setError(false);
+ onDeleteTemplate(key);
+ },
+ [setError, onDeleteTemplate]
+ );
+
return (
= ({ disabled, isLoading, templates, o
{templates.length ? (
<>
-
+
{error ? (
diff --git a/x-pack/plugins/cases/public/components/templates/schema.tsx b/x-pack/plugins/cases/public/components/templates/schema.tsx
index bddecc8c36966..7fd0347025dc1 100644
--- a/x-pack/plugins/cases/public/components/templates/schema.tsx
+++ b/x-pack/plugins/cases/public/components/templates/schema.tsx
@@ -105,7 +105,6 @@ export const schema: FormSchema = {
connectorId: {
labelAppend: OptionalFieldLabel,
label: i18n.CONNECTORS,
- defaultValue: 'none',
},
fields: {
defaultValue: null,
diff --git a/x-pack/plugins/cases/public/components/templates/template_fields.test.tsx b/x-pack/plugins/cases/public/components/templates/template_fields.test.tsx
index 5d0dd30da8e69..8073c2e25fb41 100644
--- a/x-pack/plugins/cases/public/components/templates/template_fields.test.tsx
+++ b/x-pack/plugins/cases/public/components/templates/template_fields.test.tsx
@@ -16,6 +16,7 @@ import { TemplateFields } from './template_fields';
describe('Template fields', () => {
let appMockRenderer: AppMockRenderer;
const onSubmit = jest.fn();
+ const formDefaultValue = { templateTags: [] };
const defaultProps = {
isLoading: false,
configurationTemplateTags: [],
@@ -28,7 +29,7 @@ describe('Template fields', () => {
it('renders template fields correctly', async () => {
appMockRenderer.render(
-
+
);
@@ -38,9 +39,39 @@ describe('Template fields', () => {
expect(await screen.findByTestId('template-description-input')).toBeInTheDocument();
});
+ it('renders template fields with existing value', async () => {
+ appMockRenderer.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('template-name-input')).toHaveValue('Sample template');
+
+ const templateTags = await screen.findByTestId('template-tags');
+
+ expect(await within(templateTags).findByTestId('comboBoxInput')).toHaveTextContent(
+ 'template-1'
+ );
+ expect(await within(templateTags).findByTestId('comboBoxInput')).toHaveTextContent(
+ 'template-2'
+ );
+
+ expect(await screen.findByTestId('template-description-input')).toHaveTextContent(
+ 'This is a template description'
+ );
+ });
+
it('calls onSubmit with template fields', async () => {
appMockRenderer.render(
-
+
);
@@ -49,7 +80,7 @@ describe('Template fields', () => {
const templateTags = await screen.findByTestId('template-tags');
- userEvent.paste(within(templateTags).getByRole('combobox'), 'first');
+ userEvent.paste(await within(templateTags).findByRole('combobox'), 'first');
userEvent.keyboard('{enter}');
userEvent.paste(
@@ -70,4 +101,41 @@ describe('Template fields', () => {
);
});
});
+
+ it('calls onSubmit with updated template fields', async () => {
+ appMockRenderer.render(
+
+
+
+ );
+
+ userEvent.paste(await screen.findByTestId('template-name-input'), '!!');
+
+ const templateTags = await screen.findByTestId('template-tags');
+
+ userEvent.paste(await within(templateTags).findByRole('combobox'), 'first');
+ userEvent.keyboard('{enter}');
+
+ userEvent.paste(await screen.findByTestId('template-description-input'), '..');
+
+ userEvent.click(screen.getByText('Submit'));
+
+ await waitFor(() => {
+ expect(onSubmit).toBeCalledWith(
+ {
+ name: 'Sample template!!',
+ templateDescription: 'This is a template description..',
+ templateTags: ['template-1', 'template-2', 'first'],
+ },
+ true
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/cases/public/components/templates/template_fields.tsx b/x-pack/plugins/cases/public/components/templates/template_fields.tsx
index e9c34af53ca69..adc817cee41c1 100644
--- a/x-pack/plugins/cases/public/components/templates/template_fields.tsx
+++ b/x-pack/plugins/cases/public/components/templates/template_fields.tsx
@@ -29,7 +29,7 @@ const TemplateFieldsComponent: React.FC<{
},
}}
/>
-
+
{
let appMockRenderer: AppMockRenderer;
const onSubmit = jest.fn();
+ const formDefaultValue = { templateTags: [] };
beforeEach(() => {
jest.clearAllMocks();
@@ -26,8 +27,8 @@ describe('TemplateTags', () => {
it('renders template tags', async () => {
appMockRenderer.render(
-
-
+
+
);
@@ -36,8 +37,8 @@ describe('TemplateTags', () => {
it('renders loading state', async () => {
appMockRenderer.render(
-
-
+
+
);
@@ -47,8 +48,8 @@ describe('TemplateTags', () => {
it('shows template tags options', async () => {
appMockRenderer.render(
-
-
+
+
);
@@ -59,10 +60,24 @@ describe('TemplateTags', () => {
expect(await screen.findByText('foo')).toBeInTheDocument();
});
+ it('shows template tags with current values', async () => {
+ appMockRenderer.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('template-tags')).toBeInTheDocument();
+
+ expect(await screen.findByText('foo')).toBeInTheDocument();
+
+ expect(await screen.findByText('bar')).toBeInTheDocument();
+ });
+
it('adds template tag ', async () => {
appMockRenderer.render(
-
-
+
+
);
@@ -85,4 +100,29 @@ describe('TemplateTags', () => {
);
});
});
+
+ it('adds new template tag to existing tags', async () => {
+ appMockRenderer.render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('template-tags')).toBeInTheDocument();
+
+ const comboBoxEle = await screen.findByRole('combobox');
+ userEvent.paste(comboBoxEle, 'test');
+ userEvent.keyboard('{enter}');
+
+ userEvent.click(screen.getByText('Submit'));
+
+ await waitFor(() => {
+ expect(onSubmit).toBeCalledWith(
+ {
+ templateTags: ['foo', 'bar', 'test'],
+ },
+ true
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/cases/public/components/templates/template_tags.tsx b/x-pack/plugins/cases/public/components/templates/template_tags.tsx
index 16c37154a650f..92f141a73eb85 100644
--- a/x-pack/plugins/cases/public/components/templates/template_tags.tsx
+++ b/x-pack/plugins/cases/public/components/templates/template_tags.tsx
@@ -12,11 +12,11 @@ import { ComboBoxField } from '@kbn/es-ui-shared-plugin/static/forms/components'
import * as i18n from './translations';
interface Props {
isLoading: boolean;
- tags: string[];
+ tagOptions: string[];
}
-const TemplateTagsComponent: React.FC = ({ isLoading, tags }) => {
- const options = tags.map((label) => ({
+const TemplateTagsComponent: React.FC = ({ isLoading, tagOptions }) => {
+ const options = tagOptions.map((label) => ({
label,
}));
@@ -24,13 +24,12 @@ const TemplateTagsComponent: React.FC = ({ isLoading, tags }) => {
{
let appMockRender: AppMockRenderer;
+ const onDeleteTemplate = jest.fn();
+ const onEditTemplate = jest.fn();
const props = {
templates: templatesConfigurationMock,
+ onDeleteTemplate,
+ onEditTemplate,
};
beforeEach(() => {
@@ -70,4 +75,71 @@ describe('TemplatesList', () => {
expect(screen.queryAllByTestId(`template-`, { exact: false })).toHaveLength(0);
});
+
+ it('renders edit button', async () => {
+ appMockRender.render(
+
+ );
+
+ expect(
+ await screen.findByTestId(`${templatesConfigurationMock[0].key}-template-edit`)
+ ).toBeInTheDocument();
+ });
+
+ it('renders delete button', async () => {
+ appMockRender.render(
+
+ );
+
+ expect(
+ await screen.findByTestId(`${templatesConfigurationMock[0].key}-template-delete`)
+ ).toBeInTheDocument();
+ });
+
+ it('renders delete modal', async () => {
+ appMockRender.render(
+
+ );
+
+ userEvent.click(
+ await screen.findByTestId(`${templatesConfigurationMock[0].key}-template-delete`)
+ );
+
+ expect(await screen.findByTestId('confirm-delete-modal')).toBeInTheDocument();
+ expect(await screen.findByText('Delete')).toBeInTheDocument();
+ expect(await screen.findByText('Cancel')).toBeInTheDocument();
+ });
+
+ it('calls onEditTemplate correctly', async () => {
+ appMockRender.render();
+
+ const list = await screen.findByTestId('templates-list');
+
+ userEvent.click(
+ await within(list).findByTestId(`${templatesConfigurationMock[0].key}-template-edit`)
+ );
+
+ await waitFor(() => {
+ expect(props.onEditTemplate).toHaveBeenCalledWith(templatesConfigurationMock[0].key);
+ });
+ });
+
+ it('calls onDeleteTemplate correctly', async () => {
+ appMockRender.render();
+
+ const list = await screen.findByTestId('templates-list');
+
+ userEvent.click(
+ await within(list).findByTestId(`${templatesConfigurationMock[0].key}-template-delete`)
+ );
+
+ expect(await screen.findByTestId('confirm-delete-modal')).toBeInTheDocument();
+
+ userEvent.click(await screen.findByText('Delete'));
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('confirm-delete-modal')).not.toBeInTheDocument();
+ expect(props.onDeleteTemplate).toHaveBeenCalledWith(templatesConfigurationMock[0].key);
+ });
+ });
});
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 999b02edf32a1..15d78aeb3107c 100644
--- a/x-pack/plugins/cases/public/components/templates/templates_list.tsx
+++ b/x-pack/plugins/cases/public/components/templates/templates_list.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { useCallback, useState } from 'react';
import {
EuiPanel,
EuiFlexGroup,
@@ -14,18 +14,37 @@ import {
EuiText,
EuiBadge,
useEuiTheme,
+ EuiButtonIcon,
} from '@elastic/eui';
import { css } from '@emotion/react';
-import type { CasesConfigurationUITemplate } from '../../../common/ui';
import { TruncatedText } from '../truncated_text';
-
+import type { TemplateConfiguration, TemplatesConfiguration } from '../../../common/types/domain';
+import { DeleteConfirmationModal } from '../configure_cases/delete_confirmation_modal';
+import * as i18n from './translations';
export interface Props {
- templates: CasesConfigurationUITemplate[];
+ templates: TemplatesConfiguration;
+ onDeleteTemplate: (key: string) => void;
+ onEditTemplate: (key: string) => void;
}
const TemplatesListComponent: React.FC = (props) => {
- const { templates } = props;
+ const { templates, onEditTemplate, onDeleteTemplate } = props;
const { euiTheme } = useEuiTheme();
+ const [itemToBeDeleted, SetItemToBeDeleted] = useState(null);
+
+ const onConfirm = useCallback(() => {
+ if (itemToBeDeleted) {
+ onDeleteTemplate(itemToBeDeleted.key);
+ }
+
+ SetItemToBeDeleted(null);
+ }, [onDeleteTemplate, SetItemToBeDeleted, itemToBeDeleted]);
+
+ const onCancel = useCallback(() => {
+ SetItemToBeDeleted(null);
+ }, []);
+
+ const showModal = Boolean(itemToBeDeleted);
return templates.length ? (
<>
@@ -65,12 +84,42 @@ const TemplatesListComponent: React.FC = (props) => {
: null}
+
+
+
+ onEditTemplate(template.key)}
+ />
+
+
+ SetItemToBeDeleted(template)}
+ />
+
+
+
))}
+ {showModal && itemToBeDeleted ? (
+
+ ) : 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 c48d3bce328f7..2993070046813 100644
--- a/x-pack/plugins/cases/public/components/templates/translations.ts
+++ b/x-pack/plugins/cases/public/components/templates/translations.ts
@@ -65,6 +65,18 @@ export const CONNECTOR_FIELDS = i18n.translate('xpack.cases.templates.connectorF
defaultMessage: 'External Connector Fields',
});
+export const DELETE_TITLE = (name: string) =>
+ i18n.translate('xpack.cases.configuration.deleteTitle', {
+ values: { name },
+ defaultMessage: 'Delete {name}?',
+ });
+
+export const DELETE_MESSAGE = (name: string) =>
+ i18n.translate('xpack.cases.configuration.deleteMessage', {
+ values: { name },
+ defaultMessage: 'This action will permanently delete {name}.',
+ });
+
export const MAX_TEMPLATE_LIMIT = (maxTemplates: number) =>
i18n.translate('xpack.cases.templates.maxTemplateLimit', {
values: { maxTemplates },
diff --git a/x-pack/plugins/cases/public/components/templates/types.ts b/x-pack/plugins/cases/public/components/templates/types.ts
index 38fe786e52f57..eba31a80ebe4e 100644
--- a/x-pack/plugins/cases/public/components/templates/types.ts
+++ b/x-pack/plugins/cases/public/components/templates/types.ts
@@ -24,5 +24,5 @@ export type CaseFieldsProps = Omit<
export type TemplateFormProps = Pick &
CaseFieldsProps & {
templateTags?: string[];
- templateDescription: string;
+ templateDescription?: string;
};
diff --git a/x-pack/plugins/cases/public/components/templates/utils.test.ts b/x-pack/plugins/cases/public/components/templates/utils.test.ts
index 35ea896753a0f..e23e6f56b257f 100644
--- a/x-pack/plugins/cases/public/components/templates/utils.test.ts
+++ b/x-pack/plugins/cases/public/components/templates/utils.test.ts
@@ -5,12 +5,22 @@
* 2.0.
*/
-import { templateSerializer, removeEmptyFields } from './utils';
+import type { CaseUI } from '../../../common';
+import { CaseSeverity } from '../../../common';
+import {
+ convertTemplateCustomFields,
+ getTemplateSerializedData,
+ removeEmptyFields,
+ templateDeserializer,
+} from './utils';
+import { userProfiles } from '../../containers/user_profiles/api.mock';
+import { customFieldsConfigurationMock } from '../../containers/mock';
+import { ConnectorTypes, CustomFieldTypes } from '../../../common/types/domain';
describe('utils', () => {
- describe('templateSerializer', () => {
+ describe('getTemplateSerializedData', () => {
it('serializes empty fields correctly', () => {
- const res = templateSerializer({
+ const res = getTemplateSerializedData({
key: '',
name: '',
templateDescription: '',
@@ -26,7 +36,7 @@ describe('utils', () => {
});
it('serializes connectors fields correctly', () => {
- const res = templateSerializer({
+ const res = getTemplateSerializedData({
key: '',
name: '',
templateDescription: '',
@@ -39,7 +49,7 @@ describe('utils', () => {
});
it('serializes non empty fields correctly', () => {
- const res = templateSerializer({
+ const res = getTemplateSerializedData({
key: 'key_1',
name: 'template 1',
templateDescription: 'description 1',
@@ -58,7 +68,7 @@ describe('utils', () => {
});
it('serializes custom fields correctly', () => {
- const res = templateSerializer({
+ const res = getTemplateSerializedData({
key: 'key_1',
name: 'template 1',
templateDescription: '',
@@ -81,7 +91,7 @@ describe('utils', () => {
});
it('serializes connector fields correctly', () => {
- const res = templateSerializer({
+ const res = getTemplateSerializedData({
key: 'key_1',
name: 'template 1',
templateDescription: '',
@@ -138,4 +148,182 @@ describe('utils', () => {
});
});
});
+
+ describe('templateDeserializer', () => {
+ it('deserialzies initial data correctly', () => {
+ const res = templateDeserializer({ key: 'temlate_1', name: 'Template 1', caseFields: null });
+
+ expect(res).toEqual({
+ key: 'temlate_1',
+ name: 'Template 1',
+ templateDescription: '',
+ templateTags: [],
+ tags: [],
+ connectorId: 'none',
+ customFields: {},
+ fields: null,
+ });
+ });
+
+ it('deserialzies template data correctly', () => {
+ const res = templateDeserializer({
+ key: 'temlate_1',
+ name: 'Template 1',
+ description: 'This is first template',
+ tags: ['t1', 't2'],
+ caseFields: null,
+ });
+
+ expect(res).toEqual({
+ key: 'temlate_1',
+ name: 'Template 1',
+ templateDescription: 'This is first template',
+ templateTags: ['t1', 't2'],
+ tags: [],
+ connectorId: 'none',
+ customFields: {},
+ fields: null,
+ });
+ });
+
+ it('deserialzies case fields data correctly', () => {
+ const res = templateDeserializer({
+ key: 'temlate_1',
+ name: 'Template 1',
+ caseFields: {
+ title: 'Case title',
+ description: 'This is test case',
+ category: null,
+ tags: ['foo', 'bar'],
+ severity: CaseSeverity.LOW,
+ assignees: [{ uid: userProfiles[0].uid }],
+ },
+ });
+
+ expect(res).toEqual({
+ key: 'temlate_1',
+ name: 'Template 1',
+ templateDescription: '',
+ templateTags: [],
+ title: 'Case title',
+ description: 'This is test case',
+ category: null,
+ tags: ['foo', 'bar'],
+ severity: CaseSeverity.LOW,
+ assignees: [{ uid: userProfiles[0].uid }],
+ connectorId: 'none',
+ customFields: {},
+ fields: null,
+ });
+ });
+
+ it('deserialzies custom fields data correctly', () => {
+ const res = templateDeserializer({
+ key: 'temlate_1',
+ name: 'Template 1',
+ caseFields: {
+ customFields: [
+ {
+ key: customFieldsConfigurationMock[0].key,
+ type: CustomFieldTypes.TEXT,
+ value: 'this is first custom field value',
+ },
+ {
+ key: customFieldsConfigurationMock[1].key,
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ },
+ });
+
+ expect(res).toEqual({
+ key: 'temlate_1',
+ name: 'Template 1',
+ templateDescription: '',
+ templateTags: [],
+ tags: [],
+ connectorId: 'none',
+ customFields: {
+ [customFieldsConfigurationMock[0].key]: 'this is first custom field value',
+ [customFieldsConfigurationMock[1].key]: true,
+ },
+ fields: null,
+ });
+ });
+
+ it('deserialzies connector data correctly', () => {
+ const res = templateDeserializer({
+ key: 'temlate_1',
+ name: 'Template 1',
+ caseFields: {
+ connector: {
+ id: 'servicenow-1',
+ name: 'My SN connector',
+ type: ConnectorTypes.serviceNowITSM,
+ fields: {
+ category: 'software',
+ urgency: '1',
+ severity: null,
+ impact: null,
+ subcategory: null,
+ },
+ },
+ },
+ });
+
+ expect(res).toEqual({
+ key: 'temlate_1',
+ name: 'Template 1',
+ templateDescription: '',
+ templateTags: [],
+ tags: [],
+ connectorId: 'servicenow-1',
+ customFields: {},
+ fields: {
+ category: 'software',
+ impact: undefined,
+ severity: undefined,
+ subcategory: undefined,
+ urgency: '1',
+ },
+ });
+ });
+ });
+
+ describe('convertTemplateCustomFields', () => {
+ it('converts data correctly', () => {
+ const data = [
+ {
+ key: customFieldsConfigurationMock[0].key,
+ type: CustomFieldTypes.TEXT,
+ value: 'this is first custom field value',
+ },
+ {
+ key: customFieldsConfigurationMock[1].key,
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ] as CaseUI['customFields'];
+
+ const res = convertTemplateCustomFields(data);
+
+ expect(res).toEqual({
+ [customFieldsConfigurationMock[0].key]: 'this is first custom field value',
+ [customFieldsConfigurationMock[1].key]: true,
+ });
+ });
+
+ it('returns null when customFields empty', () => {
+ const res = convertTemplateCustomFields([]);
+
+ expect(res).toEqual(null);
+ });
+
+ it('returns null when customFields undefined', () => {
+ const res = convertTemplateCustomFields(undefined);
+
+ expect(res).toEqual(null);
+ });
+ });
});
diff --git a/x-pack/plugins/cases/public/components/templates/utils.ts b/x-pack/plugins/cases/public/components/templates/utils.ts
index 65262b7cefaa4..d118942e93071 100644
--- a/x-pack/plugins/cases/public/components/templates/utils.ts
+++ b/x-pack/plugins/cases/public/components/templates/utils.ts
@@ -6,7 +6,9 @@
*/
import { isEmpty } from 'lodash';
-import { getConnectorsFormSerializer } from '../utils';
+import type { TemplateConfiguration } from '../../../common/types/domain';
+import type { CaseUI } from '../../containers/types';
+import { getConnectorsFormDeserializer, getConnectorsFormSerializer } from '../utils';
import type { TemplateFormProps } from './types';
export function removeEmptyFields>(obj: T): Partial {
@@ -22,7 +24,46 @@ export function removeEmptyFields>(obj: T): Pa
) as T;
}
-export const templateSerializer = (data: TemplateFormProps): TemplateFormProps => {
+export const convertTemplateCustomFields = (
+ customFields?: CaseUI['customFields']
+): Record | null => {
+ if (!customFields || !customFields.length) {
+ return null;
+ }
+
+ return customFields.reduce((acc, customField) => {
+ const initial = {
+ [customField.key]: customField.value,
+ };
+
+ return { ...acc, ...initial };
+ }, {});
+};
+
+export const templateDeserializer = (data: TemplateConfiguration): TemplateFormProps => {
+ if (data !== null) {
+ const { key, name, description, tags: templateTags, caseFields } = data;
+ const { connector, customFields, settings, tags, ...rest } = caseFields ?? {};
+ const connectorFields = getConnectorsFormDeserializer({ fields: connector?.fields ?? null });
+ const convertedCustomFields = convertTemplateCustomFields(customFields);
+
+ return {
+ key,
+ name,
+ templateDescription: description ?? '',
+ templateTags: templateTags ?? [],
+ connectorId: connector?.id ?? 'none',
+ fields: connectorFields.fields,
+ customFields: convertedCustomFields ?? {},
+ tags: tags ?? [],
+ ...rest,
+ };
+ }
+
+ return data;
+};
+
+export const getTemplateSerializedData = (data: TemplateFormProps): TemplateFormProps => {
if (data !== null) {
const { fields = null, ...rest } = data;
const connectorFields = getConnectorsFormSerializer({ fields });
diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts
index 77480a81d6fca..ee013b882c487 100644
--- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts
@@ -121,7 +121,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
await deleteButton.click();
- await testSubjects.existOrFail('confirm-delete-custom-field-modal');
+ await testSubjects.existOrFail('confirm-delete-modal');
await testSubjects.click('confirmModalConfirmButton');
@@ -180,6 +180,52 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
expect(await testSubjects.getVisibleText('templates-list')).to.be('Template name\ntag-t1');
});
+
+ it('updates a template', async () => {
+ await testSubjects.existOrFail('templates-form-group');
+ const editButton = await find.byCssSelector('[data-test-subj*="-template-edit"]');
+
+ await editButton.click();
+
+ await testSubjects.setValue('template-name-input', 'Updated template name!');
+ await comboBox.setCustom('template-tags', 'tag-t1');
+ await testSubjects.setValue('template-description-input', 'Template description updated');
+
+ const caseTitle = await find.byCssSelector(
+ `[data-test-subj="input"][aria-describedby="caseTitle"]`
+ );
+ await caseTitle.focus();
+ await caseTitle.type('!!');
+
+ await cases.create.setDescription('test description!!');
+
+ await cases.create.setTags('case-tag');
+ await cases.create.setCategory('new!');
+
+ await testSubjects.click('common-flyout-save');
+ expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false);
+
+ await retry.waitFor('templates-list', async () => {
+ return await testSubjects.exists('templates-list');
+ });
+
+ expect(await testSubjects.getVisibleText('templates-list')).to.be(
+ 'Updated template name!\ntag-t1'
+ );
+ });
+
+ it('deletes a template', async () => {
+ await testSubjects.existOrFail('templates-form-group');
+ const deleteButton = await find.byCssSelector('[data-test-subj*="-template-delete"]');
+
+ await deleteButton.click();
+
+ await testSubjects.existOrFail('confirm-delete-modal');
+
+ await testSubjects.click('confirmModalConfirmButton');
+
+ await testSubjects.missingOrFail('template-list');
+ });
});
});
};
diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts
index 511981d067874..35fee3f110a2a 100644
--- a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts
+++ b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts
@@ -115,7 +115,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
await deleteButton.click();
- await testSubjects.existOrFail('confirm-delete-custom-field-modal');
+ await testSubjects.existOrFail('confirm-delete-modal');
await testSubjects.click('confirmModalConfirmButton');
@@ -152,6 +152,52 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
expect(await testSubjects.getVisibleText('templates-list')).to.be('Template name\ntag-t1');
});
+
+ it('updates a template', async () => {
+ await testSubjects.existOrFail('templates-form-group');
+ const editButton = await find.byCssSelector('[data-test-subj*="-template-edit"]');
+
+ await editButton.click();
+
+ await testSubjects.setValue('template-name-input', 'Updated template name!');
+ await comboBox.setCustom('template-tags', 'tag-t1');
+ await testSubjects.setValue('template-description-input', 'Template description updated');
+
+ const caseTitle = await find.byCssSelector(
+ `[data-test-subj="input"][aria-describedby="caseTitle"]`
+ );
+ await caseTitle.focus();
+ await caseTitle.type('!!');
+
+ await cases.create.setDescription('test description!!');
+
+ await cases.create.setTags('case-tag');
+ await cases.create.setCategory('new!');
+
+ await testSubjects.click('common-flyout-save');
+ expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false);
+
+ await retry.waitFor('templates-list', async () => {
+ return await testSubjects.exists('templates-list');
+ });
+
+ expect(await testSubjects.getVisibleText('templates-list')).to.be(
+ 'Updated template name!\ntag-t1'
+ );
+ });
+
+ it('deletes a template', async () => {
+ await testSubjects.existOrFail('templates-form-group');
+ const deleteButton = await find.byCssSelector('[data-test-subj*="-template-delete"]');
+
+ await deleteButton.click();
+
+ await testSubjects.existOrFail('confirm-delete-modal');
+
+ await testSubjects.click('confirmModalConfirmButton');
+
+ await testSubjects.missingOrFail('template-list');
+ });
});
});
};
diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts
index 584d4bc6507eb..478cb6d78f775 100644
--- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts
+++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts
@@ -115,7 +115,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
await deleteButton.click();
- await testSubjects.existOrFail('confirm-delete-custom-field-modal');
+ await testSubjects.existOrFail('confirm-delete-modal');
await testSubjects.click('confirmModalConfirmButton');
@@ -152,6 +152,52 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
expect(await testSubjects.getVisibleText('templates-list')).to.be('Template name\ntag-t1');
});
+
+ it('updates a template', async () => {
+ await testSubjects.existOrFail('templates-form-group');
+ const editButton = await find.byCssSelector('[data-test-subj*="-template-edit"]');
+
+ await editButton.click();
+
+ await testSubjects.setValue('template-name-input', 'Updated template name!');
+ await comboBox.setCustom('template-tags', 'tag-t1');
+ await testSubjects.setValue('template-description-input', 'Template description updated');
+
+ const caseTitle = await find.byCssSelector(
+ `[data-test-subj="input"][aria-describedby="caseTitle"]`
+ );
+ await caseTitle.focus();
+ await caseTitle.type('!!');
+
+ await cases.create.setDescription('test description!!');
+
+ await cases.create.setTags('case-tag');
+ await cases.create.setCategory('new!');
+
+ await testSubjects.click('common-flyout-save');
+ expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false);
+
+ await retry.waitFor('templates-list', async () => {
+ return await testSubjects.exists('templates-list');
+ });
+
+ expect(await testSubjects.getVisibleText('templates-list')).to.be(
+ 'Updated template name!\ntag-t1'
+ );
+ });
+
+ it('deletes a template', async () => {
+ await testSubjects.existOrFail('templates-form-group');
+ const deleteButton = await find.byCssSelector('[data-test-subj*="-template-delete"]');
+
+ await deleteButton.click();
+
+ await testSubjects.existOrFail('confirm-delete-modal');
+
+ await testSubjects.click('confirmModalConfirmButton');
+
+ await testSubjects.missingOrFail('template-list');
+ });
});
});
};