mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[118684] Add ability for case owner selection in flyout (#120150)
* [118684] Add ability for case owner selection in flyout * Update Flyout to handle permissions internally * Fix failing tests * Code review changes * Fix tests * Update test provider to use merge * Add PR suggestions * further PR suggestions * Update language from case type to solution * Fixes from code review * Add key field for iteration and default to securitySolution * final revisions Co-authored-by: Kristof-Pierre Cummings <kristofpierre.cummings@elastic.co>
This commit is contained in:
parent
03f5c3b34c
commit
b9c563c41c
11 changed files with 461 additions and 23 deletions
|
@ -80,13 +80,19 @@ export const SUPPORTED_CONNECTORS = [
|
|||
export const MAX_ALERTS_PER_SUB_CASE = 5000;
|
||||
export const MAX_GENERATED_ALERTS_PER_SUB_CASE = 50;
|
||||
|
||||
/**
|
||||
* This must be the same value that the security solution plugin uses to define the case kind when it registers the
|
||||
* feature for the 7.13 migration only.
|
||||
*
|
||||
* This variable is being also used by test files and mocks.
|
||||
*/
|
||||
export const SECURITY_SOLUTION_OWNER = 'securitySolution';
|
||||
export const OBSERVABILITY_OWNER = 'observability';
|
||||
|
||||
export const OWNER_INFO = {
|
||||
[SECURITY_SOLUTION_OWNER]: {
|
||||
label: 'Security',
|
||||
iconType: 'logoSecurity',
|
||||
},
|
||||
[OBSERVABILITY_OWNER]: {
|
||||
label: 'Observability',
|
||||
iconType: 'logoObservability',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This flag governs enabling the case as a connector feature. It is disabled by default as the feature is not complete.
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { merge } from 'lodash';
|
||||
import React from 'react';
|
||||
import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import { DEFAULT_FEATURES, SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
import { CasesFeatures } from '../../../common/ui/types';
|
||||
import { CasesProvider } from '../../components/cases_context';
|
||||
|
@ -20,6 +21,7 @@ interface Props {
|
|||
children: React.ReactNode;
|
||||
userCanCrud?: boolean;
|
||||
features?: CasesFeatures;
|
||||
owner?: string[];
|
||||
}
|
||||
|
||||
window.scrollTo = jest.fn();
|
||||
|
@ -28,8 +30,9 @@ const MockKibanaContextProvider = createKibanaContextProviderMock();
|
|||
/** A utility for wrapping children in the providers required to run most tests */
|
||||
const TestProvidersComponent: React.FC<Props> = ({
|
||||
children,
|
||||
features,
|
||||
owner = [SECURITY_SOLUTION_OWNER],
|
||||
userCanCrud = true,
|
||||
features = {},
|
||||
}) => {
|
||||
/**
|
||||
* The empty object at the beginning avoids the mutation
|
||||
|
@ -40,9 +43,7 @@ const TestProvidersComponent: React.FC<Props> = ({
|
|||
<I18nProvider>
|
||||
<MockKibanaContextProvider>
|
||||
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
|
||||
<CasesProvider
|
||||
value={{ owner: [SECURITY_SOLUTION_OWNER], userCanCrud, features: featuresOptions }}
|
||||
>
|
||||
<CasesProvider value={{ features: featuresOptions, owner, userCanCrud }}>
|
||||
{children}
|
||||
</CasesProvider>
|
||||
</ThemeProvider>
|
||||
|
|
|
@ -56,6 +56,20 @@ export const DESCRIPTION_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SOLUTION_REQUIRED = i18n.translate(
|
||||
'xpack.cases.createCase.solutionFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'A solution is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const ARIA_KEYPAD_LEGEND = i18n.translate(
|
||||
'xpack.cases.createCase.ariaKeypadSolutionSelection',
|
||||
{
|
||||
defaultMessage: 'Single solution select',
|
||||
}
|
||||
);
|
||||
|
||||
export const COMMENT_REQUIRED = i18n.translate('xpack.cases.caseView.commentFieldRequiredError', {
|
||||
defaultMessage: 'A comment is required.',
|
||||
});
|
||||
|
@ -108,6 +122,10 @@ export const TAGS = i18n.translate('xpack.cases.caseView.tags', {
|
|||
defaultMessage: 'Tags',
|
||||
});
|
||||
|
||||
export const SOLUTION = i18n.translate('xpack.cases.caseView.solution', {
|
||||
defaultMessage: 'Solution',
|
||||
});
|
||||
|
||||
export const ACTIONS = i18n.translate('xpack.cases.allCases.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
});
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { OBSERVABILITY_OWNER } from '../../../common/constants';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { useAvailableCasesOwners } from './use_available_owners';
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
|
||||
|
||||
const hasAll = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: true,
|
||||
read_cases: true,
|
||||
},
|
||||
observabilityCases: {
|
||||
crud_cases: true,
|
||||
read_cases: true,
|
||||
},
|
||||
};
|
||||
|
||||
const hasSecurityAsCrudAndObservabilityAsRead = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: true,
|
||||
},
|
||||
observabilityCases: {
|
||||
read_cases: true,
|
||||
},
|
||||
};
|
||||
|
||||
const unrelatedFeatures = {
|
||||
bogusCapability: {
|
||||
crud_cases: true,
|
||||
read_cases: true,
|
||||
},
|
||||
};
|
||||
|
||||
const mockKibana = (permissionType: unknown = hasAll) => {
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
capabilities: permissionType,
|
||||
},
|
||||
},
|
||||
} as unknown as ReturnType<typeof useKibana>);
|
||||
};
|
||||
|
||||
describe('useAvailableCasesOwners correctly grabs user case permissions', () => {
|
||||
it('returns all available owner types if user has access to all', () => {
|
||||
mockKibana();
|
||||
const { result } = renderHook(useAvailableCasesOwners);
|
||||
|
||||
expect(result.current).toEqual([SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER]);
|
||||
});
|
||||
|
||||
it('returns no owner types if user has access to none', () => {
|
||||
mockKibana({});
|
||||
const { result } = renderHook(useAvailableCasesOwners);
|
||||
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns only the permission it should have with CRUD as default', () => {
|
||||
mockKibana(hasSecurityAsCrudAndObservabilityAsRead);
|
||||
const { result } = renderHook(useAvailableCasesOwners);
|
||||
|
||||
expect(result.current).toEqual([SECURITY_SOLUTION_OWNER]);
|
||||
});
|
||||
|
||||
it('returns only the permission it should have with READ as default', () => {
|
||||
mockKibana(hasSecurityAsCrudAndObservabilityAsRead);
|
||||
const { result } = renderHook(() => useAvailableCasesOwners('read'));
|
||||
|
||||
expect(result.current).toEqual([OBSERVABILITY_OWNER]);
|
||||
});
|
||||
|
||||
it('returns no owners when the capabilities does not contain valid entries', () => {
|
||||
mockKibana(unrelatedFeatures);
|
||||
const { result } = renderHook(useAvailableCasesOwners);
|
||||
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param level : 'crud' | 'read' (default: 'crud')
|
||||
*
|
||||
* `securitySolution` owner uses cases capability feature id: 'securitySolutionCases'; //owner
|
||||
* `observability` owner uses cases capability feature id: 'observabilityCases';
|
||||
* both solutions use `crud_cases` and `read_cases` capability names
|
||||
**/
|
||||
|
||||
export const useAvailableCasesOwners = (level: 'crud' | 'read' = 'crud'): string[] => {
|
||||
const { capabilities } = useKibana().services.application;
|
||||
const capabilityName = `${level}_cases`;
|
||||
return Object.entries(capabilities).reduce(
|
||||
(availableOwners: string[], [featureId, capability]) => {
|
||||
if (featureId.endsWith('Cases') && !!capability[capabilityName]) {
|
||||
availableOwners.push(featureId.replace('Cases', ''));
|
||||
}
|
||||
return availableOwners;
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
|
@ -23,6 +23,9 @@ jest.mock('../../containers/use_get_tags');
|
|||
jest.mock('../../containers/configure/use_connectors');
|
||||
jest.mock('../../containers/configure/use_configure');
|
||||
jest.mock('../markdown_editor/plugins/lens/use_lens_draft_comment');
|
||||
jest.mock('../app/use_available_owners', () => ({
|
||||
useAvailableCasesOwners: () => ['securitySolution', 'observability'],
|
||||
}));
|
||||
|
||||
const useGetTagsMock = useGetTags as jest.Mock;
|
||||
const useConnectorsMock = useConnectors as jest.Mock;
|
||||
|
@ -90,7 +93,7 @@ describe('CreateCaseForm', () => {
|
|||
expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('it renders all form fields', async () => {
|
||||
it('it renders all form fields except case selection', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseForm {...casesFormProps} />
|
||||
|
@ -102,6 +105,22 @@ describe('CreateCaseForm', () => {
|
|||
expect(wrapper.find(`[data-test-subj="caseDescription"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseSyncAlerts"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseOwnerSelector"]`).exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders all form fields including case selection if has permissions and no owner', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent testProviderProps={{ owner: [] }}>
|
||||
<CreateCaseForm {...casesFormProps} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="caseTitle"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseTags"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseDescription"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseSyncAlerts"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="caseOwnerSelector"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('hides the sync alerts toggle', () => {
|
||||
|
|
|
@ -31,6 +31,9 @@ import { UsePostComment } from '../../containers/use_post_comment';
|
|||
import { SubmitCaseButton } from './submit_button';
|
||||
import { FormContext } from './form_context';
|
||||
import { useCasesFeatures } from '../cases_context/use_cases_features';
|
||||
import { CreateCaseOwnerSelector } from './owner_selector';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { useAvailableCasesOwners } from '../app/use_available_owners';
|
||||
|
||||
interface ContainerProps {
|
||||
big?: boolean;
|
||||
|
@ -70,6 +73,10 @@ export const CreateCaseFormFields: React.FC<CreateCaseFormFieldsProps> = React.m
|
|||
const { isSubmitting } = useFormContext();
|
||||
const { isSyncAlertsEnabled } = useCasesFeatures();
|
||||
|
||||
const { owner } = useCasesContext();
|
||||
const availableOwners = useAvailableCasesOwners();
|
||||
const canShowCaseSolutionSelection = !owner.length && availableOwners.length;
|
||||
|
||||
const firstStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_ONE_TITLE,
|
||||
|
@ -79,13 +86,21 @@ export const CreateCaseFormFields: React.FC<CreateCaseFormFieldsProps> = React.m
|
|||
<Container>
|
||||
<Tags isLoading={isSubmitting} />
|
||||
</Container>
|
||||
{canShowCaseSolutionSelection && (
|
||||
<Container big>
|
||||
<CreateCaseOwnerSelector
|
||||
availableOwners={availableOwners}
|
||||
isLoading={isSubmitting}
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
<Container big>
|
||||
<Description isLoading={isSubmitting} />
|
||||
</Container>
|
||||
</>
|
||||
),
|
||||
}),
|
||||
[isSubmitting]
|
||||
[isSubmitting, canShowCaseSolutionSelection, availableOwners]
|
||||
);
|
||||
|
||||
const secondStep = useMemo(
|
||||
|
@ -156,12 +171,7 @@ export const CreateCaseForm: React.FC<CreateCaseFormProps> = React.memo(
|
|||
timelineIntegration,
|
||||
}) => (
|
||||
<CasesTimelineIntegrationProvider timelineIntegration={timelineIntegration}>
|
||||
<FormContext
|
||||
afterCaseCreated={afterCaseCreated}
|
||||
caseType={caseType}
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
onSuccess={onSuccess}
|
||||
>
|
||||
<FormContext afterCaseCreated={afterCaseCreated} caseType={caseType} onSuccess={onSuccess}>
|
||||
<CreateCaseFormFields
|
||||
connectors={empty}
|
||||
isLoadingConnectors={false}
|
||||
|
|
|
@ -27,13 +27,13 @@ const initialCaseValue: FormProps = {
|
|||
connectorId: 'none',
|
||||
fields: null,
|
||||
syncAlerts: true,
|
||||
selectedOwner: null,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
afterCaseCreated?: (theCase: Case, postComment: UsePostComment['postComment']) => Promise<void>;
|
||||
caseType?: CaseType;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
onSuccess?: (theCase: Case) => Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,6 @@ export const FormContext: React.FC<Props> = ({
|
|||
afterCaseCreated,
|
||||
caseType = CaseType.individual,
|
||||
children,
|
||||
hideConnectorServiceNowSir,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const { connectors, loading: isLoadingConnectors } = useConnectors();
|
||||
|
@ -62,6 +61,7 @@ export const FormContext: React.FC<Props> = ({
|
|||
isValid
|
||||
) => {
|
||||
if (isValid) {
|
||||
const { selectedOwner, ...userFormData } = dataWithoutConnectorId;
|
||||
const caseConnector = getConnectorById(dataConnectorId, connectors);
|
||||
|
||||
const connectorToUpdate = caseConnector
|
||||
|
@ -69,11 +69,11 @@ export const FormContext: React.FC<Props> = ({
|
|||
: getNoneConnector();
|
||||
|
||||
const updatedCase = await postCase({
|
||||
...dataWithoutConnectorId,
|
||||
...userFormData,
|
||||
type: caseType,
|
||||
connector: connectorToUpdate,
|
||||
settings: { syncAlerts },
|
||||
owner: owner[0],
|
||||
owner: selectedOwner ?? owner[0],
|
||||
});
|
||||
|
||||
if (afterCaseCreated && updatedCase) {
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { OBSERVABILITY_OWNER } from '../../../common/constants';
|
||||
import { useForm, Form, FormHook } from '../../common/shared_imports';
|
||||
import { CreateCaseOwnerSelector } from './owner_selector';
|
||||
import { schema, FormProps } from './schema';
|
||||
|
||||
describe('Case Owner Selection', () => {
|
||||
let globalForm: FormHook;
|
||||
|
||||
const MockHookWrapperComponent: React.FC = ({ children }) => {
|
||||
const { form } = useForm<FormProps>({
|
||||
defaultValue: { selectedOwner: '' },
|
||||
schema: {
|
||||
selectedOwner: schema.selectedOwner,
|
||||
},
|
||||
});
|
||||
|
||||
globalForm = form;
|
||||
|
||||
return <Form form={form}>{children}</Form>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseOwnerSelector availableOwners={[SECURITY_SOLUTION_OWNER]} isLoading={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="caseOwnerSelector"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER],
|
||||
[SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER],
|
||||
])('disables %s button if user only has %j', (disabledButton, permission) => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseOwnerSelector availableOwners={[permission]} isLoading={false} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="${disabledButton}RadioButton"] input`).first().props().disabled
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="${permission}RadioButton"] input`).first().props().disabled
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="${permission}RadioButton"] input`).first().props().checked
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('defaults to security Solution', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseOwnerSelector
|
||||
availableOwners={[OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER]}
|
||||
isLoading={false}
|
||||
/>
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="observabilityRadioButton"] input`).first().props().checked
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="securitySolutionRadioButton"] input`).first().props().checked
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it changes the selection', async () => {
|
||||
const wrapper = mount(
|
||||
<MockHookWrapperComponent>
|
||||
<CreateCaseOwnerSelector
|
||||
availableOwners={[OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER]}
|
||||
isLoading={false}
|
||||
/>
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find(`[data-test-subj="observabilityRadioButton"] input`)
|
||||
.first()
|
||||
.simulate('change', OBSERVABILITY_OWNER);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="observabilityRadioButton"] input`).first().props().checked
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="securitySolutionRadioButton"] input`).first().props().checked
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
expect(globalForm.getFormData()).toEqual({ selectedOwner: OBSERVABILITY_OWNER });
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find(`[data-test-subj="securitySolutionRadioButton"] input`)
|
||||
.first()
|
||||
.simulate('change', SECURITY_SOLUTION_OWNER);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="securitySolutionRadioButton"] input`).first().props().checked
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="observabilityRadioButton"] input`).first().props().checked
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
expect(globalForm.getFormData()).toEqual({ selectedOwner: SECURITY_SOLUTION_OWNER });
|
||||
});
|
||||
});
|
117
x-pack/plugins/cases/public/components/create/owner_selector.tsx
Normal file
117
x-pack/plugins/cases/public/components/create/owner_selector.tsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIcon,
|
||||
EuiKeyPadMenu,
|
||||
EuiKeyPadMenuItem,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { OBSERVABILITY_OWNER, OWNER_INFO } from '../../../common/constants';
|
||||
|
||||
import { FieldHook, getFieldValidityAndErrorMessage, UseField } from '../../common/shared_imports';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface OwnerSelectorProps {
|
||||
field: FieldHook<string>;
|
||||
isLoading: boolean;
|
||||
availableOwners: string[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
availableOwners: string[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SELECTABLE_OWNERS = [SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER] as const;
|
||||
|
||||
const FIELD_NAME = 'selectedOwner';
|
||||
|
||||
const FullWidthKeyPadMenu = euiStyled(EuiKeyPadMenu)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const FullWidthKeyPadItem = euiStyled(EuiKeyPadMenuItem)`
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const OwnerSelector = ({
|
||||
availableOwners,
|
||||
field,
|
||||
isLoading = false,
|
||||
}: OwnerSelectorProps): JSX.Element => {
|
||||
const { errorMessage, isInvalid } = getFieldValidityAndErrorMessage(field);
|
||||
const radioGroupName = useGeneratedHtmlId({ prefix: 'caseOwnerRadioGroup' });
|
||||
|
||||
const onChange = useCallback((val: string) => field.setValue(val), [field]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!field.value) {
|
||||
onChange(
|
||||
availableOwners.includes(SECURITY_SOLUTION_OWNER)
|
||||
? SECURITY_SOLUTION_OWNER
|
||||
: availableOwners[0]
|
||||
);
|
||||
}
|
||||
}, [availableOwners, field.value, onChange]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
data-test-subj="caseOwnerSelector"
|
||||
fullWidth
|
||||
isInvalid={isInvalid}
|
||||
error={errorMessage}
|
||||
helpText={field.helpText}
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
>
|
||||
<FullWidthKeyPadMenu checkable={{ ariaLegend: i18n.ARIA_KEYPAD_LEGEND }}>
|
||||
<EuiFlexGroup>
|
||||
{DEFAULT_SELECTABLE_OWNERS.map((owner) => (
|
||||
<EuiFlexItem key={owner}>
|
||||
<FullWidthKeyPadItem
|
||||
data-test-subj={`${owner}RadioButton`}
|
||||
onChange={onChange}
|
||||
checkable="single"
|
||||
name={radioGroupName}
|
||||
id={owner}
|
||||
label={OWNER_INFO[owner].label}
|
||||
isSelected={field.value === owner}
|
||||
isDisabled={isLoading || !availableOwners.includes(owner)}
|
||||
>
|
||||
<EuiIcon type={OWNER_INFO[owner].iconType} size="xl" />
|
||||
</FullWidthKeyPadItem>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</FullWidthKeyPadMenu>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
const CaseOwnerSelector: React.FC<Props> = ({ availableOwners, isLoading }) => {
|
||||
return (
|
||||
<UseField
|
||||
path={FIELD_NAME}
|
||||
component={OwnerSelector}
|
||||
componentProps={{ availableOwners, isLoading }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
CaseOwnerSelector.displayName = 'CaseOwnerSelectionComponent';
|
||||
|
||||
export const CreateCaseOwnerSelector = memo(CaseOwnerSelector);
|
|
@ -36,6 +36,7 @@ export type FormProps = Omit<CasePostRequest, 'connector' | 'settings' | 'owner'
|
|||
connectorId: string;
|
||||
fields: ConnectorTypeFields['fields'];
|
||||
syncAlerts: boolean;
|
||||
selectedOwner?: string | null;
|
||||
};
|
||||
|
||||
export const schema: FormSchema<FormProps> = {
|
||||
|
@ -62,6 +63,15 @@ export const schema: FormSchema<FormProps> = {
|
|||
},
|
||||
],
|
||||
},
|
||||
selectedOwner: {
|
||||
label: i18n.SOLUTION,
|
||||
type: FIELD_TYPES.RADIO_GROUP,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.SOLUTION_REQUIRED),
|
||||
},
|
||||
],
|
||||
},
|
||||
tags: schemaTags,
|
||||
connectorId: {
|
||||
type: FIELD_TYPES.SUPER_SELECT,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue