Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Cases] Use templates when creating a case #185880

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
d6e897b
initial commit
js-jankisalvi Jun 10, 2024
9f75fe3
Move case field components to case_form_fields
cnasikas Jun 10, 2024
4092529
edit connector data
js-jankisalvi Jun 10, 2024
9dc8b57
Move fetching connectors to parent
cnasikas Jun 11, 2024
62dd288
Create template selector
cnasikas Jun 11, 2024
7c79f60
Show template selector and use the new custom fields component
cnasikas Jun 11, 2024
2fec6d5
Move removeEmptyFields to common utils
cnasikas Jun 11, 2024
6a9c9b8
Construct selected assignees from field value
cnasikas Jun 11, 2024
23bb707
Fill case values when selecting a template
cnasikas Jun 11, 2024
73d14b2
Fix tests and types
cnasikas Jun 11, 2024
f16fb39
Use connector component from case fields in templates
cnasikas Jun 11, 2024
92ccb80
Fix bug with connectors icons
cnasikas Jun 11, 2024
eaf57b7
add unit tests
js-jankisalvi Jun 11, 2024
161b165
Add tests
cnasikas Jun 11, 2024
fe52101
add more unit tests
js-jankisalvi Jun 12, 2024
94e3a47
add e2e test
js-jankisalvi Jun 12, 2024
bffa3a4
Merge branch 'main' into create_case_with_templates
cnasikas Jun 13, 2024
2880357
Use bulk get for unknown profiles
cnasikas Jun 13, 2024
8ebab81
remove watch for tags and assignees
js-jankisalvi Jun 13, 2024
8688582
fix tags tests changes, add defaultValue tests for custom fields
js-jankisalvi Jun 13, 2024
71831ef
fix incident types issue
js-jankisalvi Jun 13, 2024
75b6e6d
Merge branch 'main' into create_case_with_templates
cnasikas Jun 16, 2024
1a7f429
Fix layout and hide templates when multiple selectors
cnasikas Jun 16, 2024
e13535a
Merge branch 'feat/case_templates' into create_case_with_templates
cnasikas Jun 17, 2024
744262e
cleanup
js-jankisalvi Jun 17, 2024
abbc024
Merge branch 'feat/case_templates' into edit-delete-template
js-jankisalvi Jun 17, 2024
3471010
Fix bug with category
cnasikas Jun 17, 2024
65d58a3
Add tests
cnasikas Jun 17, 2024
4b48d88
Add tests
cnasikas Jun 17, 2024
10681d3
Merge branch 'edit-delete-template' into create_case_with_templates
cnasikas Jun 18, 2024
40b4c59
Respect configuration on connectors
cnasikas Jun 19, 2024
d3523c7
Merge branch 'create_case_with_templates' of github.com:cnasikas/kiba…
cnasikas Jun 19, 2024
feeffce
Use serializer for templates
cnasikas Jun 19, 2024
a0a5d83
Change stack solution icon and name
cnasikas Jun 19, 2024
10f1f17
Move getOwnerDefaultValue to util
cnasikas Jun 19, 2024
46aea17
Convert owner selector to dropdown
cnasikas Jun 19, 2024
5fe1b0b
Restructure code use the solution new dropdown
cnasikas Jun 19, 2024
97d4ac8
Merge branch 'feat/case_templates' into create_case_with_templates
cnasikas Jun 20, 2024
e84733c
Merge branch 'feat/case_templates' into create_case_with_templates
cnasikas Jun 20, 2024
74e5343
Change the way Jira fetches issues
cnasikas Jun 20, 2024
1a55af7
Refactor create case form
cnasikas Jun 20, 2024
3a1554f
Add headers to flyout
cnasikas Jun 20, 2024
a57d653
Add tests for utils.
adcoelho Jun 25, 2024
2e0b9e8
Fix owner bug.
adcoelho Jun 25, 2024
face183
Fix Jest tests.
adcoelho Jun 26, 2024
0a0a99e
Merge remote-tracking branch 'upstream/feat/case_templates' into crea…
adcoelho Jun 26, 2024
4a6d025
Fix remaining FTR tests.
adcoelho Jun 26, 2024
d64e063
Merge branch 'main' into create_case_with_templates
cnasikas Jun 26, 2024
172f0dd
Fix issue with infitive rerenders
cnasikas Jun 27, 2024
2d02d83
Merge branch 'feat/case_templates' into create_case_with_templates
cnasikas Jun 27, 2024
dcc6074
Remove uncesessary check in createFormDeserializer
cnasikas Jun 27, 2024
08e5da7
Use CaseFormFields in the create case form
cnasikas Jun 27, 2024
1e1d998
Fix bug with description local storage
cnasikas Jun 28, 2024
f784b51
Fix bug with required custom fields
cnasikas Jun 28, 2024
cae03b1
Use one common schema
cnasikas Jun 28, 2024
f1deec3
Fix types
cnasikas Jun 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion x-pack/plugins/cases/common/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ export type CasesConfigurationUI = Pick<
>;

export type CasesConfigurationUICustomField = CasesConfigurationUI['customFields'][number];

export type CasesConfigurationUITemplate = CasesConfigurationUI['templates'][number];

export type SortOrder = 'asc' | 'desc';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_l
import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { userProfiles } from '../../containers/user_profiles/api.mock';
import { Assignees } from './assignees';
import type { FormProps } from './schema';
import { act, waitFor, screen } from '@testing-library/react';
import * as api from '../../containers/user_profiles/api';
import type { UserProfile } from '@kbn/user-profile-components';
Expand All @@ -29,7 +28,7 @@ describe('Assignees', () => {
let appMockRender: AppMockRenderer;

const MockHookWrapperComponent: FC<PropsWithChildren<unknown>> = ({ children }) => {
const { form } = useForm<FormProps>();
const { form } = useForm();
globalForm = form;

return <Form form={form}>{children}</Form>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import { MAX_ASSIGNEES_PER_CASE } from '../../../common/constants';
import { useSuggestUserProfiles } from '../../containers/user_profiles/use_suggest_user_profiles';
import { useCasesContext } from '../cases_context/use_cases_context';
import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile';
import { OptionalFieldLabel } from './optional_field_label';
import * as i18n from './translations';
import { OptionalFieldLabel } from '../optional_field_label';
import * as i18n from '../create/translations';
import { bringCurrentUserToFrontAndSort } from '../user_profiles/sort';
import { useAvailableCasesOwners } from '../app/use_available_owners';
import { getAllPermissionsExceptFrom } from '../../utils/permissions';
Expand All @@ -40,14 +40,14 @@ interface Props {
isLoading: boolean;
}

type UserProfileComboBoxOption = EuiComboBoxOptionOption<string> & UserProfileWithAvatar;

interface FieldProps {
field: FieldHook;
options: EuiComboBoxOptionOption[];
field: FieldHook<CaseAssignees>;
options: UserProfileComboBoxOption[];
isLoading: boolean;
isDisabled: boolean;
currentUserProfile?: UserProfile;
selectedOptions: EuiComboBoxOptionOption[];
setSelectedOptions: React.Dispatch<React.SetStateAction<EuiComboBoxOptionOption[]>>;
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
onSearchComboChange: (value: string) => void;
}

Expand All @@ -73,91 +73,84 @@ const userProfileToComboBoxOption = (userProfile: UserProfileWithAvatar) => ({
data: userProfile.data,
});

const comboBoxOptionToAssignee = (option: EuiComboBoxOptionOption) => ({ uid: option.value });
const comboBoxOptionToAssignee = (option: EuiComboBoxOptionOption<string>) => ({
uid: option.value ?? '',
});

const AssigneesFieldComponent: React.FC<FieldProps> = React.memo(
({
field,
isLoading,
isDisabled,
options,
currentUserProfile,
selectedOptions,
setSelectedOptions,
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
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<EuiComboBoxOptionOption<string>>) => {
setValue(currentOptions.map((option) => comboBoxOptionToAssignee(option)));
},
[setSelectedOptions, setValue]
[setValue]
);

const onSelfAssign = useCallback(() => {
if (!currentUserProfile) {
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<string> & 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 (
<EuiFlexGroup
alignItems="center"
justifyContent="flexStart"
gutterSize="s"
responsive={false}
>
<EuiFlexItem grow={false}>
<UserAvatar user={user} avatar={data.avatar} size="s" />
</EuiFlexItem>
<EuiFlexGroup
alignItems="center"
justifyContent="flexStart"
gutterSize="s"
justifyContent="spaceBetween"
gutterSize="none"
responsive={false}
>
<EuiFlexItem grow={false}>
<UserAvatar user={user} avatar={data.avatar} size="s" />
<EuiFlexItem>
<EuiHighlight search={searchValue} className={contentClassName}>
{displayName}
</EuiHighlight>
</EuiFlexItem>
<EuiFlexGroup
alignItems="center"
justifyContent="spaceBetween"
gutterSize="none"
responsive={false}
>
<EuiFlexItem>
<EuiHighlight search={searchValue} className={contentClassName}>
{displayName}
</EuiHighlight>
{user.email && user.email !== displayName ? (
<EuiFlexItem grow={false}>
<EuiTextColor color={'subdued'}>
<EuiHighlight search={searchValue} className={contentClassName}>
{user.email}
</EuiHighlight>
</EuiTextColor>
</EuiFlexItem>
{user.email && user.email !== displayName ? (
<EuiFlexItem grow={false}>
<EuiTextColor color={'subdued'}>
<EuiHighlight search={searchValue} className={contentClassName}>
{user.email}
</EuiHighlight>
</EuiTextColor>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
) : null}
</EuiFlexGroup>
);
},
[]
);
</EuiFlexGroup>
);
}, []);

const isCurrentUserSelected = Boolean(
selectedOptions?.find((option) => option.value === currentUserProfile?.uid)
selectedAssignees?.find((assignee) => assignee.uid === currentUserProfile?.uid)
);

return (
Expand Down Expand Up @@ -204,7 +197,6 @@ const AssigneesComponent: React.FC<Props> = ({ isLoading: isLoadingForm }) => {
const { owner: owners } = useCasesContext();
const availableOwners = useAvailableCasesOwners(getAllPermissionsExceptFrom('delete'));
const [searchTerm, setSearchTerm] = useState('');
const [selectedOptions, setSelectedOptions] = useState<EuiComboBoxOptionOption[]>();
const { isUserTyping, onContentChange, onDebounce } = useIsUserTyping();
const hasOwners = owners.length > 0;

Expand Down Expand Up @@ -251,8 +243,6 @@ const AssigneesComponent: React.FC<Props> = ({ isLoading: isLoadingForm }) => {
componentProps={{
isLoading,
isDisabled,
selectedOptions,
setSelectedOptions,
options,
onSearchComboChange,
currentUserProfile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import type { FormProps } from './schema';
import type { AppMockRenderer } from '../../common/mock';
import { createAppMockRenderer } from '../../common/mock';
import { Category } from './category';
Expand All @@ -28,7 +27,7 @@ describe('Category', () => {
const onSubmit = jest.fn();

const FormComponent: FC<PropsWithChildren<unknown>> = ({ children }) => {
const { form } = useForm<FormProps>({ onSubmit });
const { form } = useForm({ onSubmit });

return (
<Form form={form}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import React, { memo } from 'react';
import { useGetCategories } from '../../containers/use_get_categories';
import { CategoryFormField } from '../category/category_form_field';
import { OptionalFieldLabel } from './optional_field_label';
import { OptionalFieldLabel } from '../optional_field_label';

interface Props {
isLoading: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_ty
import { useGetSeverity } from '../connectors/resilient/use_get_severity';
import { useGetChoices } from '../connectors/servicenow/use_get_choices';
import { incidentTypes, severity, choices } from '../connectors/mock';
import type { FormProps } from './schema';
import { schema } from './schema';
import type { CreateCaseFormSchema } from '../create/schema';
import { schema } from '../create/schema';
import type { AppMockRenderer } from '../../common/mock';
import {
noConnectorsCasePermission,
Expand Down Expand Up @@ -60,14 +60,15 @@ const defaultProps = {
connectors: connectorsMock,
isLoading: false,
isLoadingConnectors: false,
configurationConnector: useCaseConfigureResponse.data.connector,
};

describe('Connector', () => {
let appMockRender: AppMockRenderer;
let globalForm: FormHook;

const MockHookWrapperComponent: FC<PropsWithChildren<unknown>> = ({ children }) => {
const { form } = useForm<FormProps>({
const { form } = useForm<CreateCaseFormSchema>({
defaultValue: { connectorId: connectorsMock[0].id, fields: null },
schema: {
connectorId: schema.connectorId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,41 @@
* 2.0.
*/

import React, { memo, useMemo } from 'react';
import React, { memo, useEffect, useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';

import type { FieldConfig } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { UseField, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import {
UseField,
useFormData,
useFormContext,
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import type { ActionConnector } from '../../../common/types/domain';
import { ConnectorSelector } from '../connector_selector/form';
import { ConnectorFieldsForm } from '../connectors/fields_form';
import { schema } from './schema';
import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration';
import { schema } from '../create/schema';
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved
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';

interface Props {
connectors: ActionConnector[];
isLoading: boolean;
isLoadingConnectors: boolean;
configurationConnector: CasesConfigurationUI['connector'];
}

const ConnectorComponent: React.FC<Props> = ({ connectors, isLoading, isLoadingConnectors }) => {
const ConnectorComponent: React.FC<Props> = ({
connectors,
isLoading,
isLoadingConnectors,
configurationConnector,
}) => {
const [{ connectorId }] = useFormData({ watch: ['connectorId'] });
const { setFieldValue } = useFormContext();
const connector = getConnectorById(connectorId, connectors) ?? null;

const {
data: { connector: configurationConnector },
} = useGetCaseConfiguration();

const { actions } = useApplicationCapabilities();
const { permissions } = useCasesContext();
const hasReadPermissions = permissions.connectors && actions.read;
Expand All @@ -49,6 +55,10 @@ const ConnectorComponent: React.FC<Props> = ({ connectors, isLoading, isLoadingC
connectors,
});

useEffect(() => {
setFieldValue('connectorId', configurationConnector.id);
}, [configurationConnector.id, setFieldValue]);
cnasikas marked this conversation as resolved.
Show resolved Hide resolved

if (!hasReadPermissions) {
return (
<EuiText data-test-subj="create-case-connector-permissions-error-msg" size="s">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import * as i18n from './translations';

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

const CustomFieldsComponent: React.FC<Props> = ({
isLoading,
setCustomFieldsOptional,
setCustomFieldsOptional = false,
configurationCustomFields,
}) => {
const sortedCustomFields = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { Description } from './description';
import { schema } from './schema';
import { schema } from '../create/schema';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this schema be imported from x-pack/plugins/cases/public/components/case_form_fields/schema.tsx?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That applies only to the templates and doesn't throw an error when the description is empty.

Line 73 needs to test that 👍 .

import type { AppMockRenderer } from '../../common/mock';
import { createAppMockRenderer } from '../../common/mock';
import { MAX_DESCRIPTION_LENGTH } from '../../../common/constants';
Expand Down
18 changes: 6 additions & 12 deletions x-pack/plugins/cases/public/components/case_form_fields/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@

import React, { memo } from 'react';
import { EuiFlexGroup } from '@elastic/eui';
import { Title } from '../create/title';
import { Tags } from '../create/tags';
import { Category } from '../create/category';
import { Severity } from '../create/severity';
import { Description } from '../create/description';
import { Title } from './title';
import { Tags } from './tags';
import { Category } from './category';
import { Severity } from './severity';
import { Description } from './description';
import { useCasesFeatures } from '../../common/use_cases_features';
import { Assignees } from '../create/assignees';
import { Assignees } from './assignees';
import { CustomFields } from './custom_fields';
import type { CasesConfigurationUI } from '../../containers/types';

Expand All @@ -33,17 +33,11 @@ const CaseFormFieldsComponent: React.FC<Props> = ({
return (
<EuiFlexGroup data-test-subj="case-form-fields" direction="column">
<Title isLoading={isLoading} />

{caseAssignmentAuthorized ? <Assignees isLoading={isLoading} /> : null}

<Tags isLoading={isLoading} />

<Category isLoading={isLoading} />

<Severity isLoading={isLoading} />

<Description isLoading={isLoading} />

<CustomFields
isLoading={isLoading}
setCustomFieldsOptional={setCustomFieldsOptional}
Expand Down
Loading