[Cases] Swimlane: Fix bug when creating the connector with empty mapping. (#103446)
This commit is contained in:
parent
affe82b73c
commit
3ac067fc91
|
@ -4,21 +4,12 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { EuiCallOut, EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import * as i18n from '../translations';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { useGetApplication } from '../use_get_application';
|
||||
import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from '../types';
|
||||
import { SwimlaneActionConnector } from '../types';
|
||||
import { IErrorObject } from '../../../../../types';
|
||||
|
||||
interface Props {
|
||||
|
@ -27,8 +18,6 @@ interface Props {
|
|||
editActionSecrets: (property: string, value: any) => void;
|
||||
errors: IErrorObject;
|
||||
readOnly: boolean;
|
||||
updateCurrentStep: (step: number) => void;
|
||||
updateFields: (items: SwimlaneFieldMappingConfig[]) => void;
|
||||
}
|
||||
|
||||
const SwimlaneConnectionComponent: React.FunctionComponent<Props> = ({
|
||||
|
@ -37,33 +26,10 @@ const SwimlaneConnectionComponent: React.FunctionComponent<Props> = ({
|
|||
editActionSecrets,
|
||||
errors,
|
||||
readOnly,
|
||||
updateCurrentStep,
|
||||
updateFields,
|
||||
}) => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const { apiUrl, appId } = action.config;
|
||||
const { apiToken } = action.secrets;
|
||||
const { docLinks } = useKibana().services;
|
||||
const { getApplication } = useGetApplication({
|
||||
toastNotifications: toasts,
|
||||
apiToken,
|
||||
appId,
|
||||
apiUrl,
|
||||
});
|
||||
const isValid = apiUrl && apiToken && appId;
|
||||
|
||||
const connectSwimlane = useCallback(async () => {
|
||||
// fetch swimlane application configuration
|
||||
const application = await getApplication();
|
||||
|
||||
if (application?.fields) {
|
||||
const allFields = application.fields;
|
||||
updateFields(allFields);
|
||||
updateCurrentStep(2);
|
||||
}
|
||||
}, [getApplication, updateCurrentStep, updateFields]);
|
||||
|
||||
const onChangeConfig = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>, key: 'apiUrl' | 'appId') => {
|
||||
|
@ -186,14 +152,6 @@ const SwimlaneConnectionComponent: React.FunctionComponent<Props> = ({
|
|||
/>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiButton
|
||||
disabled={!isValid}
|
||||
onClick={connectSwimlane}
|
||||
data-test-subj="swimlaneConfigureMapping"
|
||||
>
|
||||
{i18n.SW_RETRIEVE_CONFIGURATION_LABEL}
|
||||
</EuiButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,13 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFormRow,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiButtonGroup,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption, EuiButtonGroup } from '@elastic/eui';
|
||||
import * as i18n from '../translations';
|
||||
import {
|
||||
SwimlaneActionConnector,
|
||||
|
@ -102,10 +96,6 @@ const SwimlaneFieldsComponent: React.FC<Props> = ({
|
|||
[errors]
|
||||
);
|
||||
|
||||
const resetConnection = useCallback(() => {
|
||||
updateCurrentStep(1);
|
||||
}, [updateCurrentStep]);
|
||||
|
||||
const editMappings = useCallback(
|
||||
(key: keyof SwimlaneMappingConfig, e: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
if (e.length === 0) {
|
||||
|
@ -132,14 +122,6 @@ const SwimlaneFieldsComponent: React.FC<Props> = ({
|
|||
[editActionConfig, fieldIdMap, mappings]
|
||||
);
|
||||
|
||||
/**
|
||||
* Connector type needs to be updated on mount to All.
|
||||
* Otherwise it is undefined and this will cause an error
|
||||
* if the user saves the connector without any mapping
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => editActionConfig('connectorType', connectorType), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectorType !== prevConnectorType.current) {
|
||||
prevConnectorType.current = connectorType;
|
||||
|
@ -305,7 +287,6 @@ const SwimlaneFieldsComponent: React.FC<Props> = ({
|
|||
</EuiFormRow>
|
||||
</>
|
||||
)}
|
||||
<EuiButton onClick={resetConnection}>{i18n.SW_CONFIGURE_API_LABEL}</EuiButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
ConnectorValidationResult,
|
||||
|
@ -18,10 +19,23 @@ import {
|
|||
SwimlaneSecrets,
|
||||
SwimlaneActionParams,
|
||||
} from './types';
|
||||
import * as i18n from './translations';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
import { validateMappingForConnector } from './helpers';
|
||||
|
||||
export const SW_SELECT_MESSAGE_TEXT = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create record in Swimlane',
|
||||
}
|
||||
);
|
||||
|
||||
export const SW_ACTION_TYPE_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Create Swimlane Record',
|
||||
}
|
||||
);
|
||||
|
||||
export function getActionType(): ActionTypeModel<
|
||||
SwimlaneConfig,
|
||||
SwimlaneSecrets,
|
||||
|
@ -30,11 +44,12 @@ export function getActionType(): ActionTypeModel<
|
|||
return {
|
||||
id: '.swimlane',
|
||||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: i18n.SW_SELECT_MESSAGE_TEXT,
|
||||
actionTypeTitle: i18n.SW_ACTION_TYPE_TITLE,
|
||||
selectMessage: SW_SELECT_MESSAGE_TEXT,
|
||||
actionTypeTitle: SW_ACTION_TYPE_TITLE,
|
||||
validateConnector: async (
|
||||
action: SwimlaneActionConnector
|
||||
): Promise<ConnectorValidationResult<SwimlaneConfig, SwimlaneSecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
apiUrl: new Array<string>(),
|
||||
appId: new Array<string>(),
|
||||
|
@ -51,19 +66,22 @@ export function getActionType(): ActionTypeModel<
|
|||
};
|
||||
|
||||
if (!action.config.apiUrl) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, i18n.SW_API_URL_REQUIRED];
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.SW_API_URL_REQUIRED];
|
||||
} else if (action.config.apiUrl) {
|
||||
if (!isValidUrl(action.config.apiUrl)) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, i18n.SW_API_URL_INVALID];
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.SW_API_URL_INVALID];
|
||||
}
|
||||
}
|
||||
|
||||
if (!action.secrets.apiToken) {
|
||||
secretsErrors.apiToken = [...secretsErrors.apiToken, i18n.SW_REQUIRED_API_TOKEN_TEXT];
|
||||
secretsErrors.apiToken = [
|
||||
...secretsErrors.apiToken,
|
||||
translations.SW_REQUIRED_API_TOKEN_TEXT,
|
||||
];
|
||||
}
|
||||
|
||||
if (!action.config.appId) {
|
||||
configErrors.appId = [...configErrors.appId, i18n.SW_REQUIRED_APP_ID_TEXT];
|
||||
configErrors.appId = [...configErrors.appId, translations.SW_REQUIRED_APP_ID_TEXT];
|
||||
}
|
||||
|
||||
const mappingErrors = validateMappingForConnector(
|
||||
|
@ -80,6 +98,7 @@ export function getActionType(): ActionTypeModel<
|
|||
validateParams: async (
|
||||
actionParams: SwimlaneActionParams
|
||||
): Promise<GenericValidationResult<unknown>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
'subActionParams.incident.ruleName': new Array<string>(),
|
||||
'subActionParams.incident.alertId': new Array<string>(),
|
||||
|
@ -91,11 +110,11 @@ export function getActionType(): ActionTypeModel<
|
|||
const hasIncident = actionParams.subActionParams && actionParams.subActionParams.incident;
|
||||
|
||||
if (hasIncident && !actionParams.subActionParams.incident.ruleName?.length) {
|
||||
errors['subActionParams.incident.ruleName'].push(i18n.SW_REQUIRED_RULE_NAME);
|
||||
errors['subActionParams.incident.ruleName'].push(translations.SW_REQUIRED_RULE_NAME);
|
||||
}
|
||||
|
||||
if (hasIncident && !actionParams.subActionParams.incident.alertId?.length) {
|
||||
errors['subActionParams.incident.alertId'].push(i18n.SW_REQUIRED_ALERT_ID);
|
||||
errors['subActionParams.incident.alertId'].push(translations.SW_REQUIRED_ALERT_ID);
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
|
|
|
@ -5,94 +5,153 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useMemo, useState } from 'react';
|
||||
import { EuiForm, EuiSpacer, EuiStepsHorizontal, EuiStepStatus } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
import React, { Fragment, useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import {
|
||||
EuiForm,
|
||||
EuiSpacer,
|
||||
EuiStepsHorizontal,
|
||||
EuiStepStatus,
|
||||
EuiButton,
|
||||
EuiFormRow,
|
||||
} from '@elastic/eui';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from './types';
|
||||
import {
|
||||
SwimlaneActionConnector,
|
||||
SwimlaneConnectorType,
|
||||
SwimlaneFieldMappingConfig,
|
||||
} from './types';
|
||||
import { SwimlaneConnection, SwimlaneFields } from './steps';
|
||||
import { useGetApplication } from './use_get_application';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const SwimlaneActionConnectorFields: React.FunctionComponent<
|
||||
ActionConnectorFieldsProps<SwimlaneActionConnector>
|
||||
> = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const { apiUrl, appId, mappings, connectorType } = action.config;
|
||||
const { apiToken } = action.secrets;
|
||||
|
||||
const { getApplication, isLoading: isLoadingApplication } = useGetApplication({
|
||||
toastNotifications: toasts,
|
||||
apiToken,
|
||||
appId,
|
||||
apiUrl,
|
||||
});
|
||||
|
||||
const hasConfigurationErrors =
|
||||
errors.apiUrl?.length > 0 || errors.appId?.length > 0 || errors.apiToken?.length > 0;
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<number>(1);
|
||||
const [stepsStatuses, setStepsStatuses] = useState<{
|
||||
connection: EuiStepStatus;
|
||||
fields: EuiStepStatus;
|
||||
}>({ connection: 'incomplete', fields: 'incomplete' });
|
||||
const [fields, setFields] = useState<SwimlaneFieldMappingConfig[]>([]);
|
||||
|
||||
const updateCurrentStep = useCallback(
|
||||
(step: number) => {
|
||||
setCurrentStep(step);
|
||||
if (step === 2) {
|
||||
setStepsStatuses((statuses) => ({ ...statuses, connection: 'complete' }));
|
||||
} else if (step === 1) {
|
||||
setStepsStatuses({
|
||||
fields: 'incomplete',
|
||||
connection: 'incomplete',
|
||||
});
|
||||
editActionConfig('mappings', action.config.mappings);
|
||||
}
|
||||
},
|
||||
[action.config.mappings, editActionConfig]
|
||||
const updateCurrentStep = useCallback((step: number) => {
|
||||
setCurrentStep(step);
|
||||
}, []);
|
||||
|
||||
const onNextStep = useCallback(async () => {
|
||||
// fetch swimlane application configuration
|
||||
const application = await getApplication();
|
||||
|
||||
if (application?.fields) {
|
||||
const allFields = application.fields;
|
||||
setFields(allFields);
|
||||
setCurrentStep(2);
|
||||
}
|
||||
}, [getApplication]);
|
||||
|
||||
const resetConnection = useCallback(() => {
|
||||
setCurrentStep(1);
|
||||
}, []);
|
||||
|
||||
const hasMappingErrors = useMemo(
|
||||
() => Object.values(errors?.mappings ?? {}).some((mappingError) => mappingError.length !== 0),
|
||||
[errors?.mappings]
|
||||
);
|
||||
|
||||
const setupSteps = useMemo(
|
||||
const steps = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: i18n.SW_CONFIGURE_CONNECTION_LABEL,
|
||||
status: stepsStatuses.connection,
|
||||
isSelected: currentStep === 1,
|
||||
isComplete: currentStep === 2,
|
||||
onClick: () => updateCurrentStep(1),
|
||||
},
|
||||
{
|
||||
title: i18n.SW_MAPPING_TITLE_TEXT_FIELD_LABEL,
|
||||
disabled: stepsStatuses.connection !== 'complete',
|
||||
status: stepsStatuses.fields,
|
||||
onClick: () => updateCurrentStep(2),
|
||||
disabled: hasConfigurationErrors || isLoadingApplication,
|
||||
isSelected: currentStep === 2,
|
||||
onClick: onNextStep,
|
||||
status: hasMappingErrors ? ('danger' as EuiStepStatus) : undefined,
|
||||
},
|
||||
],
|
||||
[stepsStatuses.connection, stepsStatuses.fields, updateCurrentStep]
|
||||
[
|
||||
currentStep,
|
||||
hasConfigurationErrors,
|
||||
hasMappingErrors,
|
||||
isLoadingApplication,
|
||||
onNextStep,
|
||||
updateCurrentStep,
|
||||
]
|
||||
);
|
||||
|
||||
const editActionConfigCb = useCallback(
|
||||
(k: string, v: string) => {
|
||||
editActionConfig(k, v);
|
||||
if (
|
||||
Object.values(errors?.mappings ?? {}).every((mappingError) => mappingError.length === 0)
|
||||
) {
|
||||
setStepsStatuses((statuses) => ({ ...statuses, fields: 'complete' }));
|
||||
} else {
|
||||
setStepsStatuses((statuses) => ({ ...statuses, fields: 'incomplete' }));
|
||||
}
|
||||
},
|
||||
[editActionConfig, errors?.mappings]
|
||||
);
|
||||
/**
|
||||
* Connector type needs to be updated on mount to All.
|
||||
* Otherwise it is undefined and this will cause an error
|
||||
* if the user saves the connector without going to the
|
||||
* second step. Same for mapping.
|
||||
*/
|
||||
useEffect(() => {
|
||||
editActionConfig('connectorType', connectorType ?? SwimlaneConnectorType.All);
|
||||
editActionConfig('mappings', mappings ?? {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiStepsHorizontal steps={setupSteps} />
|
||||
<EuiStepsHorizontal steps={steps} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiForm>
|
||||
{currentStep === 1 && (
|
||||
<SwimlaneConnection
|
||||
action={action}
|
||||
editActionConfig={editActionConfigCb}
|
||||
editActionSecrets={editActionSecrets}
|
||||
readOnly={readOnly}
|
||||
errors={errors}
|
||||
updateCurrentStep={updateCurrentStep}
|
||||
updateFields={setFields}
|
||||
/>
|
||||
<>
|
||||
<SwimlaneConnection
|
||||
action={action}
|
||||
editActionConfig={editActionConfig}
|
||||
editActionSecrets={editActionSecrets}
|
||||
readOnly={readOnly}
|
||||
errors={errors}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow fullWidth helpText={i18n.SW_FIELDS_BUTTON_HELP_TEXT}>
|
||||
<EuiButton
|
||||
disabled={hasConfigurationErrors || isLoadingApplication}
|
||||
isLoading={isLoadingApplication}
|
||||
onClick={onNextStep}
|
||||
data-test-subj="swimlaneConfigureMapping"
|
||||
iconType="arrowRight"
|
||||
iconSide="right"
|
||||
>
|
||||
{i18n.SW_NEXT}
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<SwimlaneFields
|
||||
action={action}
|
||||
editActionConfig={editActionConfigCb}
|
||||
updateCurrentStep={updateCurrentStep}
|
||||
fields={fields}
|
||||
errors={errors}
|
||||
/>
|
||||
<>
|
||||
<SwimlaneFields
|
||||
action={action}
|
||||
editActionConfig={editActionConfig}
|
||||
updateCurrentStep={updateCurrentStep}
|
||||
fields={fields}
|
||||
errors={errors}
|
||||
/>
|
||||
<EuiButton onClick={resetConnection} iconType="arrowLeft">
|
||||
{i18n.SW_BACK}
|
||||
</EuiButton>
|
||||
</>
|
||||
)}
|
||||
</EuiForm>
|
||||
</Fragment>
|
||||
|
|
|
@ -7,20 +7,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SW_SELECT_MESSAGE_TEXT = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create record in Swimlane',
|
||||
}
|
||||
);
|
||||
|
||||
export const SW_ACTION_TYPE_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Create Swimlane Record',
|
||||
}
|
||||
);
|
||||
|
||||
export const SW_REQUIRED_RULE_NAME = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredRuleName',
|
||||
{
|
||||
|
@ -190,11 +176,6 @@ export const SW_RETRIEVE_CONFIGURATION_LABEL = i18n.translate(
|
|||
{ defaultMessage: 'Configure Fields' }
|
||||
);
|
||||
|
||||
export const SW_CONFIGURE_API_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.configureAPILabel',
|
||||
{ defaultMessage: 'Configure API' }
|
||||
);
|
||||
|
||||
export const SW_CONNECTOR_TYPE_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.connectorType',
|
||||
{
|
||||
|
@ -220,7 +201,7 @@ export const EMPTY_MAPPING_WARNING_DESC = i18n.translate(
|
|||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningDesc',
|
||||
{
|
||||
defaultMessage:
|
||||
'This connector cannot be selected because it is missing the required case field mappings. You can edit this connector to add required field mappings or select a connector of type Alerts.',
|
||||
'This connector cannot be selected because it is missing the required alert field mappings. You can edit this connector to add required field mappings or select a connector of type Alerts.',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -273,10 +254,24 @@ export const SW_REQUIRED_ALERT_ID = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SW_ALERT_SOURCE_TOOLTIP = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceTooltip',
|
||||
export const SW_BACK = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.prevStep',
|
||||
{
|
||||
defaultMessage: 'The index of the alert. Use {index} in Detections.',
|
||||
values: { index: '{{context.rule.output_index}}' },
|
||||
defaultMessage: 'Back',
|
||||
}
|
||||
);
|
||||
|
||||
export const SW_NEXT = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStep',
|
||||
{
|
||||
defaultMessage: 'Next',
|
||||
}
|
||||
);
|
||||
|
||||
export const SW_FIELDS_BUTTON_HELP_TEXT = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStepHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'If field mappings are not configured, Swimlane connector type will be set to all.',
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue