mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[SECURITY SOLUTIONS] Bug case connector (#93104)
* bring back case connector to design * disable connector sir in collection * missing to only create collection type * fix fields connector when you need to hide service-now sir
This commit is contained in:
parent
90976ee119
commit
2903844dd1
22 changed files with 568 additions and 219 deletions
|
@ -447,6 +447,9 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
caseFields={caseData.connector.fields}
|
||||
connectors={connectors}
|
||||
disabled={!userCanCrud}
|
||||
hideConnectorServiceNowSir={
|
||||
subCaseId != null || caseData.type === CaseType.collection
|
||||
}
|
||||
isLoading={isLoadingConnectors || (isLoading && updateKey === 'connector')}
|
||||
onSubmit={onSubmitConnector}
|
||||
selectedConnector={caseData.connector.id}
|
||||
|
|
|
@ -34,22 +34,122 @@ describe('ConnectorsDropdown', () => {
|
|||
test('it formats the connectors correctly', () => {
|
||||
const selectProps = wrapper.find(EuiSuperSelect).props();
|
||||
|
||||
expect(selectProps.options).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: 'none',
|
||||
'data-test-subj': 'dropdown-connector-no-connector',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value: 'servicenow-1',
|
||||
'data-test-subj': 'dropdown-connector-servicenow-1',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value: 'resilient-2',
|
||||
'data-test-subj': 'dropdown-connector-resilient-2',
|
||||
}),
|
||||
])
|
||||
);
|
||||
expect(selectProps.options).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"data-test-subj": "dropdown-connector-no-connector",
|
||||
"inputDisplay": <EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<Styled(EuiIcon)
|
||||
size="m"
|
||||
type="minusInCircle"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<span
|
||||
data-test-subj="dropdown-connector-no-connector"
|
||||
>
|
||||
No connector selected
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
"value": "none",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "dropdown-connector-servicenow-1",
|
||||
"inputDisplay": <EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<Styled(EuiIcon)
|
||||
size="m"
|
||||
type="test-file-stub"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<span>
|
||||
My Connector
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
"value": "servicenow-1",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "dropdown-connector-resilient-2",
|
||||
"inputDisplay": <EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<Styled(EuiIcon)
|
||||
size="m"
|
||||
type="test-file-stub"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<span>
|
||||
My Connector 2
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
"value": "resilient-2",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "dropdown-connector-jira-1",
|
||||
"inputDisplay": <EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<Styled(EuiIcon)
|
||||
size="m"
|
||||
type="test-file-stub"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<span>
|
||||
Jira
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
"value": "jira-1",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "dropdown-connector-servicenow-sir",
|
||||
"inputDisplay": <EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<Styled(EuiIcon)
|
||||
size="m"
|
||||
type="test-file-stub"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<span>
|
||||
My Connector SIR
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
"value": "servicenow-sir",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('it disables the dropdown', () => {
|
||||
|
@ -79,4 +179,25 @@ describe('ConnectorsDropdown', () => {
|
|||
|
||||
expect(newWrapper.find('button span:not([data-euiicon-type])').text()).toEqual('My Connector');
|
||||
});
|
||||
|
||||
test('if the props hideConnectorServiceNowSir is true, the connector should not be part of the list of options ', () => {
|
||||
const newWrapper = mount(
|
||||
<ConnectorsDropdown
|
||||
{...props}
|
||||
selectedConnector={'servicenow-1'}
|
||||
hideConnectorServiceNowSir={true}
|
||||
/>,
|
||||
{
|
||||
wrappingComponent: TestProviders,
|
||||
}
|
||||
);
|
||||
const selectProps = newWrapper.find(EuiSuperSelect).props();
|
||||
const options = selectProps.options as Array<{ 'data-test-subj': string }>;
|
||||
expect(
|
||||
options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-1')
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-sir')
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import React, { useMemo } from 'react';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ConnectorTypes } from '../../../../../case/common/api';
|
||||
import { ActionConnector } from '../../containers/configure/types';
|
||||
import { connectorsConfiguration } from '../connectors';
|
||||
import * as i18n from './translations';
|
||||
|
@ -20,6 +21,7 @@ export interface Props {
|
|||
onChange: (id: string) => void;
|
||||
selectedConnector: string;
|
||||
appendAddConnectorButton?: boolean;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
}
|
||||
|
||||
const ICON_SIZE = 'm';
|
||||
|
@ -61,29 +63,36 @@ const ConnectorsDropdownComponent: React.FC<Props> = ({
|
|||
onChange,
|
||||
selectedConnector,
|
||||
appendAddConnectorButton = false,
|
||||
hideConnectorServiceNowSir = false,
|
||||
}) => {
|
||||
const connectorsAsOptions = useMemo(() => {
|
||||
const connectorsFormatted = connectors.reduce(
|
||||
(acc, connector) => [
|
||||
...acc,
|
||||
{
|
||||
value: connector.id,
|
||||
inputDisplay: (
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconExtended
|
||||
type={connectorsConfiguration[connector.actionTypeId]?.logo ?? ''}
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<span>{connector.name}</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
'data-test-subj': `dropdown-connector-${connector.id}`,
|
||||
},
|
||||
],
|
||||
(acc, connector) => {
|
||||
if (hideConnectorServiceNowSir && connector.actionTypeId === ConnectorTypes.serviceNowSIR) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
value: connector.id,
|
||||
inputDisplay: (
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconExtended
|
||||
type={connectorsConfiguration[connector.actionTypeId]?.logo ?? ''}
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<span>{connector.name}</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
'data-test-subj': `dropdown-connector-${connector.id}`,
|
||||
},
|
||||
];
|
||||
},
|
||||
[noConnectorOption]
|
||||
);
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ interface ConnectorSelectorProps {
|
|||
isEdit: boolean;
|
||||
isLoading: boolean;
|
||||
handleChange?: (newValue: string) => void;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
}
|
||||
export const ConnectorSelector = ({
|
||||
connectors,
|
||||
|
@ -32,6 +33,7 @@ export const ConnectorSelector = ({
|
|||
isEdit = true,
|
||||
isLoading = false,
|
||||
handleChange,
|
||||
hideConnectorServiceNowSir = false,
|
||||
}: ConnectorSelectorProps) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const onChange = useCallback(
|
||||
|
@ -58,6 +60,7 @@ export const ConnectorSelector = ({
|
|||
<ConnectorsDropdown
|
||||
connectors={connectors}
|
||||
disabled={disabled}
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
isLoading={isLoading}
|
||||
onChange={onChange}
|
||||
selectedConnector={isEmpty(field.value) ? 'none' : field.value}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { ActionParamsProps } from '../../../../../../triggers_actions_ui/public/types';
|
||||
import { CommentType } from '../../../../../../case/common/api';
|
||||
|
@ -21,13 +21,15 @@ import * as i18n from './translations';
|
|||
|
||||
const Container = styled.div`
|
||||
${({ theme }) => `
|
||||
margin-top: ${theme.eui?.euiSize ?? '16px'};
|
||||
padding: ${theme.eui?.euiSizeS ?? '8px'} ${theme.eui?.euiSizeL ?? '24px'} ${
|
||||
theme.eui?.euiSizeL ?? '24px'
|
||||
} ${theme.eui?.euiSizeL ?? '24px'};
|
||||
`}
|
||||
`;
|
||||
|
||||
const defaultAlertComment = {
|
||||
type: CommentType.generatedAlert,
|
||||
alerts: `[{{#context.alerts}}{"_id": "{{_id}}", "_index": "{{_index}}", "ruleId": "{{rule.id}}", "ruleName": "{{rule.name}}"}__SEPARATOR__{{/context.alerts}}]`,
|
||||
alerts: `[{{#context.alerts}}{"_id": "{{_id}}", "_index": "{{_index}}", "ruleId": "{{signal.rule.id}}", "ruleName": "{{signal.rule.name}}"}__SEPARATOR__{{/context.alerts}}]`,
|
||||
};
|
||||
|
||||
const CaseParamsFields: React.FunctionComponent<ActionParamsProps<CaseActionParams>> = ({
|
||||
|
@ -90,12 +92,13 @@ const CaseParamsFields: React.FunctionComponent<ActionParamsProps<CaseActionPara
|
|||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut size="s" title={i18n.CASE_CONNECTOR_CALL_OUT_INFO} iconType="iInCircle" />
|
||||
<Container>
|
||||
<ExistingCase onCaseChanged={onCaseChanged} selectedCase={selectedCase} />
|
||||
</Container>
|
||||
</>
|
||||
<Container>
|
||||
<ExistingCase onCaseChanged={onCaseChanged} selectedCase={selectedCase} />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut size="s" title={i18n.CASE_CONNECTOR_CALL_OUT_TITLE} iconType="iInCircle">
|
||||
<p>{i18n.CASE_CONNECTOR_CALL_OUT_MSG}</p>
|
||||
</EuiCallOut>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -5,22 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiCallOut,
|
||||
EuiTextColor,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { memo, useEffect, useCallback, useState } from 'react';
|
||||
import React, { memo, useMemo, useCallback } from 'react';
|
||||
import { CaseType } from '../../../../../../case/common/api';
|
||||
import { Case } from '../../../containers/types';
|
||||
import { useDeleteCases } from '../../../containers/use_delete_cases';
|
||||
import { useGetCase } from '../../../containers/use_get_case';
|
||||
import { ConfirmDeleteCaseModal } from '../../confirm_delete_case';
|
||||
import {
|
||||
useGetCases,
|
||||
DEFAULT_QUERY_PARAMS,
|
||||
DEFAULT_FILTER_OPTIONS,
|
||||
} from '../../../containers/use_get_cases';
|
||||
import { useCreateCaseModal } from '../../use_create_case_modal';
|
||||
import * as i18n from './translations';
|
||||
import { CasesDropdown, ADD_CASE_BUTTON_ID } from './cases_dropdown';
|
||||
|
||||
interface ExistingCaseProps {
|
||||
selectedCase: string | null;
|
||||
|
@ -28,76 +21,53 @@ interface ExistingCaseProps {
|
|||
}
|
||||
|
||||
const ExistingCaseComponent: React.FC<ExistingCaseProps> = ({ onCaseChanged, selectedCase }) => {
|
||||
const { data, isLoading, isError } = useGetCase(selectedCase ?? '');
|
||||
const [createdCase, setCreatedCase] = useState<Case | null>(null);
|
||||
const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases(DEFAULT_QUERY_PARAMS, {
|
||||
...DEFAULT_FILTER_OPTIONS,
|
||||
onlyCollectionType: true,
|
||||
});
|
||||
|
||||
const onCaseCreated = useCallback(
|
||||
(newCase: Case) => {
|
||||
(newCase) => {
|
||||
refetchCases();
|
||||
onCaseChanged(newCase.id);
|
||||
setCreatedCase(newCase);
|
||||
},
|
||||
[onCaseChanged]
|
||||
[onCaseChanged, refetchCases]
|
||||
);
|
||||
|
||||
const { modal, openModal } = useCreateCaseModal({ caseType: CaseType.collection, onCaseCreated });
|
||||
const { modal, openModal } = useCreateCaseModal({
|
||||
onCaseCreated,
|
||||
caseType: CaseType.collection,
|
||||
// FUTURE DEVELOPER
|
||||
// We are making the assumption that this component is only used in rules creation
|
||||
// that's why we want to hide ServiceNow SIR
|
||||
hideConnectorServiceNowSir: true,
|
||||
});
|
||||
|
||||
// Delete case
|
||||
const {
|
||||
dispatchResetIsDeleted,
|
||||
handleOnDeleteConfirm,
|
||||
handleToggleModal,
|
||||
isLoading: isDeleting,
|
||||
isDeleted,
|
||||
isDisplayConfirmDeleteModal,
|
||||
} = useDeleteCases();
|
||||
const onChange = useCallback(
|
||||
(id: string) => {
|
||||
if (id === ADD_CASE_BUTTON_ID) {
|
||||
openModal();
|
||||
return;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isDeleted) {
|
||||
setCreatedCase(null);
|
||||
onCaseChanged('');
|
||||
dispatchResetIsDeleted();
|
||||
}
|
||||
// onCaseChanged and/or dispatchResetIsDeleted causes re-renders
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDeleted]);
|
||||
onCaseChanged(id);
|
||||
},
|
||||
[onCaseChanged, openModal]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isError && data != null) {
|
||||
setCreatedCase(data);
|
||||
onCaseChanged(data.id);
|
||||
}
|
||||
// onCaseChanged causes re-renders
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, isLoading, isError]);
|
||||
const isCasesLoading = useMemo(
|
||||
() => isLoadingCases.includes('cases') || isLoadingCases.includes('caseUpdate'),
|
||||
[isLoadingCases]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{createdCase == null && isEmpty(selectedCase) && (
|
||||
<EuiButton fill fullWidth onClick={openModal}>
|
||||
{i18n.CREATE_CASE}
|
||||
</EuiButton>
|
||||
)}
|
||||
{createdCase == null && isLoading && <EuiLoadingSpinner size="m" />}
|
||||
{createdCase != null && !isLoading && (
|
||||
<>
|
||||
<EuiCallOut title={i18n.CONNECTED_CASE} color="success">
|
||||
<EuiTextColor color="default">
|
||||
{createdCase.title}{' '}
|
||||
{!isDeleting && (
|
||||
<EuiButtonIcon color="danger" onClick={handleToggleModal} iconType="trash" />
|
||||
)}
|
||||
{isDeleting && <EuiLoadingSpinner size="m" />}
|
||||
</EuiTextColor>
|
||||
</EuiCallOut>
|
||||
<ConfirmDeleteCaseModal
|
||||
caseTitle={createdCase.title}
|
||||
isModalVisible={isDisplayConfirmDeleteModal}
|
||||
isPlural={false}
|
||||
onCancel={handleToggleModal}
|
||||
onConfirm={handleOnDeleteConfirm.bind(null, [createdCase])}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<CasesDropdown
|
||||
isLoading={isCasesLoading}
|
||||
cases={cases.cases}
|
||||
selectedCase={selectedCase ?? undefined}
|
||||
onCaseChanged={onChange}
|
||||
/>
|
||||
{modal}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -40,7 +40,7 @@ export const CASE_CONNECTOR_COMMENT_REQUIRED = i18n.translate(
|
|||
export const CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel',
|
||||
{
|
||||
defaultMessage: 'Case',
|
||||
defaultMessage: 'Case allowing sub-cases',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -72,10 +72,18 @@ export const CASE_CONNECTOR_CASE_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_CALL_OUT_INFO = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.callOutInfo',
|
||||
export const CASE_CONNECTOR_CALL_OUT_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.callOutTitle',
|
||||
{
|
||||
defaultMessage: 'All alerts after rule creation will be appended to the selected case.',
|
||||
defaultMessage: 'Generated alerts will be attached to sub-cases',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASE_CONNECTOR_CALL_OUT_MSG = i18n.translate(
|
||||
'xpack.securitySolution.case.components.connectors.case.callOutMsg',
|
||||
{
|
||||
defaultMessage:
|
||||
'A case can contain multiple sub-cases to allow grouping of generated alerts. Sub-cases will give more granular control over the status of these generated alerts and prevents having too many alerts attached to one case.',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React, { memo, useCallback } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { ConnectorTypes } from '../../../../../case/common/api';
|
||||
import { UseField, useFormData, FieldHook, useFormContext } from '../../../shared_imports';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { ConnectorSelector } from '../connector_selector/form';
|
||||
|
@ -18,19 +19,32 @@ import { FormProps } from './schema';
|
|||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
}
|
||||
|
||||
interface ConnectorsFieldProps {
|
||||
connectors: ActionConnector[];
|
||||
field: FieldHook<FormProps['fields']>;
|
||||
isEdit: boolean;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
}
|
||||
|
||||
const ConnectorFields = ({ connectors, isEdit, field }: ConnectorsFieldProps) => {
|
||||
const ConnectorFields = ({
|
||||
connectors,
|
||||
isEdit,
|
||||
field,
|
||||
hideConnectorServiceNowSir = false,
|
||||
}: ConnectorsFieldProps) => {
|
||||
const [{ connectorId }] = useFormData({ watch: ['connectorId'] });
|
||||
const { setValue } = field;
|
||||
const connector = getConnectorById(connectorId, connectors) ?? null;
|
||||
|
||||
let connector = getConnectorById(connectorId, connectors) ?? null;
|
||||
if (
|
||||
connector &&
|
||||
hideConnectorServiceNowSir &&
|
||||
connector.actionTypeId === ConnectorTypes.serviceNowSIR
|
||||
) {
|
||||
connector = null;
|
||||
}
|
||||
return (
|
||||
<ConnectorFieldsForm
|
||||
connector={connector}
|
||||
|
@ -41,7 +55,7 @@ const ConnectorFields = ({ connectors, isEdit, field }: ConnectorsFieldProps) =>
|
|||
);
|
||||
};
|
||||
|
||||
const ConnectorComponent: React.FC<Props> = ({ isLoading }) => {
|
||||
const ConnectorComponent: React.FC<Props> = ({ hideConnectorServiceNowSir = false, isLoading }) => {
|
||||
const { getFields } = useFormContext();
|
||||
const { loading: isLoadingConnectors, connectors } = useConnectors();
|
||||
const handleConnectorChange = useCallback(
|
||||
|
@ -61,6 +75,7 @@ const ConnectorComponent: React.FC<Props> = ({ isLoading }) => {
|
|||
componentProps={{
|
||||
connectors,
|
||||
handleChange: handleConnectorChange,
|
||||
hideConnectorServiceNowSir,
|
||||
dataTestSubj: 'caseConnectors',
|
||||
disabled: isLoading || isLoadingConnectors,
|
||||
idAria: 'caseConnectors',
|
||||
|
@ -74,6 +89,7 @@ const ConnectorComponent: React.FC<Props> = ({ isLoading }) => {
|
|||
component={ConnectorFields}
|
||||
componentProps={{
|
||||
connectors,
|
||||
hideConnectorServiceNowSir,
|
||||
isEdit: true,
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -36,78 +36,84 @@ const MySpinner = styled(EuiLoadingSpinner)`
|
|||
`;
|
||||
|
||||
interface Props {
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
withSteps?: boolean;
|
||||
}
|
||||
|
||||
export const CreateCaseForm: React.FC<Props> = React.memo(({ withSteps = true }) => {
|
||||
const { isSubmitting } = useFormContext();
|
||||
export const CreateCaseForm: React.FC<Props> = React.memo(
|
||||
({ hideConnectorServiceNowSir = false, withSteps = true }) => {
|
||||
const { isSubmitting } = useFormContext();
|
||||
|
||||
const firstStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_ONE_TITLE,
|
||||
children: (
|
||||
<>
|
||||
<Title isLoading={isSubmitting} />
|
||||
const firstStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_ONE_TITLE,
|
||||
children: (
|
||||
<>
|
||||
<Title isLoading={isSubmitting} />
|
||||
<Container>
|
||||
<Tags isLoading={isSubmitting} />
|
||||
</Container>
|
||||
<Container big>
|
||||
<Description isLoading={isSubmitting} />
|
||||
</Container>
|
||||
</>
|
||||
),
|
||||
}),
|
||||
[isSubmitting]
|
||||
);
|
||||
|
||||
const secondStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_TWO_TITLE,
|
||||
children: (
|
||||
<Container>
|
||||
<Tags isLoading={isSubmitting} />
|
||||
<SyncAlertsToggle isLoading={isSubmitting} />
|
||||
</Container>
|
||||
<Container big>
|
||||
<Description isLoading={isSubmitting} />
|
||||
),
|
||||
}),
|
||||
[isSubmitting]
|
||||
);
|
||||
|
||||
const thirdStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_THREE_TITLE,
|
||||
children: (
|
||||
<Container>
|
||||
<Connector
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
isLoading={isSubmitting}
|
||||
/>
|
||||
</Container>
|
||||
</>
|
||||
),
|
||||
}),
|
||||
[isSubmitting]
|
||||
);
|
||||
),
|
||||
}),
|
||||
[hideConnectorServiceNowSir, isSubmitting]
|
||||
);
|
||||
|
||||
const secondStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_TWO_TITLE,
|
||||
children: (
|
||||
<Container>
|
||||
<SyncAlertsToggle isLoading={isSubmitting} />
|
||||
</Container>
|
||||
),
|
||||
}),
|
||||
[isSubmitting]
|
||||
);
|
||||
const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [
|
||||
firstStep,
|
||||
secondStep,
|
||||
thirdStep,
|
||||
]);
|
||||
|
||||
const thirdStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_THREE_TITLE,
|
||||
children: (
|
||||
<Container>
|
||||
<Connector isLoading={isSubmitting} />
|
||||
</Container>
|
||||
),
|
||||
}),
|
||||
[isSubmitting]
|
||||
);
|
||||
|
||||
const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [
|
||||
firstStep,
|
||||
secondStep,
|
||||
thirdStep,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSubmitting && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />}
|
||||
{withSteps ? (
|
||||
<EuiSteps
|
||||
headingElement="h2"
|
||||
steps={allSteps}
|
||||
data-test-subj={'case-creation-form-steps'}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{firstStep.children}
|
||||
{secondStep.children}
|
||||
{thirdStep.children}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{isSubmitting && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />}
|
||||
{withSteps ? (
|
||||
<EuiSteps
|
||||
headingElement="h2"
|
||||
steps={allSteps}
|
||||
data-test-subj={'case-creation-form-steps'}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{firstStep.children}
|
||||
{secondStep.children}
|
||||
{thirdStep.children}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CreateCaseForm.displayName = 'CreateCaseForm';
|
||||
|
|
|
@ -19,7 +19,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service'
|
|||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { useCaseConfigure } from '../../containers/configure/use_configure';
|
||||
import { Case } from '../../containers/types';
|
||||
import { CaseType } from '../../../../../case/common/api';
|
||||
import { CaseType, ConnectorTypes } from '../../../../../case/common/api';
|
||||
|
||||
const initialCaseValue: FormProps = {
|
||||
description: '',
|
||||
|
@ -31,29 +31,40 @@ const initialCaseValue: FormProps = {
|
|||
};
|
||||
|
||||
interface Props {
|
||||
caseType?: CaseType;
|
||||
onSuccess?: (theCase: Case) => Promise<void>;
|
||||
afterCaseCreated?: (theCase: Case) => Promise<void>;
|
||||
caseType?: CaseType;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
onSuccess?: (theCase: Case) => Promise<void>;
|
||||
}
|
||||
|
||||
export const FormContext: React.FC<Props> = ({
|
||||
afterCaseCreated,
|
||||
caseType = CaseType.individual,
|
||||
children,
|
||||
hideConnectorServiceNowSir,
|
||||
onSuccess,
|
||||
afterCaseCreated,
|
||||
}) => {
|
||||
const { connectors } = useConnectors();
|
||||
const { connector: configurationConnector } = useCaseConfigure();
|
||||
const { postCase } = usePostCase();
|
||||
const { pushCaseToExternalService } = usePostPushToService();
|
||||
|
||||
const connectorId = useMemo(
|
||||
() =>
|
||||
connectors.some((connector) => connector.id === configurationConnector.id)
|
||||
? configurationConnector.id
|
||||
: 'none',
|
||||
[configurationConnector.id, connectors]
|
||||
);
|
||||
const connectorId = useMemo(() => {
|
||||
if (
|
||||
hideConnectorServiceNowSir &&
|
||||
configurationConnector.type === ConnectorTypes.serviceNowSIR
|
||||
) {
|
||||
return 'none';
|
||||
}
|
||||
return connectors.some((connector) => connector.id === configurationConnector.id)
|
||||
? configurationConnector.id
|
||||
: 'none';
|
||||
}, [
|
||||
configurationConnector.id,
|
||||
configurationConnector.type,
|
||||
connectors,
|
||||
hideConnectorServiceNowSir,
|
||||
]);
|
||||
|
||||
const submitCase = useCallback(
|
||||
async (
|
||||
|
|
|
@ -34,7 +34,6 @@ import * as i18n from './translations';
|
|||
interface EditConnectorProps {
|
||||
caseFields: ConnectorTypeFields['fields'];
|
||||
connectors: ActionConnector[];
|
||||
disabled?: boolean;
|
||||
isLoading: boolean;
|
||||
onSubmit: (
|
||||
connectorId: string,
|
||||
|
@ -44,6 +43,8 @@ interface EditConnectorProps {
|
|||
) => void;
|
||||
selectedConnector: string;
|
||||
userActions: CaseUserActions[];
|
||||
disabled?: boolean;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
}
|
||||
|
||||
const MyFlexGroup = styled(EuiFlexGroup)`
|
||||
|
@ -105,6 +106,7 @@ export const EditConnector = React.memo(
|
|||
caseFields,
|
||||
connectors,
|
||||
disabled = false,
|
||||
hideConnectorServiceNowSir = false,
|
||||
isLoading,
|
||||
onSubmit,
|
||||
selectedConnector,
|
||||
|
@ -234,6 +236,7 @@ export const EditConnector = React.memo(
|
|||
dataTestSubj: 'caseConnectors',
|
||||
defaultValue: selectedConnector,
|
||||
disabled,
|
||||
hideConnectorServiceNowSir,
|
||||
idAria: 'caseConnectors',
|
||||
isEdit: editConnector,
|
||||
isLoading,
|
||||
|
|
|
@ -21,6 +21,7 @@ export interface CreateCaseModalProps {
|
|||
onCloseCaseModal: () => void;
|
||||
onSuccess: (theCase: Case) => Promise<void>;
|
||||
caseType?: CaseType;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
|
@ -35,6 +36,7 @@ const CreateModalComponent: React.FC<CreateCaseModalProps> = ({
|
|||
onCloseCaseModal,
|
||||
onSuccess,
|
||||
caseType = CaseType.individual,
|
||||
hideConnectorServiceNowSir = false,
|
||||
}) => {
|
||||
return isModalOpen ? (
|
||||
<EuiModal onClose={onCloseCaseModal} data-test-subj="all-cases-modal">
|
||||
|
@ -42,8 +44,15 @@ const CreateModalComponent: React.FC<CreateCaseModalProps> = ({
|
|||
<EuiModalHeaderTitle>{i18n.CREATE_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<FormContext caseType={caseType} onSuccess={onSuccess}>
|
||||
<CreateCaseForm withSteps={false} />
|
||||
<FormContext
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
caseType={caseType}
|
||||
onSuccess={onSuccess}
|
||||
>
|
||||
<CreateCaseForm
|
||||
withSteps={false}
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
/>
|
||||
<Container>
|
||||
<SubmitCaseButton />
|
||||
</Container>
|
||||
|
|
|
@ -13,6 +13,7 @@ import { CreateCaseModal } from './create_case_modal';
|
|||
export interface UseCreateCaseModalProps {
|
||||
onCaseCreated: (theCase: Case) => void;
|
||||
caseType?: CaseType;
|
||||
hideConnectorServiceNowSir?: boolean;
|
||||
}
|
||||
export interface UseCreateCaseModalReturnedValues {
|
||||
modal: JSX.Element;
|
||||
|
@ -24,6 +25,7 @@ export interface UseCreateCaseModalReturnedValues {
|
|||
export const useCreateCaseModal = ({
|
||||
caseType = CaseType.individual,
|
||||
onCaseCreated,
|
||||
hideConnectorServiceNowSir = false,
|
||||
}: UseCreateCaseModalProps) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const closeModal = useCallback(() => setIsModalOpen(false), []);
|
||||
|
@ -41,6 +43,7 @@ export const useCreateCaseModal = ({
|
|||
modal: (
|
||||
<CreateCaseModal
|
||||
caseType={caseType}
|
||||
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||
isModalOpen={isModalOpen}
|
||||
onCloseCaseModal={closeModal}
|
||||
onSuccess={onSuccess}
|
||||
|
@ -50,7 +53,7 @@ export const useCreateCaseModal = ({
|
|||
closeModal,
|
||||
openModal,
|
||||
}),
|
||||
[caseType, closeModal, isModalOpen, onSuccess, openModal]
|
||||
[caseType, closeModal, hideConnectorServiceNowSir, isModalOpen, onSuccess, openModal]
|
||||
);
|
||||
|
||||
return state;
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
CasesResponse,
|
||||
CasesStatusResponse,
|
||||
CaseStatuses,
|
||||
CaseType,
|
||||
CaseUserActionsResponse,
|
||||
CommentRequest,
|
||||
CommentType,
|
||||
|
@ -165,6 +166,7 @@ export const getSubCaseUserActions = async (
|
|||
|
||||
export const getCases = async ({
|
||||
filterOptions = {
|
||||
onlyCollectionType: false,
|
||||
search: '',
|
||||
reporters: [],
|
||||
status: CaseStatuses.open,
|
||||
|
@ -183,6 +185,7 @@ export const getCases = async ({
|
|||
tags: filterOptions.tags.map((t) => `"${t.replace(/"/g, '\\"')}"`),
|
||||
status: filterOptions.status,
|
||||
...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}),
|
||||
...(filterOptions.onlyCollectionType ? { type: CaseType.collection } : {}),
|
||||
...queryParams,
|
||||
};
|
||||
const response = await KibanaServices.get().http.fetch<CasesFindResponse>(`${CASES_URL}/_find`, {
|
||||
|
|
|
@ -99,6 +99,7 @@ export interface FilterOptions {
|
|||
status: CaseStatuses;
|
||||
tags: string[];
|
||||
reporters: User[];
|
||||
onlyCollectionType?: boolean;
|
||||
}
|
||||
|
||||
export interface CasesStatus {
|
||||
|
|
|
@ -97,6 +97,7 @@ export const DEFAULT_FILTER_OPTIONS: FilterOptions = {
|
|||
reporters: [],
|
||||
status: CaseStatuses.open,
|
||||
tags: [],
|
||||
onlyCollectionType: false,
|
||||
};
|
||||
|
||||
export const DEFAULT_QUERY_PARAMS: QueryParams = {
|
||||
|
@ -129,10 +130,13 @@ export interface UseGetCases extends UseGetCasesState {
|
|||
setSelectedCases: (mySelectedCases: Case[]) => void;
|
||||
}
|
||||
|
||||
export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => {
|
||||
export const useGetCases = (
|
||||
initialQueryParams?: QueryParams,
|
||||
initialFilterOptions?: FilterOptions
|
||||
): UseGetCases => {
|
||||
const [state, dispatch] = useReducer(dataFetchReducer, {
|
||||
data: initialData,
|
||||
filterOptions: DEFAULT_FILTER_OPTIONS,
|
||||
filterOptions: initialFilterOptions ?? DEFAULT_FILTER_OPTIONS,
|
||||
isError: false,
|
||||
loading: [],
|
||||
queryParams: initialQueryParams ?? DEFAULT_QUERY_PARAMS,
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { RuleActionsField } from './index';
|
||||
import { getSupportedActions, RuleActionsField } from './index';
|
||||
import { useForm, Form } from '../../../../shared_imports';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { useFormFieldMock } from '../../../../common/mock';
|
||||
import { ActionType } from '../../../../../../actions/common';
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
describe('RuleActionsField', () => {
|
||||
|
@ -45,7 +46,11 @@ describe('RuleActionsField', () => {
|
|||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<RuleActionsField field={field} messageVariables={messageVariables} />
|
||||
<RuleActionsField
|
||||
field={field}
|
||||
messageVariables={messageVariables}
|
||||
hasErrorOnCreationCaseAction={false}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
@ -53,4 +58,63 @@ describe('RuleActionsField', () => {
|
|||
|
||||
expect(wrapper.dive().find('ActionForm')).toHaveLength(0);
|
||||
});
|
||||
|
||||
describe('#getSupportedActions', () => {
|
||||
const actions: ActionType[] = [
|
||||
{
|
||||
id: '.jira',
|
||||
name: 'My Jira',
|
||||
enabled: true,
|
||||
enabledInConfig: false,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'gold',
|
||||
},
|
||||
{
|
||||
id: '.case',
|
||||
name: 'Cases',
|
||||
enabled: true,
|
||||
enabledInConfig: false,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
},
|
||||
];
|
||||
|
||||
it('if we have an error on case action creation, we do not support case connector', () => {
|
||||
expect(getSupportedActions(actions, true)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"enabled": true,
|
||||
"enabledInConfig": false,
|
||||
"enabledInLicense": true,
|
||||
"id": ".jira",
|
||||
"minimumLicenseRequired": "gold",
|
||||
"name": "My Jira",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('if we do NOT have an error on case action creation, we are supporting case connector', () => {
|
||||
expect(getSupportedActions(actions, false)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"enabled": true,
|
||||
"enabledInConfig": false,
|
||||
"enabledInLicense": true,
|
||||
"id": ".jira",
|
||||
"minimumLicenseRequired": "gold",
|
||||
"name": "My Jira",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"enabledInConfig": false,
|
||||
"enabledInLicense": true,
|
||||
"id": ".case",
|
||||
"minimumLicenseRequired": "basic",
|
||||
"name": "Cases",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,6 +26,7 @@ import { FORM_ERRORS_TITLE } from './translations';
|
|||
|
||||
interface Props {
|
||||
field: FieldHook;
|
||||
hasErrorOnCreationCaseAction: boolean;
|
||||
messageVariables: ActionVariables;
|
||||
}
|
||||
|
||||
|
@ -39,7 +40,44 @@ const FieldErrorsContainer = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) => {
|
||||
const ContainerActions = styled.div.attrs(
|
||||
({ className = '', $caseIndexes = [] }: { className?: string; $caseIndexes: string[] }) => ({
|
||||
className,
|
||||
})
|
||||
)<{ $caseIndexes: string[] }>`
|
||||
${({ $caseIndexes }) =>
|
||||
$caseIndexes.map(
|
||||
(index) => `
|
||||
div[id="${index}"].euiAccordion__childWrapper .euiAccordion__padding--l {
|
||||
padding: 0px;
|
||||
.euiFlexGroup {
|
||||
display: none;
|
||||
}
|
||||
.euiSpacer.euiSpacer--xl {
|
||||
height: 0px;
|
||||
}
|
||||
}
|
||||
`
|
||||
)}
|
||||
`;
|
||||
|
||||
export const getSupportedActions = (
|
||||
actionTypes: ActionType[],
|
||||
hasErrorOnCreationCaseAction: boolean
|
||||
): ActionType[] => {
|
||||
return actionTypes.filter((actionType) => {
|
||||
if (actionType.id === '.case' && hasErrorOnCreationCaseAction) {
|
||||
return false;
|
||||
}
|
||||
return NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id);
|
||||
});
|
||||
};
|
||||
|
||||
export const RuleActionsField: React.FC<Props> = ({
|
||||
field,
|
||||
hasErrorOnCreationCaseAction,
|
||||
messageVariables,
|
||||
}) => {
|
||||
const [fieldErrors, setFieldErrors] = useState<string | null>(null);
|
||||
const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>();
|
||||
const form = useFormContext();
|
||||
|
@ -54,6 +92,17 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
|||
[field.value]
|
||||
);
|
||||
|
||||
const caseActionIndexes = useMemo(
|
||||
() =>
|
||||
actions.reduce<string[]>((acc, action, actionIndex) => {
|
||||
if (action.actionTypeId === '.case') {
|
||||
return [...acc, `${actionIndex}`];
|
||||
}
|
||||
return acc;
|
||||
}, []),
|
||||
[actions]
|
||||
);
|
||||
|
||||
const setActionIdByIndex = useCallback(
|
||||
(id: string, index: number) => {
|
||||
const updatedActions = [...(actions as Array<Partial<AlertAction>>)];
|
||||
|
@ -83,13 +132,11 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
|||
useEffect(() => {
|
||||
(async function () {
|
||||
const actionTypes = await loadActionTypes({ http });
|
||||
const supportedTypes = actionTypes.filter((actionType) =>
|
||||
NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id)
|
||||
);
|
||||
const supportedTypes = getSupportedActions(actionTypes, hasErrorOnCreationCaseAction);
|
||||
setSupportedActionTypes(supportedTypes);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [hasErrorOnCreationCaseAction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting || !field.errors.length) {
|
||||
|
@ -104,7 +151,7 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
|||
if (!supportedActionTypes) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContainerActions $caseIndexes={caseActionIndexes}>
|
||||
{fieldErrors ? (
|
||||
<>
|
||||
<FieldErrorsContainer>
|
||||
|
@ -126,6 +173,6 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
|||
actionTypes={supportedActionTypes}
|
||||
defaultActionMessage={DEFAULT_ACTION_MESSAGE}
|
||||
/>
|
||||
</>
|
||||
</ContainerActions>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -36,6 +36,7 @@ import { useKibana } from '../../../../common/lib/kibana';
|
|||
import { getSchema } from './schema';
|
||||
import * as I18n from './translations';
|
||||
import { APP_ID } from '../../../../../common/constants';
|
||||
import { useManageCaseAction } from './use_manage_case_action';
|
||||
|
||||
interface StepRuleActionsProps extends RuleStepProps {
|
||||
defaultValues?: ActionsStepRule | null;
|
||||
|
@ -70,6 +71,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
setForm,
|
||||
actionMessageParams,
|
||||
}) => {
|
||||
const [isLoadingCaseAction, hasErrorOnCreationCaseAction] = useManageCaseAction();
|
||||
const {
|
||||
services: {
|
||||
application,
|
||||
|
@ -138,13 +140,14 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
() => ({
|
||||
idAria: 'detectionEngineStepRuleActionsThrottle',
|
||||
isDisabled: isLoading,
|
||||
isLoading: isLoadingCaseAction,
|
||||
dataTestSubj: 'detectionEngineStepRuleActionsThrottle',
|
||||
hasNoInitialSelection: false,
|
||||
euiFieldProps: {
|
||||
options: throttleOptions,
|
||||
},
|
||||
}),
|
||||
[isLoading, throttleOptions]
|
||||
[isLoading, isLoadingCaseAction, throttleOptions]
|
||||
);
|
||||
|
||||
const displayActionsOptions = useMemo(
|
||||
|
@ -157,13 +160,14 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
component={RuleActionsField}
|
||||
componentProps={{
|
||||
messageVariables: actionMessageParams,
|
||||
hasErrorOnCreationCaseAction,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<UseField path="actions" component={GhostFormField} />
|
||||
),
|
||||
[throttle, actionMessageParams]
|
||||
[throttle, actionMessageParams, hasErrorOnCreationCaseAction]
|
||||
);
|
||||
// only display the actions dropdown if the user has "read" privileges for actions
|
||||
const displayActionsDropDown = useMemo(() => {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { useEffect, useRef, useState } from 'react';
|
||||
import { ACTION_URL } from '../../../../../../case/common/constants';
|
||||
import { KibanaServices } from '../../../../common/lib/kibana';
|
||||
|
||||
interface CaseAction {
|
||||
actionTypeId: string;
|
||||
id: string;
|
||||
isPreconfigured: boolean;
|
||||
name: string;
|
||||
referencedByCount: number;
|
||||
}
|
||||
|
||||
const CASE_ACTION_NAME = 'Cases';
|
||||
|
||||
export const useManageCaseAction = () => {
|
||||
const hasInit = useRef(true);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const abortCtrl = new AbortController();
|
||||
const fetchActions = async () => {
|
||||
try {
|
||||
const actions = await KibanaServices.get().http.fetch<CaseAction[]>(ACTION_URL, {
|
||||
method: 'GET',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
if (!actions.some((a) => a.actionTypeId === '.case' && a.name === CASE_ACTION_NAME)) {
|
||||
await KibanaServices.get().http.post<CaseAction[]>(`${ACTION_URL}/action`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
actionTypeId: '.case',
|
||||
config: {},
|
||||
name: CASE_ACTION_NAME,
|
||||
secrets: {},
|
||||
}),
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
} catch {
|
||||
setLoading(false);
|
||||
setHasError(true);
|
||||
}
|
||||
};
|
||||
if (hasInit.current) {
|
||||
hasInit.current = false;
|
||||
fetchActions();
|
||||
}
|
||||
|
||||
return () => {
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, []);
|
||||
return [loading, hasError];
|
||||
};
|
|
@ -17199,7 +17199,6 @@
|
|||
"xpack.securitySolution.case.common.noConnector": "コネクターを選択していません",
|
||||
"xpack.securitySolution.case.components.connectors.case.actionTypeTitle": "ケース",
|
||||
"xpack.securitySolution.case.components.connectors.case.addNewCaseOption": "新規ケースの追加",
|
||||
"xpack.securitySolution.case.components.connectors.case.callOutInfo": "ルールを作成した後のすべてのアラートは、選択したケースの最後に追加されます。",
|
||||
"xpack.securitySolution.case.components.connectors.case.caseRequired": "ケースの選択が必要です。",
|
||||
"xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder": "ケースを選択",
|
||||
"xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel": "ケース",
|
||||
|
|
|
@ -17242,7 +17242,6 @@
|
|||
"xpack.securitySolution.case.common.noConnector": "未选择任何连接器",
|
||||
"xpack.securitySolution.case.components.connectors.case.actionTypeTitle": "案例",
|
||||
"xpack.securitySolution.case.components.connectors.case.addNewCaseOption": "添加新案例",
|
||||
"xpack.securitySolution.case.components.connectors.case.callOutInfo": "规则创建后的所有告警将追加到选定案例。",
|
||||
"xpack.securitySolution.case.components.connectors.case.caseRequired": "必须选择策略。",
|
||||
"xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder": "选择案例",
|
||||
"xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel": "案例",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue