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}
|
caseFields={caseData.connector.fields}
|
||||||
connectors={connectors}
|
connectors={connectors}
|
||||||
disabled={!userCanCrud}
|
disabled={!userCanCrud}
|
||||||
|
hideConnectorServiceNowSir={
|
||||||
|
subCaseId != null || caseData.type === CaseType.collection
|
||||||
|
}
|
||||||
isLoading={isLoadingConnectors || (isLoading && updateKey === 'connector')}
|
isLoading={isLoadingConnectors || (isLoading && updateKey === 'connector')}
|
||||||
onSubmit={onSubmitConnector}
|
onSubmit={onSubmitConnector}
|
||||||
selectedConnector={caseData.connector.id}
|
selectedConnector={caseData.connector.id}
|
||||||
|
|
|
@ -34,22 +34,122 @@ describe('ConnectorsDropdown', () => {
|
||||||
test('it formats the connectors correctly', () => {
|
test('it formats the connectors correctly', () => {
|
||||||
const selectProps = wrapper.find(EuiSuperSelect).props();
|
const selectProps = wrapper.find(EuiSuperSelect).props();
|
||||||
|
|
||||||
expect(selectProps.options).toEqual(
|
expect(selectProps.options).toMatchInlineSnapshot(`
|
||||||
expect.arrayContaining([
|
Array [
|
||||||
expect.objectContaining({
|
Object {
|
||||||
value: 'none',
|
"data-test-subj": "dropdown-connector-no-connector",
|
||||||
'data-test-subj': 'dropdown-connector-no-connector',
|
"inputDisplay": <EuiFlexGroup
|
||||||
}),
|
alignItems="center"
|
||||||
expect.objectContaining({
|
gutterSize="none"
|
||||||
value: 'servicenow-1',
|
>
|
||||||
'data-test-subj': 'dropdown-connector-servicenow-1',
|
<EuiFlexItem
|
||||||
}),
|
grow={false}
|
||||||
expect.objectContaining({
|
>
|
||||||
value: 'resilient-2',
|
<Styled(EuiIcon)
|
||||||
'data-test-subj': 'dropdown-connector-resilient-2',
|
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', () => {
|
test('it disables the dropdown', () => {
|
||||||
|
@ -79,4 +179,25 @@ describe('ConnectorsDropdown', () => {
|
||||||
|
|
||||||
expect(newWrapper.find('button span:not([data-euiicon-type])').text()).toEqual('My Connector');
|
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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { ConnectorTypes } from '../../../../../case/common/api';
|
||||||
import { ActionConnector } from '../../containers/configure/types';
|
import { ActionConnector } from '../../containers/configure/types';
|
||||||
import { connectorsConfiguration } from '../connectors';
|
import { connectorsConfiguration } from '../connectors';
|
||||||
import * as i18n from './translations';
|
import * as i18n from './translations';
|
||||||
|
@ -20,6 +21,7 @@ export interface Props {
|
||||||
onChange: (id: string) => void;
|
onChange: (id: string) => void;
|
||||||
selectedConnector: string;
|
selectedConnector: string;
|
||||||
appendAddConnectorButton?: boolean;
|
appendAddConnectorButton?: boolean;
|
||||||
|
hideConnectorServiceNowSir?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ICON_SIZE = 'm';
|
const ICON_SIZE = 'm';
|
||||||
|
@ -61,29 +63,36 @@ const ConnectorsDropdownComponent: React.FC<Props> = ({
|
||||||
onChange,
|
onChange,
|
||||||
selectedConnector,
|
selectedConnector,
|
||||||
appendAddConnectorButton = false,
|
appendAddConnectorButton = false,
|
||||||
|
hideConnectorServiceNowSir = false,
|
||||||
}) => {
|
}) => {
|
||||||
const connectorsAsOptions = useMemo(() => {
|
const connectorsAsOptions = useMemo(() => {
|
||||||
const connectorsFormatted = connectors.reduce(
|
const connectorsFormatted = connectors.reduce(
|
||||||
(acc, connector) => [
|
(acc, connector) => {
|
||||||
...acc,
|
if (hideConnectorServiceNowSir && connector.actionTypeId === ConnectorTypes.serviceNowSIR) {
|
||||||
{
|
return acc;
|
||||||
value: connector.id,
|
}
|
||||||
inputDisplay: (
|
|
||||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
return [
|
||||||
<EuiFlexItem grow={false}>
|
...acc,
|
||||||
<EuiIconExtended
|
{
|
||||||
type={connectorsConfiguration[connector.actionTypeId]?.logo ?? ''}
|
value: connector.id,
|
||||||
size={ICON_SIZE}
|
inputDisplay: (
|
||||||
/>
|
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||||
</EuiFlexItem>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiFlexItem>
|
<EuiIconExtended
|
||||||
<span>{connector.name}</span>
|
type={connectorsConfiguration[connector.actionTypeId]?.logo ?? ''}
|
||||||
</EuiFlexItem>
|
size={ICON_SIZE}
|
||||||
</EuiFlexGroup>
|
/>
|
||||||
),
|
</EuiFlexItem>
|
||||||
'data-test-subj': `dropdown-connector-${connector.id}`,
|
<EuiFlexItem>
|
||||||
},
|
<span>{connector.name}</span>
|
||||||
],
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
),
|
||||||
|
'data-test-subj': `dropdown-connector-${connector.id}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
[noConnectorOption]
|
[noConnectorOption]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ interface ConnectorSelectorProps {
|
||||||
isEdit: boolean;
|
isEdit: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
handleChange?: (newValue: string) => void;
|
handleChange?: (newValue: string) => void;
|
||||||
|
hideConnectorServiceNowSir?: boolean;
|
||||||
}
|
}
|
||||||
export const ConnectorSelector = ({
|
export const ConnectorSelector = ({
|
||||||
connectors,
|
connectors,
|
||||||
|
@ -32,6 +33,7 @@ export const ConnectorSelector = ({
|
||||||
isEdit = true,
|
isEdit = true,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
handleChange,
|
handleChange,
|
||||||
|
hideConnectorServiceNowSir = false,
|
||||||
}: ConnectorSelectorProps) => {
|
}: ConnectorSelectorProps) => {
|
||||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
|
@ -58,6 +60,7 @@ export const ConnectorSelector = ({
|
||||||
<ConnectorsDropdown
|
<ConnectorsDropdown
|
||||||
connectors={connectors}
|
connectors={connectors}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
selectedConnector={isEmpty(field.value) ? 'none' : field.value}
|
selectedConnector={isEmpty(field.value) ? 'none' : field.value}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
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 { ActionParamsProps } from '../../../../../../triggers_actions_ui/public/types';
|
||||||
import { CommentType } from '../../../../../../case/common/api';
|
import { CommentType } from '../../../../../../case/common/api';
|
||||||
|
@ -21,13 +21,15 @@ import * as i18n from './translations';
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
${({ theme }) => `
|
${({ 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 = {
|
const defaultAlertComment = {
|
||||||
type: CommentType.generatedAlert,
|
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>> = ({
|
const CaseParamsFields: React.FunctionComponent<ActionParamsProps<CaseActionParams>> = ({
|
||||||
|
@ -90,12 +92,13 @@ const CaseParamsFields: React.FunctionComponent<ActionParamsProps<CaseActionPara
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Container>
|
||||||
<EuiCallOut size="s" title={i18n.CASE_CONNECTOR_CALL_OUT_INFO} iconType="iInCircle" />
|
<ExistingCase onCaseChanged={onCaseChanged} selectedCase={selectedCase} />
|
||||||
<Container>
|
<EuiSpacer size="m" />
|
||||||
<ExistingCase onCaseChanged={onCaseChanged} selectedCase={selectedCase} />
|
<EuiCallOut size="s" title={i18n.CASE_CONNECTOR_CALL_OUT_TITLE} iconType="iInCircle">
|
||||||
</Container>
|
<p>{i18n.CASE_CONNECTOR_CALL_OUT_MSG}</p>
|
||||||
</>
|
</EuiCallOut>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,22 +5,15 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import React, { memo, useMemo, useCallback } from 'react';
|
||||||
EuiButton,
|
|
||||||
EuiButtonIcon,
|
|
||||||
EuiCallOut,
|
|
||||||
EuiTextColor,
|
|
||||||
EuiLoadingSpinner,
|
|
||||||
} from '@elastic/eui';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import React, { memo, useEffect, useCallback, useState } from 'react';
|
|
||||||
import { CaseType } from '../../../../../../case/common/api';
|
import { CaseType } from '../../../../../../case/common/api';
|
||||||
import { Case } from '../../../containers/types';
|
import {
|
||||||
import { useDeleteCases } from '../../../containers/use_delete_cases';
|
useGetCases,
|
||||||
import { useGetCase } from '../../../containers/use_get_case';
|
DEFAULT_QUERY_PARAMS,
|
||||||
import { ConfirmDeleteCaseModal } from '../../confirm_delete_case';
|
DEFAULT_FILTER_OPTIONS,
|
||||||
|
} from '../../../containers/use_get_cases';
|
||||||
import { useCreateCaseModal } from '../../use_create_case_modal';
|
import { useCreateCaseModal } from '../../use_create_case_modal';
|
||||||
import * as i18n from './translations';
|
import { CasesDropdown, ADD_CASE_BUTTON_ID } from './cases_dropdown';
|
||||||
|
|
||||||
interface ExistingCaseProps {
|
interface ExistingCaseProps {
|
||||||
selectedCase: string | null;
|
selectedCase: string | null;
|
||||||
|
@ -28,76 +21,53 @@ interface ExistingCaseProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExistingCaseComponent: React.FC<ExistingCaseProps> = ({ onCaseChanged, selectedCase }) => {
|
const ExistingCaseComponent: React.FC<ExistingCaseProps> = ({ onCaseChanged, selectedCase }) => {
|
||||||
const { data, isLoading, isError } = useGetCase(selectedCase ?? '');
|
const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases(DEFAULT_QUERY_PARAMS, {
|
||||||
const [createdCase, setCreatedCase] = useState<Case | null>(null);
|
...DEFAULT_FILTER_OPTIONS,
|
||||||
|
onlyCollectionType: true,
|
||||||
|
});
|
||||||
|
|
||||||
const onCaseCreated = useCallback(
|
const onCaseCreated = useCallback(
|
||||||
(newCase: Case) => {
|
(newCase) => {
|
||||||
|
refetchCases();
|
||||||
onCaseChanged(newCase.id);
|
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 onChange = useCallback(
|
||||||
const {
|
(id: string) => {
|
||||||
dispatchResetIsDeleted,
|
if (id === ADD_CASE_BUTTON_ID) {
|
||||||
handleOnDeleteConfirm,
|
openModal();
|
||||||
handleToggleModal,
|
return;
|
||||||
isLoading: isDeleting,
|
}
|
||||||
isDeleted,
|
|
||||||
isDisplayConfirmDeleteModal,
|
|
||||||
} = useDeleteCases();
|
|
||||||
|
|
||||||
useEffect(() => {
|
onCaseChanged(id);
|
||||||
if (isDeleted) {
|
},
|
||||||
setCreatedCase(null);
|
[onCaseChanged, openModal]
|
||||||
onCaseChanged('');
|
);
|
||||||
dispatchResetIsDeleted();
|
|
||||||
}
|
|
||||||
// onCaseChanged and/or dispatchResetIsDeleted causes re-renders
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isDeleted]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const isCasesLoading = useMemo(
|
||||||
if (!isLoading && !isError && data != null) {
|
() => isLoadingCases.includes('cases') || isLoadingCases.includes('caseUpdate'),
|
||||||
setCreatedCase(data);
|
[isLoadingCases]
|
||||||
onCaseChanged(data.id);
|
);
|
||||||
}
|
|
||||||
// onCaseChanged causes re-renders
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [data, isLoading, isError]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{createdCase == null && isEmpty(selectedCase) && (
|
<CasesDropdown
|
||||||
<EuiButton fill fullWidth onClick={openModal}>
|
isLoading={isCasesLoading}
|
||||||
{i18n.CREATE_CASE}
|
cases={cases.cases}
|
||||||
</EuiButton>
|
selectedCase={selectedCase ?? undefined}
|
||||||
)}
|
onCaseChanged={onChange}
|
||||||
{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])}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{modal}
|
{modal}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const CASE_CONNECTOR_COMMENT_REQUIRED = i18n.translate(
|
||||||
export const CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL = i18n.translate(
|
export const CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL = i18n.translate(
|
||||||
'xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel',
|
'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(
|
export const CASE_CONNECTOR_CALL_OUT_TITLE = i18n.translate(
|
||||||
'xpack.securitySolution.case.components.connectors.case.callOutInfo',
|
'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 React, { memo, useCallback } from 'react';
|
||||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||||
|
|
||||||
|
import { ConnectorTypes } from '../../../../../case/common/api';
|
||||||
import { UseField, useFormData, FieldHook, useFormContext } from '../../../shared_imports';
|
import { UseField, useFormData, FieldHook, useFormContext } from '../../../shared_imports';
|
||||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||||
import { ConnectorSelector } from '../connector_selector/form';
|
import { ConnectorSelector } from '../connector_selector/form';
|
||||||
|
@ -18,19 +19,32 @@ import { FormProps } from './schema';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
hideConnectorServiceNowSir?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectorsFieldProps {
|
interface ConnectorsFieldProps {
|
||||||
connectors: ActionConnector[];
|
connectors: ActionConnector[];
|
||||||
field: FieldHook<FormProps['fields']>;
|
field: FieldHook<FormProps['fields']>;
|
||||||
isEdit: boolean;
|
isEdit: boolean;
|
||||||
|
hideConnectorServiceNowSir?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectorFields = ({ connectors, isEdit, field }: ConnectorsFieldProps) => {
|
const ConnectorFields = ({
|
||||||
|
connectors,
|
||||||
|
isEdit,
|
||||||
|
field,
|
||||||
|
hideConnectorServiceNowSir = false,
|
||||||
|
}: ConnectorsFieldProps) => {
|
||||||
const [{ connectorId }] = useFormData({ watch: ['connectorId'] });
|
const [{ connectorId }] = useFormData({ watch: ['connectorId'] });
|
||||||
const { setValue } = field;
|
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 (
|
return (
|
||||||
<ConnectorFieldsForm
|
<ConnectorFieldsForm
|
||||||
connector={connector}
|
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 { getFields } = useFormContext();
|
||||||
const { loading: isLoadingConnectors, connectors } = useConnectors();
|
const { loading: isLoadingConnectors, connectors } = useConnectors();
|
||||||
const handleConnectorChange = useCallback(
|
const handleConnectorChange = useCallback(
|
||||||
|
@ -61,6 +75,7 @@ const ConnectorComponent: React.FC<Props> = ({ isLoading }) => {
|
||||||
componentProps={{
|
componentProps={{
|
||||||
connectors,
|
connectors,
|
||||||
handleChange: handleConnectorChange,
|
handleChange: handleConnectorChange,
|
||||||
|
hideConnectorServiceNowSir,
|
||||||
dataTestSubj: 'caseConnectors',
|
dataTestSubj: 'caseConnectors',
|
||||||
disabled: isLoading || isLoadingConnectors,
|
disabled: isLoading || isLoadingConnectors,
|
||||||
idAria: 'caseConnectors',
|
idAria: 'caseConnectors',
|
||||||
|
@ -74,6 +89,7 @@ const ConnectorComponent: React.FC<Props> = ({ isLoading }) => {
|
||||||
component={ConnectorFields}
|
component={ConnectorFields}
|
||||||
componentProps={{
|
componentProps={{
|
||||||
connectors,
|
connectors,
|
||||||
|
hideConnectorServiceNowSir,
|
||||||
isEdit: true,
|
isEdit: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -36,78 +36,84 @@ const MySpinner = styled(EuiLoadingSpinner)`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
hideConnectorServiceNowSir?: boolean;
|
||||||
withSteps?: boolean;
|
withSteps?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateCaseForm: React.FC<Props> = React.memo(({ withSteps = true }) => {
|
export const CreateCaseForm: React.FC<Props> = React.memo(
|
||||||
const { isSubmitting } = useFormContext();
|
({ hideConnectorServiceNowSir = false, withSteps = true }) => {
|
||||||
|
const { isSubmitting } = useFormContext();
|
||||||
|
|
||||||
const firstStep = useMemo(
|
const firstStep = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
title: i18n.STEP_ONE_TITLE,
|
title: i18n.STEP_ONE_TITLE,
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<Title isLoading={isSubmitting} />
|
<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>
|
<Container>
|
||||||
<Tags isLoading={isSubmitting} />
|
<SyncAlertsToggle isLoading={isSubmitting} />
|
||||||
</Container>
|
</Container>
|
||||||
<Container big>
|
),
|
||||||
<Description isLoading={isSubmitting} />
|
}),
|
||||||
|
[isSubmitting]
|
||||||
|
);
|
||||||
|
|
||||||
|
const thirdStep = useMemo(
|
||||||
|
() => ({
|
||||||
|
title: i18n.STEP_THREE_TITLE,
|
||||||
|
children: (
|
||||||
|
<Container>
|
||||||
|
<Connector
|
||||||
|
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
),
|
||||||
),
|
}),
|
||||||
}),
|
[hideConnectorServiceNowSir, isSubmitting]
|
||||||
[isSubmitting]
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const secondStep = useMemo(
|
const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [
|
||||||
() => ({
|
firstStep,
|
||||||
title: i18n.STEP_TWO_TITLE,
|
secondStep,
|
||||||
children: (
|
thirdStep,
|
||||||
<Container>
|
]);
|
||||||
<SyncAlertsToggle isLoading={isSubmitting} />
|
|
||||||
</Container>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
[isSubmitting]
|
|
||||||
);
|
|
||||||
|
|
||||||
const thirdStep = useMemo(
|
return (
|
||||||
() => ({
|
<>
|
||||||
title: i18n.STEP_THREE_TITLE,
|
{isSubmitting && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />}
|
||||||
children: (
|
{withSteps ? (
|
||||||
<Container>
|
<EuiSteps
|
||||||
<Connector isLoading={isSubmitting} />
|
headingElement="h2"
|
||||||
</Container>
|
steps={allSteps}
|
||||||
),
|
data-test-subj={'case-creation-form-steps'}
|
||||||
}),
|
/>
|
||||||
[isSubmitting]
|
) : (
|
||||||
);
|
<>
|
||||||
|
{firstStep.children}
|
||||||
const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [
|
{secondStep.children}
|
||||||
firstStep,
|
{thirdStep.children}
|
||||||
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}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CreateCaseForm.displayName = 'CreateCaseForm';
|
CreateCaseForm.displayName = 'CreateCaseForm';
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service'
|
||||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||||
import { useCaseConfigure } from '../../containers/configure/use_configure';
|
import { useCaseConfigure } from '../../containers/configure/use_configure';
|
||||||
import { Case } from '../../containers/types';
|
import { Case } from '../../containers/types';
|
||||||
import { CaseType } from '../../../../../case/common/api';
|
import { CaseType, ConnectorTypes } from '../../../../../case/common/api';
|
||||||
|
|
||||||
const initialCaseValue: FormProps = {
|
const initialCaseValue: FormProps = {
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -31,29 +31,40 @@ const initialCaseValue: FormProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
caseType?: CaseType;
|
|
||||||
onSuccess?: (theCase: Case) => Promise<void>;
|
|
||||||
afterCaseCreated?: (theCase: Case) => Promise<void>;
|
afterCaseCreated?: (theCase: Case) => Promise<void>;
|
||||||
|
caseType?: CaseType;
|
||||||
|
hideConnectorServiceNowSir?: boolean;
|
||||||
|
onSuccess?: (theCase: Case) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormContext: React.FC<Props> = ({
|
export const FormContext: React.FC<Props> = ({
|
||||||
|
afterCaseCreated,
|
||||||
caseType = CaseType.individual,
|
caseType = CaseType.individual,
|
||||||
children,
|
children,
|
||||||
|
hideConnectorServiceNowSir,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
afterCaseCreated,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { connectors } = useConnectors();
|
const { connectors } = useConnectors();
|
||||||
const { connector: configurationConnector } = useCaseConfigure();
|
const { connector: configurationConnector } = useCaseConfigure();
|
||||||
const { postCase } = usePostCase();
|
const { postCase } = usePostCase();
|
||||||
const { pushCaseToExternalService } = usePostPushToService();
|
const { pushCaseToExternalService } = usePostPushToService();
|
||||||
|
|
||||||
const connectorId = useMemo(
|
const connectorId = useMemo(() => {
|
||||||
() =>
|
if (
|
||||||
connectors.some((connector) => connector.id === configurationConnector.id)
|
hideConnectorServiceNowSir &&
|
||||||
? configurationConnector.id
|
configurationConnector.type === ConnectorTypes.serviceNowSIR
|
||||||
: 'none',
|
) {
|
||||||
[configurationConnector.id, connectors]
|
return 'none';
|
||||||
);
|
}
|
||||||
|
return connectors.some((connector) => connector.id === configurationConnector.id)
|
||||||
|
? configurationConnector.id
|
||||||
|
: 'none';
|
||||||
|
}, [
|
||||||
|
configurationConnector.id,
|
||||||
|
configurationConnector.type,
|
||||||
|
connectors,
|
||||||
|
hideConnectorServiceNowSir,
|
||||||
|
]);
|
||||||
|
|
||||||
const submitCase = useCallback(
|
const submitCase = useCallback(
|
||||||
async (
|
async (
|
||||||
|
|
|
@ -34,7 +34,6 @@ import * as i18n from './translations';
|
||||||
interface EditConnectorProps {
|
interface EditConnectorProps {
|
||||||
caseFields: ConnectorTypeFields['fields'];
|
caseFields: ConnectorTypeFields['fields'];
|
||||||
connectors: ActionConnector[];
|
connectors: ActionConnector[];
|
||||||
disabled?: boolean;
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onSubmit: (
|
onSubmit: (
|
||||||
connectorId: string,
|
connectorId: string,
|
||||||
|
@ -44,6 +43,8 @@ interface EditConnectorProps {
|
||||||
) => void;
|
) => void;
|
||||||
selectedConnector: string;
|
selectedConnector: string;
|
||||||
userActions: CaseUserActions[];
|
userActions: CaseUserActions[];
|
||||||
|
disabled?: boolean;
|
||||||
|
hideConnectorServiceNowSir?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MyFlexGroup = styled(EuiFlexGroup)`
|
const MyFlexGroup = styled(EuiFlexGroup)`
|
||||||
|
@ -105,6 +106,7 @@ export const EditConnector = React.memo(
|
||||||
caseFields,
|
caseFields,
|
||||||
connectors,
|
connectors,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
hideConnectorServiceNowSir = false,
|
||||||
isLoading,
|
isLoading,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
selectedConnector,
|
selectedConnector,
|
||||||
|
@ -234,6 +236,7 @@ export const EditConnector = React.memo(
|
||||||
dataTestSubj: 'caseConnectors',
|
dataTestSubj: 'caseConnectors',
|
||||||
defaultValue: selectedConnector,
|
defaultValue: selectedConnector,
|
||||||
disabled,
|
disabled,
|
||||||
|
hideConnectorServiceNowSir,
|
||||||
idAria: 'caseConnectors',
|
idAria: 'caseConnectors',
|
||||||
isEdit: editConnector,
|
isEdit: editConnector,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|
|
@ -21,6 +21,7 @@ export interface CreateCaseModalProps {
|
||||||
onCloseCaseModal: () => void;
|
onCloseCaseModal: () => void;
|
||||||
onSuccess: (theCase: Case) => Promise<void>;
|
onSuccess: (theCase: Case) => Promise<void>;
|
||||||
caseType?: CaseType;
|
caseType?: CaseType;
|
||||||
|
hideConnectorServiceNowSir?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
@ -35,6 +36,7 @@ const CreateModalComponent: React.FC<CreateCaseModalProps> = ({
|
||||||
onCloseCaseModal,
|
onCloseCaseModal,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
caseType = CaseType.individual,
|
caseType = CaseType.individual,
|
||||||
|
hideConnectorServiceNowSir = false,
|
||||||
}) => {
|
}) => {
|
||||||
return isModalOpen ? (
|
return isModalOpen ? (
|
||||||
<EuiModal onClose={onCloseCaseModal} data-test-subj="all-cases-modal">
|
<EuiModal onClose={onCloseCaseModal} data-test-subj="all-cases-modal">
|
||||||
|
@ -42,8 +44,15 @@ const CreateModalComponent: React.FC<CreateCaseModalProps> = ({
|
||||||
<EuiModalHeaderTitle>{i18n.CREATE_TITLE}</EuiModalHeaderTitle>
|
<EuiModalHeaderTitle>{i18n.CREATE_TITLE}</EuiModalHeaderTitle>
|
||||||
</EuiModalHeader>
|
</EuiModalHeader>
|
||||||
<EuiModalBody>
|
<EuiModalBody>
|
||||||
<FormContext caseType={caseType} onSuccess={onSuccess}>
|
<FormContext
|
||||||
<CreateCaseForm withSteps={false} />
|
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||||
|
caseType={caseType}
|
||||||
|
onSuccess={onSuccess}
|
||||||
|
>
|
||||||
|
<CreateCaseForm
|
||||||
|
withSteps={false}
|
||||||
|
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||||
|
/>
|
||||||
<Container>
|
<Container>
|
||||||
<SubmitCaseButton />
|
<SubmitCaseButton />
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { CreateCaseModal } from './create_case_modal';
|
||||||
export interface UseCreateCaseModalProps {
|
export interface UseCreateCaseModalProps {
|
||||||
onCaseCreated: (theCase: Case) => void;
|
onCaseCreated: (theCase: Case) => void;
|
||||||
caseType?: CaseType;
|
caseType?: CaseType;
|
||||||
|
hideConnectorServiceNowSir?: boolean;
|
||||||
}
|
}
|
||||||
export interface UseCreateCaseModalReturnedValues {
|
export interface UseCreateCaseModalReturnedValues {
|
||||||
modal: JSX.Element;
|
modal: JSX.Element;
|
||||||
|
@ -24,6 +25,7 @@ export interface UseCreateCaseModalReturnedValues {
|
||||||
export const useCreateCaseModal = ({
|
export const useCreateCaseModal = ({
|
||||||
caseType = CaseType.individual,
|
caseType = CaseType.individual,
|
||||||
onCaseCreated,
|
onCaseCreated,
|
||||||
|
hideConnectorServiceNowSir = false,
|
||||||
}: UseCreateCaseModalProps) => {
|
}: UseCreateCaseModalProps) => {
|
||||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||||
const closeModal = useCallback(() => setIsModalOpen(false), []);
|
const closeModal = useCallback(() => setIsModalOpen(false), []);
|
||||||
|
@ -41,6 +43,7 @@ export const useCreateCaseModal = ({
|
||||||
modal: (
|
modal: (
|
||||||
<CreateCaseModal
|
<CreateCaseModal
|
||||||
caseType={caseType}
|
caseType={caseType}
|
||||||
|
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
|
||||||
isModalOpen={isModalOpen}
|
isModalOpen={isModalOpen}
|
||||||
onCloseCaseModal={closeModal}
|
onCloseCaseModal={closeModal}
|
||||||
onSuccess={onSuccess}
|
onSuccess={onSuccess}
|
||||||
|
@ -50,7 +53,7 @@ export const useCreateCaseModal = ({
|
||||||
closeModal,
|
closeModal,
|
||||||
openModal,
|
openModal,
|
||||||
}),
|
}),
|
||||||
[caseType, closeModal, isModalOpen, onSuccess, openModal]
|
[caseType, closeModal, hideConnectorServiceNowSir, isModalOpen, onSuccess, openModal]
|
||||||
);
|
);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
CasesResponse,
|
CasesResponse,
|
||||||
CasesStatusResponse,
|
CasesStatusResponse,
|
||||||
CaseStatuses,
|
CaseStatuses,
|
||||||
|
CaseType,
|
||||||
CaseUserActionsResponse,
|
CaseUserActionsResponse,
|
||||||
CommentRequest,
|
CommentRequest,
|
||||||
CommentType,
|
CommentType,
|
||||||
|
@ -165,6 +166,7 @@ export const getSubCaseUserActions = async (
|
||||||
|
|
||||||
export const getCases = async ({
|
export const getCases = async ({
|
||||||
filterOptions = {
|
filterOptions = {
|
||||||
|
onlyCollectionType: false,
|
||||||
search: '',
|
search: '',
|
||||||
reporters: [],
|
reporters: [],
|
||||||
status: CaseStatuses.open,
|
status: CaseStatuses.open,
|
||||||
|
@ -183,6 +185,7 @@ export const getCases = async ({
|
||||||
tags: filterOptions.tags.map((t) => `"${t.replace(/"/g, '\\"')}"`),
|
tags: filterOptions.tags.map((t) => `"${t.replace(/"/g, '\\"')}"`),
|
||||||
status: filterOptions.status,
|
status: filterOptions.status,
|
||||||
...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}),
|
...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}),
|
||||||
|
...(filterOptions.onlyCollectionType ? { type: CaseType.collection } : {}),
|
||||||
...queryParams,
|
...queryParams,
|
||||||
};
|
};
|
||||||
const response = await KibanaServices.get().http.fetch<CasesFindResponse>(`${CASES_URL}/_find`, {
|
const response = await KibanaServices.get().http.fetch<CasesFindResponse>(`${CASES_URL}/_find`, {
|
||||||
|
|
|
@ -99,6 +99,7 @@ export interface FilterOptions {
|
||||||
status: CaseStatuses;
|
status: CaseStatuses;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
reporters: User[];
|
reporters: User[];
|
||||||
|
onlyCollectionType?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CasesStatus {
|
export interface CasesStatus {
|
||||||
|
|
|
@ -97,6 +97,7 @@ export const DEFAULT_FILTER_OPTIONS: FilterOptions = {
|
||||||
reporters: [],
|
reporters: [],
|
||||||
status: CaseStatuses.open,
|
status: CaseStatuses.open,
|
||||||
tags: [],
|
tags: [],
|
||||||
|
onlyCollectionType: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_QUERY_PARAMS: QueryParams = {
|
export const DEFAULT_QUERY_PARAMS: QueryParams = {
|
||||||
|
@ -129,10 +130,13 @@ export interface UseGetCases extends UseGetCasesState {
|
||||||
setSelectedCases: (mySelectedCases: Case[]) => void;
|
setSelectedCases: (mySelectedCases: Case[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => {
|
export const useGetCases = (
|
||||||
|
initialQueryParams?: QueryParams,
|
||||||
|
initialFilterOptions?: FilterOptions
|
||||||
|
): UseGetCases => {
|
||||||
const [state, dispatch] = useReducer(dataFetchReducer, {
|
const [state, dispatch] = useReducer(dataFetchReducer, {
|
||||||
data: initialData,
|
data: initialData,
|
||||||
filterOptions: DEFAULT_FILTER_OPTIONS,
|
filterOptions: initialFilterOptions ?? DEFAULT_FILTER_OPTIONS,
|
||||||
isError: false,
|
isError: false,
|
||||||
loading: [],
|
loading: [],
|
||||||
queryParams: initialQueryParams ?? DEFAULT_QUERY_PARAMS,
|
queryParams: initialQueryParams ?? DEFAULT_QUERY_PARAMS,
|
||||||
|
|
|
@ -8,10 +8,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import { RuleActionsField } from './index';
|
import { getSupportedActions, RuleActionsField } from './index';
|
||||||
import { useForm, Form } from '../../../../shared_imports';
|
import { useForm, Form } from '../../../../shared_imports';
|
||||||
import { useKibana } from '../../../../common/lib/kibana';
|
import { useKibana } from '../../../../common/lib/kibana';
|
||||||
import { useFormFieldMock } from '../../../../common/mock';
|
import { useFormFieldMock } from '../../../../common/mock';
|
||||||
|
import { ActionType } from '../../../../../../actions/common';
|
||||||
jest.mock('../../../../common/lib/kibana');
|
jest.mock('../../../../common/lib/kibana');
|
||||||
|
|
||||||
describe('RuleActionsField', () => {
|
describe('RuleActionsField', () => {
|
||||||
|
@ -45,7 +46,11 @@ describe('RuleActionsField', () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form form={form}>
|
<Form form={form}>
|
||||||
<RuleActionsField field={field} messageVariables={messageVariables} />
|
<RuleActionsField
|
||||||
|
field={field}
|
||||||
|
messageVariables={messageVariables}
|
||||||
|
hasErrorOnCreationCaseAction={false}
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -53,4 +58,63 @@ describe('RuleActionsField', () => {
|
||||||
|
|
||||||
expect(wrapper.dive().find('ActionForm')).toHaveLength(0);
|
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 {
|
interface Props {
|
||||||
field: FieldHook;
|
field: FieldHook;
|
||||||
|
hasErrorOnCreationCaseAction: boolean;
|
||||||
messageVariables: ActionVariables;
|
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 [fieldErrors, setFieldErrors] = useState<string | null>(null);
|
||||||
const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>();
|
const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>();
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
|
@ -54,6 +92,17 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
||||||
[field.value]
|
[field.value]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const caseActionIndexes = useMemo(
|
||||||
|
() =>
|
||||||
|
actions.reduce<string[]>((acc, action, actionIndex) => {
|
||||||
|
if (action.actionTypeId === '.case') {
|
||||||
|
return [...acc, `${actionIndex}`];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []),
|
||||||
|
[actions]
|
||||||
|
);
|
||||||
|
|
||||||
const setActionIdByIndex = useCallback(
|
const setActionIdByIndex = useCallback(
|
||||||
(id: string, index: number) => {
|
(id: string, index: number) => {
|
||||||
const updatedActions = [...(actions as Array<Partial<AlertAction>>)];
|
const updatedActions = [...(actions as Array<Partial<AlertAction>>)];
|
||||||
|
@ -83,13 +132,11 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function () {
|
(async function () {
|
||||||
const actionTypes = await loadActionTypes({ http });
|
const actionTypes = await loadActionTypes({ http });
|
||||||
const supportedTypes = actionTypes.filter((actionType) =>
|
const supportedTypes = getSupportedActions(actionTypes, hasErrorOnCreationCaseAction);
|
||||||
NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id)
|
|
||||||
);
|
|
||||||
setSupportedActionTypes(supportedTypes);
|
setSupportedActionTypes(supportedTypes);
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, [hasErrorOnCreationCaseAction]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSubmitting || !field.errors.length) {
|
if (isSubmitting || !field.errors.length) {
|
||||||
|
@ -104,7 +151,7 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
||||||
if (!supportedActionTypes) return <></>;
|
if (!supportedActionTypes) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ContainerActions $caseIndexes={caseActionIndexes}>
|
||||||
{fieldErrors ? (
|
{fieldErrors ? (
|
||||||
<>
|
<>
|
||||||
<FieldErrorsContainer>
|
<FieldErrorsContainer>
|
||||||
|
@ -126,6 +173,6 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
||||||
actionTypes={supportedActionTypes}
|
actionTypes={supportedActionTypes}
|
||||||
defaultActionMessage={DEFAULT_ACTION_MESSAGE}
|
defaultActionMessage={DEFAULT_ACTION_MESSAGE}
|
||||||
/>
|
/>
|
||||||
</>
|
</ContainerActions>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { useKibana } from '../../../../common/lib/kibana';
|
||||||
import { getSchema } from './schema';
|
import { getSchema } from './schema';
|
||||||
import * as I18n from './translations';
|
import * as I18n from './translations';
|
||||||
import { APP_ID } from '../../../../../common/constants';
|
import { APP_ID } from '../../../../../common/constants';
|
||||||
|
import { useManageCaseAction } from './use_manage_case_action';
|
||||||
|
|
||||||
interface StepRuleActionsProps extends RuleStepProps {
|
interface StepRuleActionsProps extends RuleStepProps {
|
||||||
defaultValues?: ActionsStepRule | null;
|
defaultValues?: ActionsStepRule | null;
|
||||||
|
@ -70,6 +71,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
||||||
setForm,
|
setForm,
|
||||||
actionMessageParams,
|
actionMessageParams,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isLoadingCaseAction, hasErrorOnCreationCaseAction] = useManageCaseAction();
|
||||||
const {
|
const {
|
||||||
services: {
|
services: {
|
||||||
application,
|
application,
|
||||||
|
@ -138,13 +140,14 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
||||||
() => ({
|
() => ({
|
||||||
idAria: 'detectionEngineStepRuleActionsThrottle',
|
idAria: 'detectionEngineStepRuleActionsThrottle',
|
||||||
isDisabled: isLoading,
|
isDisabled: isLoading,
|
||||||
|
isLoading: isLoadingCaseAction,
|
||||||
dataTestSubj: 'detectionEngineStepRuleActionsThrottle',
|
dataTestSubj: 'detectionEngineStepRuleActionsThrottle',
|
||||||
hasNoInitialSelection: false,
|
hasNoInitialSelection: false,
|
||||||
euiFieldProps: {
|
euiFieldProps: {
|
||||||
options: throttleOptions,
|
options: throttleOptions,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[isLoading, throttleOptions]
|
[isLoading, isLoadingCaseAction, throttleOptions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const displayActionsOptions = useMemo(
|
const displayActionsOptions = useMemo(
|
||||||
|
@ -157,13 +160,14 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
||||||
component={RuleActionsField}
|
component={RuleActionsField}
|
||||||
componentProps={{
|
componentProps={{
|
||||||
messageVariables: actionMessageParams,
|
messageVariables: actionMessageParams,
|
||||||
|
hasErrorOnCreationCaseAction,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<UseField path="actions" component={GhostFormField} />
|
<UseField path="actions" component={GhostFormField} />
|
||||||
),
|
),
|
||||||
[throttle, actionMessageParams]
|
[throttle, actionMessageParams, hasErrorOnCreationCaseAction]
|
||||||
);
|
);
|
||||||
// only display the actions dropdown if the user has "read" privileges for actions
|
// only display the actions dropdown if the user has "read" privileges for actions
|
||||||
const displayActionsDropDown = useMemo(() => {
|
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.common.noConnector": "コネクターを選択していません",
|
||||||
"xpack.securitySolution.case.components.connectors.case.actionTypeTitle": "ケース",
|
"xpack.securitySolution.case.components.connectors.case.actionTypeTitle": "ケース",
|
||||||
"xpack.securitySolution.case.components.connectors.case.addNewCaseOption": "新規ケースの追加",
|
"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.caseRequired": "ケースの選択が必要です。",
|
||||||
"xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder": "ケースを選択",
|
"xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder": "ケースを選択",
|
||||||
"xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel": "ケース",
|
"xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel": "ケース",
|
||||||
|
|
|
@ -17242,7 +17242,6 @@
|
||||||
"xpack.securitySolution.case.common.noConnector": "未选择任何连接器",
|
"xpack.securitySolution.case.common.noConnector": "未选择任何连接器",
|
||||||
"xpack.securitySolution.case.components.connectors.case.actionTypeTitle": "案例",
|
"xpack.securitySolution.case.components.connectors.case.actionTypeTitle": "案例",
|
||||||
"xpack.securitySolution.case.components.connectors.case.addNewCaseOption": "添加新案例",
|
"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.caseRequired": "必须选择策略。",
|
||||||
"xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder": "选择案例",
|
"xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder": "选择案例",
|
||||||
"xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel": "案例",
|
"xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel": "案例",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue