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 all 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
4 changes: 2 additions & 2 deletions x-pack/plugins/cases/common/constants/owners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export const OWNER_INFO: Record<Owner, RouteInfo> = {
[GENERAL_CASES_OWNER]: {
id: GENERAL_CASES_OWNER,
appId: 'management',
label: 'Stack',
iconType: 'casesApp',
label: 'Management',
iconType: 'managementApp',
appRoute: '/app/management/insightsAndAlerting',
validRuleConsumers: [AlertConsumers.ML, AlertConsumers.STACK_ALERTS, AlertConsumers.EXAMPLE],
},
Expand Down
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 @@ -40,113 +40,99 @@ describe('Assignees', () => {
});

it('renders', async () => {
const result = appMockRender.render(
appMockRender.render(
<MockHookWrapperComponent>
<Assignees isLoading={false} />
</MockHookWrapperComponent>
);

await waitFor(() => {
expect(result.getByTestId('comboBoxSearchInput')).not.toBeDisabled();
expect(screen.queryByTestId('comboBoxSearchInput')).not.toBeDisabled();
});

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

it('does not render the assign yourself link when the current user profile is undefined', async () => {
const spyOnGetCurrentUserProfile = jest.spyOn(api, 'getCurrentUserProfile');
spyOnGetCurrentUserProfile.mockResolvedValue(undefined as unknown as UserProfile);

const result = appMockRender.render(
appMockRender.render(
<MockHookWrapperComponent>
<Assignees isLoading={false} />
</MockHookWrapperComponent>
);

await waitFor(() => {
expect(result.getByTestId('comboBoxSearchInput')).not.toBeDisabled();
expect(screen.queryByTestId('comboBoxSearchInput')).not.toBeDisabled();
});

expect(result.queryByTestId('create-case-assign-yourself-link')).not.toBeInTheDocument();
expect(result.getByTestId('createCaseAssigneesComboBox')).toBeInTheDocument();
expect(screen.queryByTestId('create-case-assign-yourself-link')).not.toBeInTheDocument();
expect(await screen.findByTestId('createCaseAssigneesComboBox')).toBeInTheDocument();
});

it('selects the current user correctly', async () => {
const spyOnGetCurrentUserProfile = jest.spyOn(api, 'getCurrentUserProfile');
spyOnGetCurrentUserProfile.mockResolvedValue(currentUserProfile);

const result = appMockRender.render(
appMockRender.render(
<MockHookWrapperComponent>
<Assignees isLoading={false} />
</MockHookWrapperComponent>
);

await waitFor(() => {
expect(result.getByTestId('comboBoxSearchInput')).not.toBeDisabled();
expect(screen.queryByTestId('comboBoxSearchInput')).not.toBeDisabled();
});

act(() => {
userEvent.click(result.getByTestId('create-case-assign-yourself-link'));
});
userEvent.click(await screen.findByTestId('create-case-assign-yourself-link'));

await waitFor(() => {
expect(globalForm.getFormData()).toEqual({ assignees: [{ uid: currentUserProfile.uid }] });
});
expect(globalForm.getFormData()).toEqual({ assignees: [{ uid: currentUserProfile.uid }] });
});

it('disables the assign yourself button if the current user is already selected', async () => {
const spyOnGetCurrentUserProfile = jest.spyOn(api, 'getCurrentUserProfile');
spyOnGetCurrentUserProfile.mockResolvedValue(currentUserProfile);

const result = appMockRender.render(
appMockRender.render(
<MockHookWrapperComponent>
<Assignees isLoading={false} />
</MockHookWrapperComponent>
);

await waitFor(() => {
expect(result.getByTestId('comboBoxSearchInput')).not.toBeDisabled();
expect(screen.queryByTestId('comboBoxSearchInput')).not.toBeDisabled();
});

act(() => {
userEvent.click(result.getByTestId('create-case-assign-yourself-link'));
});
userEvent.click(await screen.findByTestId('create-case-assign-yourself-link'));

await waitFor(() => {
expect(globalForm.getFormData()).toEqual({ assignees: [{ uid: currentUserProfile.uid }] });
});

expect(result.getByTestId('create-case-assign-yourself-link')).toBeDisabled();
expect(await screen.findByTestId('create-case-assign-yourself-link')).toBeDisabled();
});

it('assignees users correctly', async () => {
const result = appMockRender.render(
appMockRender.render(
<MockHookWrapperComponent>
<Assignees isLoading={false} />
</MockHookWrapperComponent>
);

await waitFor(() => {
expect(result.getByTestId('comboBoxSearchInput')).not.toBeDisabled();
expect(screen.queryByTestId('comboBoxSearchInput')).not.toBeDisabled();
});

await act(async () => {
await userEvent.type(result.getByTestId('comboBoxSearchInput'), 'dr', { delay: 1 });
});
await userEvent.type(await screen.findByTestId('comboBoxSearchInput'), 'dr', { delay: 1 });

await waitFor(() => {
expect(
result.getByTestId('comboBoxOptionsList createCaseAssigneesComboBox-optionsList')
).toBeInTheDocument();
});
expect(
await screen.findByTestId('comboBoxOptionsList createCaseAssigneesComboBox-optionsList')
).toBeInTheDocument();

await waitFor(async () => {
expect(result.getByText(`${currentUserProfile.user.full_name}`)).toBeInTheDocument();
});
expect(await screen.findByText(`${currentUserProfile.user.full_name}`)).toBeInTheDocument();

act(() => {
userEvent.click(result.getByText(`${currentUserProfile.user.full_name}`));
});
userEvent.click(await screen.findByText(`${currentUserProfile.user.full_name}`));

await waitFor(() => {
expect(globalForm.getFormData()).toEqual({ assignees: [{ uid: currentUserProfile.uid }] });
Expand Down Expand Up @@ -185,25 +171,62 @@ describe('Assignees', () => {
);

await waitFor(() => {
expect(screen.getByTestId('comboBoxSearchInput')).not.toBeDisabled();
expect(screen.queryByTestId('comboBoxSearchInput')).not.toBeDisabled();
});

userEvent.click(await screen.findByTestId('comboBoxSearchInput'));

expect(await screen.findByText('Turtle')).toBeInTheDocument();
expect(await screen.findByText('turtle')).toBeInTheDocument();

userEvent.click(screen.getByText('Turtle'), undefined, { skipPointerEventsCheck: true });

// ensure that the similar user is still available for selection
expect(await screen.findByText('turtle')).toBeInTheDocument();
});

it('fetches the unknown user profiles using bulk_get', async () => {
// the profile is not returned by the suggest API
const userProfile = {
uid: 'u_qau3P4T1H-_f1dNHyEOWJzVkGQhLH1gnNMVvYxqmZcs_0',
enabled: true,
data: {},
user: {
username: 'uncertain_crawdad',
email: '[email protected]',
full_name: 'Uncertain Crawdad',
},
};

const spyOnBulkGetUserProfiles = jest.spyOn(api, 'bulkGetUserProfiles');
spyOnBulkGetUserProfiles.mockResolvedValue([userProfile]);

appMockRender.render(
<MockHookWrapperComponent>
<Assignees isLoading={false} />
</MockHookWrapperComponent>
);

expect(screen.queryByText(userProfile.user.full_name)).not.toBeInTheDocument();

act(() => {
userEvent.click(screen.getByTestId('comboBoxSearchInput'));
globalForm.setFieldValue('assignees', [{ uid: userProfile.uid }]);
});

await waitFor(() => {
expect(screen.getByText('Turtle')).toBeInTheDocument();
expect(screen.getByText('turtle')).toBeInTheDocument();
expect(globalForm.getFormData()).toEqual({
assignees: [{ uid: userProfile.uid }],
});
});

act(() => {
userEvent.click(screen.getByText('Turtle'), undefined, { skipPointerEventsCheck: true });
});

// ensure that the similar user is still available for selection
await waitFor(() => {
expect(screen.getByText('turtle')).toBeInTheDocument();
expect(spyOnBulkGetUserProfiles).toBeCalledTimes(1);
expect(spyOnBulkGetUserProfiles).toHaveBeenCalledWith({
security: expect.anything(),
uids: [userProfile.uid],
});
});

expect(await screen.findByText(userProfile.user.full_name)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { isEmpty } from 'lodash';
import { isEmpty, differenceWith } from 'lodash';
import React, { memo, useCallback, useState } from 'react';
import type { EuiComboBoxOptionOption } from '@elastic/eui';
import {
Expand All @@ -23,18 +23,22 @@ import type { FieldConfig, FieldHook } from '@kbn/es-ui-shared-plugin/static/for
import {
UseField,
getFieldValidityAndErrorMessage,
useFormData,
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import type { CaseAssignees } from '../../../common/types/domain';
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';
import { useIsUserTyping } from '../../common/use_is_user_typing';
import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles';

const FIELD_ID = 'assignees';

interface Props {
isLoading: boolean;
Expand Down Expand Up @@ -172,6 +176,7 @@ const AssigneesFieldComponent: React.FC<FieldProps> = React.memo(
}
isInvalid={isInvalid}
error={errorMessage}
data-test-subj="caseAssignees"
>
<EuiComboBox
fullWidth
Expand All @@ -195,6 +200,7 @@ AssigneesFieldComponent.displayName = 'AssigneesFieldComponent';

const AssigneesComponent: React.FC<Props> = ({ isLoading: isLoadingForm }) => {
const { owner: owners } = useCasesContext();
const [{ assignees }] = useFormData<{ assignees?: CaseAssignees }>({ watch: [FIELD_ID] });
const availableOwners = useAvailableCasesOwners(getAllPermissionsExceptFrom('delete'));
const [searchTerm, setSearchTerm] = useState('');
const { isUserTyping, onContentChange, onDebounce } = useIsUserTyping();
Expand All @@ -204,7 +210,7 @@ const AssigneesComponent: React.FC<Props> = ({ isLoading: isLoadingForm }) => {
useGetCurrentUserProfile();

const {
data: userProfiles,
data: userProfiles = [],
isLoading: isLoadingSuggest,
isFetching: isFetchingSuggest,
} = useSuggestUserProfiles({
Expand All @@ -213,10 +219,22 @@ const AssigneesComponent: React.FC<Props> = ({ isLoading: isLoadingForm }) => {
onDebounce,
});

const assigneesWithoutProfiles = differenceWith(
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
assignees ?? [],
userProfiles ?? [],
(assignee, userProfile) => assignee.uid === userProfile.uid
);

const { data: bulkUserProfiles = new Map(), isFetching: isLoadingBulkGetUserProfiles } =
useBulkGetUserProfiles({ uids: assigneesWithoutProfiles.map((assignee) => assignee.uid) });

const bulkUserProfilesAsArray = Array.from(bulkUserProfiles).map(([_, profile]) => profile);

const options =
bringCurrentUserToFrontAndSort(currentUserProfile, userProfiles)?.map((userProfile) =>
userProfileToComboBoxOption(userProfile)
) ?? [];
bringCurrentUserToFrontAndSort(currentUserProfile, [
...userProfiles,
...bulkUserProfilesAsArray,
])?.map((userProfile) => userProfileToComboBoxOption(userProfile)) ?? [];

const onSearchComboChange = (value: string) => {
if (!isEmpty(value)) {
Expand All @@ -229,15 +247,16 @@ const AssigneesComponent: React.FC<Props> = ({ isLoading: isLoadingForm }) => {
const isLoading =
isLoadingForm ||
isLoadingCurrentUserProfile ||
isLoadingBulkGetUserProfiles ||
isLoadingSuggest ||
isFetchingSuggest ||
isUserTyping;

const isDisabled = isLoadingForm || isLoadingCurrentUserProfile;
const isDisabled = isLoadingForm || isLoadingCurrentUserProfile || isLoadingBulkGetUserProfiles;

return (
<UseField
path="assignees"
path={FIELD_ID}
config={getConfig()}
component={AssigneesFieldComponent}
componentProps={{
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
Loading