[Alerting UI] Reduced triggersActionsUi bundle size by making all action types UI validation messages translations asynchronous. (#100525)
* [Alerting UI] Reduced triggersActionsUi bundle size by making all connectors validation messages translations asyncronus. * changed validation logic to be async * fixed action form * fixed tests * fixed tests * fixed validation usage in security * fixed due to comments * fixed due to comments * added spinner for the validation awaiting * fixed typechecks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
71b4c38c4a
commit
45ae6cc39b
|
@ -25,7 +25,7 @@ const validateParams = (actionParams: CaseActionParams) => {
|
|||
validationResult.errors.caseId.push(i18n.CASE_CONNECTOR_CASE_REQUIRED);
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
};
|
||||
|
||||
export function getActionType(): ActionTypeModel {
|
||||
|
@ -34,7 +34,7 @@ export function getActionType(): ActionTypeModel {
|
|||
iconClass: 'securityAnalyticsApp',
|
||||
selectMessage: i18n.CASE_CONNECTOR_DESC,
|
||||
actionTypeTitle: i18n.CASE_CONNECTOR_TITLE,
|
||||
validateConnector: () => ({ config: { errors: {} }, secrets: { errors: {} } }),
|
||||
validateConnector: () => Promise.resolve({ config: { errors: {} }, secrets: { errors: {} } }),
|
||||
validateParams,
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: lazy(() => import('./alert_fields')),
|
||||
|
|
|
@ -94,12 +94,12 @@ describe('alert_form', () => {
|
|||
id: 'alert-action-type',
|
||||
iconClass: '',
|
||||
selectMessage: '',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
|
|
|
@ -89,6 +89,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
...(defaultValues ?? stepActionsDefaultValue),
|
||||
kibanaSiemAppUrl: kibanaAbsoluteUrl,
|
||||
};
|
||||
|
||||
const schema = useMemo(() => getSchema({ actionTypeRegistry }), [actionTypeRegistry]);
|
||||
const { form } = useForm<ActionsStepRule>({
|
||||
defaultValue: initialState,
|
||||
|
|
|
@ -15,13 +15,13 @@ describe('stepRuleActions schema', () => {
|
|||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
|
||||
describe('validateSingleAction', () => {
|
||||
it('should validate single action', () => {
|
||||
it('should validate single action', async () => {
|
||||
(isUuid as jest.Mock).mockReturnValue(true);
|
||||
(validateActionParams as jest.Mock).mockReturnValue([]);
|
||||
(validateMustache as jest.Mock).mockReturnValue([]);
|
||||
|
||||
expect(
|
||||
validateSingleAction(
|
||||
await validateSingleAction(
|
||||
{
|
||||
id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4',
|
||||
group: 'default',
|
||||
|
@ -33,12 +33,12 @@ describe('stepRuleActions schema', () => {
|
|||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should validate single action with invalid mustache template', () => {
|
||||
it('should validate single action with invalid mustache template', async () => {
|
||||
(isUuid as jest.Mock).mockReturnValue(true);
|
||||
(validateActionParams as jest.Mock).mockReturnValue([]);
|
||||
(validateMustache as jest.Mock).mockReturnValue(['Message is not valid mustache template']);
|
||||
|
||||
const errors = validateSingleAction(
|
||||
const errors = await validateSingleAction(
|
||||
{
|
||||
id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4',
|
||||
group: 'default',
|
||||
|
@ -54,12 +54,12 @@ describe('stepRuleActions schema', () => {
|
|||
expect(errors[0]).toEqual('Message is not valid mustache template');
|
||||
});
|
||||
|
||||
it('should validate single action with incorrect id', () => {
|
||||
it('should validate single action with incorrect id', async () => {
|
||||
(isUuid as jest.Mock).mockReturnValue(false);
|
||||
(validateMustache as jest.Mock).mockReturnValue([]);
|
||||
(validateActionParams as jest.Mock).mockReturnValue([]);
|
||||
|
||||
const errors = validateSingleAction(
|
||||
const errors = await validateSingleAction(
|
||||
{
|
||||
id: '823d4',
|
||||
group: 'default',
|
||||
|
@ -74,10 +74,10 @@ describe('stepRuleActions schema', () => {
|
|||
});
|
||||
|
||||
describe('validateRuleActionsField', () => {
|
||||
it('should validate rule actions field', () => {
|
||||
it('should validate rule actions field', async () => {
|
||||
const validator = validateRuleActionsField(actionTypeRegistry);
|
||||
|
||||
const result = validator({
|
||||
const result = await validator({
|
||||
path: '',
|
||||
value: [],
|
||||
form: {} as FormHook,
|
||||
|
@ -88,11 +88,11 @@ describe('stepRuleActions schema', () => {
|
|||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should validate incorrect rule actions field', () => {
|
||||
it('should validate incorrect rule actions field', async () => {
|
||||
(getActionTypeName as jest.Mock).mockReturnValue('Slack');
|
||||
const validator = validateRuleActionsField(actionTypeRegistry);
|
||||
|
||||
const result = validator({
|
||||
const result = await validator({
|
||||
path: '',
|
||||
value: [
|
||||
{
|
||||
|
@ -117,7 +117,7 @@ describe('stepRuleActions schema', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should validate multiple incorrect rule actions field', () => {
|
||||
it('should validate multiple incorrect rule actions field', async () => {
|
||||
(isUuid as jest.Mock).mockReturnValueOnce(false);
|
||||
(getActionTypeName as jest.Mock).mockReturnValueOnce('Slack');
|
||||
(isUuid as jest.Mock).mockReturnValueOnce(true);
|
||||
|
@ -126,7 +126,7 @@ describe('stepRuleActions schema', () => {
|
|||
(validateMustache as jest.Mock).mockReturnValue(['Component is not valid mustache template']);
|
||||
const validator = validateRuleActionsField(actionTypeRegistry);
|
||||
|
||||
const result = validator({
|
||||
const result = await validator({
|
||||
path: '',
|
||||
value: [
|
||||
{
|
||||
|
|
|
@ -13,42 +13,46 @@ import {
|
|||
AlertAction,
|
||||
ActionTypeRegistryContract,
|
||||
} from '../../../../../../triggers_actions_ui/public';
|
||||
import { FormSchema, ValidationFunc, ERROR_CODE } from '../../../../shared_imports';
|
||||
import {
|
||||
FormSchema,
|
||||
ValidationFunc,
|
||||
ERROR_CODE,
|
||||
ValidationError,
|
||||
} from '../../../../shared_imports';
|
||||
import { ActionsStepRule } from '../../../pages/detection_engine/rules/types';
|
||||
import * as I18n from './translations';
|
||||
import { isUuid, getActionTypeName, validateMustache, validateActionParams } from './utils';
|
||||
|
||||
export const validateSingleAction = (
|
||||
export const validateSingleAction = async (
|
||||
actionItem: AlertAction,
|
||||
actionTypeRegistry: ActionTypeRegistryContract
|
||||
): string[] => {
|
||||
): Promise<string[]> => {
|
||||
if (!isUuid(actionItem.id)) {
|
||||
return [I18n.NO_CONNECTOR_SELECTED];
|
||||
}
|
||||
|
||||
const actionParamsErrors = validateActionParams(actionItem, actionTypeRegistry);
|
||||
const actionParamsErrors = await validateActionParams(actionItem, actionTypeRegistry);
|
||||
const mustacheErrors = validateMustache(actionItem.params);
|
||||
|
||||
return [...actionParamsErrors, ...mustacheErrors];
|
||||
};
|
||||
|
||||
export const validateRuleActionsField = (actionTypeRegistry: ActionTypeRegistryContract) => (
|
||||
export const validateRuleActionsField = (actionTypeRegistry: ActionTypeRegistryContract) => async (
|
||||
...data: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
): Promise<ValidationError<ERROR_CODE> | void | undefined> => {
|
||||
const [{ value, path }] = data as [{ value: AlertAction[]; path: string }];
|
||||
|
||||
const errors = value.reduce((acc, actionItem) => {
|
||||
const errorsArray = validateSingleAction(actionItem, actionTypeRegistry);
|
||||
const errors = [];
|
||||
for (const actionItem of value) {
|
||||
const errorsArray = await validateSingleAction(actionItem, actionTypeRegistry);
|
||||
|
||||
if (errorsArray.length) {
|
||||
const actionTypeName = getActionTypeName(actionItem.actionTypeId);
|
||||
const errorsListItems = errorsArray.map((error) => `* ${error}\n`);
|
||||
|
||||
return [...acc, `\n**${actionTypeName}:**\n${errorsListItems.join('')}`];
|
||||
errors.push(`\n**${actionTypeName}:**\n${errorsListItems.join('')}`);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
return {
|
||||
|
|
|
@ -61,11 +61,11 @@ describe('stepRuleActions utils', () => {
|
|||
actionTypeRegistry.get.mockReturnValue(actionMock);
|
||||
});
|
||||
|
||||
it('should validate action params', () => {
|
||||
it('should validate action params', async () => {
|
||||
validateParamsMock.mockReturnValue({ errors: [] });
|
||||
|
||||
expect(
|
||||
validateActionParams(
|
||||
await validateActionParams(
|
||||
{
|
||||
id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4',
|
||||
group: 'default',
|
||||
|
@ -79,13 +79,13 @@ describe('stepRuleActions utils', () => {
|
|||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should validate incorrect action params', () => {
|
||||
it('should validate incorrect action params', async () => {
|
||||
validateParamsMock.mockReturnValue({
|
||||
errors: ['Message is required'],
|
||||
});
|
||||
|
||||
expect(
|
||||
validateActionParams(
|
||||
await validateActionParams(
|
||||
{
|
||||
id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4',
|
||||
group: 'default',
|
||||
|
@ -97,7 +97,7 @@ describe('stepRuleActions utils', () => {
|
|||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should validate incorrect action params and filter error objects', () => {
|
||||
it('should validate incorrect action params and filter error objects', async () => {
|
||||
validateParamsMock.mockReturnValue({
|
||||
errors: [
|
||||
{
|
||||
|
@ -107,7 +107,7 @@ describe('stepRuleActions utils', () => {
|
|||
});
|
||||
|
||||
expect(
|
||||
validateActionParams(
|
||||
await validateActionParams(
|
||||
{
|
||||
id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4',
|
||||
group: 'default',
|
||||
|
@ -119,13 +119,13 @@ describe('stepRuleActions utils', () => {
|
|||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should validate incorrect action params and filter duplicated errors', () => {
|
||||
it('should validate incorrect action params and filter duplicated errors', async () => {
|
||||
validateParamsMock.mockReturnValue({
|
||||
errors: ['Message is required', 'Message is required', 'Message is required'],
|
||||
});
|
||||
|
||||
expect(
|
||||
validateActionParams(
|
||||
await validateActionParams(
|
||||
{
|
||||
id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4',
|
||||
group: 'default',
|
||||
|
|
|
@ -41,11 +41,11 @@ export const validateMustache = (params: AlertAction['params']) => {
|
|||
return errors;
|
||||
};
|
||||
|
||||
export const validateActionParams = (
|
||||
export const validateActionParams = async (
|
||||
actionItem: AlertAction,
|
||||
actionTypeRegistry: ActionTypeRegistryContract
|
||||
): string[] => {
|
||||
const actionErrors = actionTypeRegistry
|
||||
): Promise<string[]> => {
|
||||
const actionErrors = await actionTypeRegistry
|
||||
.get(actionItem.actionTypeId)
|
||||
?.validateParams(actionItem.params);
|
||||
|
||||
|
|
|
@ -888,10 +888,10 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Send to Server log',
|
||||
}
|
||||
),
|
||||
validateConnector: (): ValidationResult => {
|
||||
validateConnector: (): Promise<ValidationResult> => {
|
||||
return { errors: {} };
|
||||
},
|
||||
validateParams: (actionParams: ServerLogActionParams): ValidationResult => {
|
||||
validateParams: (actionParams: ServerLogActionParams): Promise<ValidationResult> => {
|
||||
// validation of action params implementation
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
|
@ -929,10 +929,10 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Send to email',
|
||||
}
|
||||
),
|
||||
validateConnector: (action: EmailActionConnector): ValidationResult => {
|
||||
validateConnector: (action: EmailActionConnector): Promise<ValidationResult> => {
|
||||
// validation of connector properties implementation
|
||||
},
|
||||
validateParams: (actionParams: EmailActionParams): ValidationResult => {
|
||||
validateParams: (actionParams: EmailActionParams): Promise<ValidationResult> => {
|
||||
// validation of action params implementation
|
||||
},
|
||||
actionConnectorFields: EmailActionConnectorFields,
|
||||
|
@ -967,10 +967,10 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Send to Slack',
|
||||
}
|
||||
),
|
||||
validateConnector: (action: SlackActionConnector): ValidationResult => {
|
||||
validateConnector: (action: SlackActionConnector): Promise<ValidationResult> => {
|
||||
// validation of connector properties implementation
|
||||
},
|
||||
validateParams: (actionParams: SlackActionParams): ValidationResult => {
|
||||
validateParams: (actionParams: SlackActionParams): Promise<ValidationResult> => {
|
||||
// validation of action params implementation
|
||||
},
|
||||
actionConnectorFields: SlackActionFields,
|
||||
|
@ -1000,12 +1000,12 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Index data into Elasticsearch.',
|
||||
}
|
||||
),
|
||||
validateConnector: (): ValidationResult => {
|
||||
validateConnector: (): Promise<ValidationResult> => {
|
||||
return { errors: {} };
|
||||
},
|
||||
actionConnectorFields: IndexActionConnectorFields,
|
||||
actionParamsFields: IndexParamsFields,
|
||||
validateParams: (): ValidationResult => {
|
||||
validateParams: (): Promise<ValidationResult> => {
|
||||
return { errors: {} };
|
||||
},
|
||||
};
|
||||
|
@ -1046,10 +1046,10 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Send a request to a web service.',
|
||||
}
|
||||
),
|
||||
validateConnector: (action: WebhookActionConnector): ValidationResult => {
|
||||
validateConnector: (action: WebhookActionConnector): Promise<ValidationResult> => {
|
||||
// validation of connector properties implementation
|
||||
},
|
||||
validateParams: (actionParams: WebhookActionParams): ValidationResult => {
|
||||
validateParams: (actionParams: WebhookActionParams): Promise<ValidationResult> => {
|
||||
// validation of action params implementation
|
||||
},
|
||||
actionConnectorFields: WebhookActionConnectorFields,
|
||||
|
@ -1086,10 +1086,10 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Send to PagerDuty',
|
||||
}
|
||||
),
|
||||
validateConnector: (action: PagerDutyActionConnector): ValidationResult => {
|
||||
validateConnector: (action: PagerDutyActionConnector): Promise<ValidationResult> => {
|
||||
// validation of connector properties implementation
|
||||
},
|
||||
validateParams: (actionParams: PagerDutyActionParams): ValidationResult => {
|
||||
validateParams: (actionParams: PagerDutyActionParams): Promise<ValidationResult> => {
|
||||
// validation of action params implementation
|
||||
},
|
||||
actionConnectorFields: PagerDutyActionConnectorFields,
|
||||
|
@ -1113,8 +1113,8 @@ Each action type should be defined as an `ActionTypeModel` object with the follo
|
|||
iconClass: IconType;
|
||||
selectMessage: string;
|
||||
actionTypeTitle?: string;
|
||||
validateConnector: (connector: any) => ValidationResult;
|
||||
validateParams: (actionParams: any) => ValidationResult;
|
||||
validateConnector: (connector: any) => Promise<ValidationResult>;
|
||||
validateParams: (actionParams: any) => Promise<ValidationResult>;
|
||||
actionConnectorFields: React.FunctionComponent<any> | null;
|
||||
actionParamsFields: React.LazyExoticComponent<ComponentType<ActionParamsProps<ActionParams>>>;
|
||||
```
|
||||
|
@ -1186,7 +1186,7 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Example Action',
|
||||
}
|
||||
),
|
||||
validateConnector: (action: ExampleActionConnector): ValidationResult => {
|
||||
validateConnector: (action: ExampleActionConnector): Promise<ValidationResult> => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
someConnectorField: new Array<string>(),
|
||||
|
@ -1204,7 +1204,7 @@ export function getActionType(): ActionTypeModel {
|
|||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: (actionParams: ExampleActionParams): ValidationResult => {
|
||||
validateParams: (actionParams: ExampleActionParams): Promise<ValidationResult> => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
message: new Array<string>(),
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
|
||||
describe('connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
|
@ -49,7 +49,7 @@ describe('connector validation', () => {
|
|||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
|
@ -66,7 +66,7 @@ describe('connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation succeeds when connector config is valid with empty user/password', () => {
|
||||
test('connector validation succeeds when connector config is valid with empty user/password', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: null,
|
||||
|
@ -85,7 +85,7 @@ describe('connector validation', () => {
|
|||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
|
@ -101,7 +101,7 @@ describe('connector validation', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
test('connector validation fails when connector config is not valid', () => {
|
||||
test('connector validation fails when connector config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
|
@ -116,7 +116,7 @@ describe('connector validation', () => {
|
|||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
|
@ -132,7 +132,7 @@ describe('connector validation', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
test('connector validation fails when user specified but not password', () => {
|
||||
test('connector validation fails when user specified but not password', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
|
@ -151,7 +151,7 @@ describe('connector validation', () => {
|
|||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
|
@ -167,7 +167,7 @@ describe('connector validation', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
test('connector validation fails when password specified but not user', () => {
|
||||
test('connector validation fails when password specified but not user', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: null,
|
||||
|
@ -186,7 +186,7 @@ describe('connector validation', () => {
|
|||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
|
@ -205,7 +205,7 @@ describe('connector validation', () => {
|
|||
});
|
||||
|
||||
describe('action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
to: [],
|
||||
cc: ['test1@test.com'],
|
||||
|
@ -213,7 +213,7 @@ describe('action params validation', () => {
|
|||
subject: 'test',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
to: [],
|
||||
cc: [],
|
||||
|
@ -224,13 +224,13 @@ describe('action params validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('action params validation fails when action params is not valid', () => {
|
||||
test('action params validation fails when action params is not valid', async () => {
|
||||
const actionParams = {
|
||||
to: ['test@test.com'],
|
||||
subject: 'test',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
to: [],
|
||||
cc: [],
|
||||
|
|
|
@ -31,9 +31,12 @@ export function getActionType(): ActionTypeModel<EmailConfig, EmailSecrets, Emai
|
|||
defaultMessage: 'Send to email',
|
||||
}
|
||||
),
|
||||
validateConnector: (
|
||||
validateConnector: async (
|
||||
action: EmailActionConnector
|
||||
): ConnectorValidationResult<Omit<EmailConfig, 'secure' | 'hasAuth'>, EmailSecrets> => {
|
||||
): Promise<
|
||||
ConnectorValidationResult<Omit<EmailConfig, 'secure' | 'hasAuth'>, EmailSecrets>
|
||||
> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
from: new Array<string>(),
|
||||
port: new Array<string>(),
|
||||
|
@ -49,74 +52,25 @@ export function getActionType(): ActionTypeModel<EmailConfig, EmailSecrets, Emai
|
|||
secrets: { errors: secretsErrors },
|
||||
};
|
||||
if (!action.config.from) {
|
||||
configErrors.from.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText',
|
||||
{
|
||||
defaultMessage: 'Sender is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
configErrors.from.push(translations.SENDER_REQUIRED);
|
||||
}
|
||||
if (action.config.from && !action.config.from.trim().match(mailformat)) {
|
||||
configErrors.from.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText',
|
||||
{
|
||||
defaultMessage: 'Sender is not a valid email address.',
|
||||
}
|
||||
)
|
||||
);
|
||||
configErrors.from.push(translations.SENDER_NOT_VALID);
|
||||
}
|
||||
if (!action.config.port) {
|
||||
configErrors.port.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText',
|
||||
{
|
||||
defaultMessage: 'Port is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
configErrors.port.push(translations.PORT_REQUIRED);
|
||||
}
|
||||
if (!action.config.host) {
|
||||
configErrors.host.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText',
|
||||
{
|
||||
defaultMessage: 'Host is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
configErrors.host.push(translations.HOST_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.user.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthUserNameText',
|
||||
{
|
||||
defaultMessage: 'Username is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.user.push(translations.USERNAME_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.password.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText',
|
||||
{
|
||||
defaultMessage: 'Password is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED);
|
||||
}
|
||||
if (action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.password.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText',
|
||||
{
|
||||
defaultMessage: 'Password is required when username is used.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED_FOR_USER_USED);
|
||||
}
|
||||
if (!action.secrets.user && action.secrets.password) {
|
||||
secretsErrors.user.push(
|
||||
|
@ -130,9 +84,10 @@ export function getActionType(): ActionTypeModel<EmailConfig, EmailSecrets, Emai
|
|||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: (
|
||||
validateParams: async (
|
||||
actionParams: EmailActionParams
|
||||
): GenericValidationResult<EmailActionParams> => {
|
||||
): Promise<GenericValidationResult<EmailActionParams>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
to: new Array<string>(),
|
||||
cc: new Array<string>(),
|
||||
|
@ -146,35 +101,16 @@ export function getActionType(): ActionTypeModel<EmailConfig, EmailSecrets, Emai
|
|||
(!(actionParams.cc instanceof Array) || actionParams.cc.length === 0) &&
|
||||
(!(actionParams.bcc instanceof Array) || actionParams.bcc.length === 0)
|
||||
) {
|
||||
const errorText = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText',
|
||||
{
|
||||
defaultMessage: 'No To, Cc, or Bcc entry. At least one entry is required.',
|
||||
}
|
||||
);
|
||||
const errorText = translations.TO_CC_REQUIRED;
|
||||
errors.to.push(errorText);
|
||||
errors.cc.push(errorText);
|
||||
errors.bcc.push(errorText);
|
||||
}
|
||||
if (!actionParams.message?.length) {
|
||||
errors.message.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText',
|
||||
{
|
||||
defaultMessage: 'Message is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
errors.message.push(translations.MESSAGE_REQUIRED);
|
||||
}
|
||||
if (!actionParams.subject?.length) {
|
||||
errors.subject.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText',
|
||||
{
|
||||
defaultMessage: 'Subject is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
errors.subject.push(translations.SUBJECT_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
|
|
|
@ -38,6 +38,17 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const isFromInvalid: boolean =
|
||||
from !== undefined && errors.from !== undefined && errors.from.length > 0;
|
||||
const isHostInvalid: boolean =
|
||||
host !== undefined && errors.host !== undefined && errors.host.length > 0;
|
||||
const isPortInvalid: boolean =
|
||||
port !== undefined && errors.port !== undefined && errors.port.length > 0;
|
||||
|
||||
const isPasswordInvalid: boolean =
|
||||
password !== undefined && errors.password !== undefined && errors.password.length > 0;
|
||||
const isUserInvalid: boolean =
|
||||
user !== undefined && errors.user !== undefined && errors.user.length > 0;
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
|
@ -46,7 +57,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
id="from"
|
||||
fullWidth
|
||||
error={errors.from}
|
||||
isInvalid={errors.from.length > 0 && from !== undefined}
|
||||
isInvalid={isFromInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel',
|
||||
{
|
||||
|
@ -65,7 +76,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
<EuiFieldText
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={errors.from.length > 0 && from !== undefined}
|
||||
isInvalid={isFromInvalid}
|
||||
name="from"
|
||||
value={from || ''}
|
||||
data-test-subj="emailFromInput"
|
||||
|
@ -87,7 +98,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
id="emailHost"
|
||||
fullWidth
|
||||
error={errors.host}
|
||||
isInvalid={errors.host.length > 0 && host !== undefined}
|
||||
isInvalid={isHostInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel',
|
||||
{
|
||||
|
@ -98,7 +109,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
<EuiFieldText
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={errors.host.length > 0 && host !== undefined}
|
||||
isInvalid={isHostInvalid}
|
||||
name="host"
|
||||
value={host || ''}
|
||||
data-test-subj="emailHostInput"
|
||||
|
@ -121,7 +132,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
fullWidth
|
||||
placeholder="587"
|
||||
error={errors.port}
|
||||
isInvalid={errors.port.length > 0 && port !== undefined}
|
||||
isInvalid={isPortInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel',
|
||||
{
|
||||
|
@ -131,7 +142,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
>
|
||||
<EuiFieldNumber
|
||||
prepend=":"
|
||||
isInvalid={errors.port.length > 0 && port !== undefined}
|
||||
isInvalid={isPortInvalid}
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
name="port"
|
||||
|
@ -221,7 +232,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
id="emailUser"
|
||||
fullWidth
|
||||
error={errors.user}
|
||||
isInvalid={errors.user.length > 0 && user !== undefined}
|
||||
isInvalid={isUserInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel',
|
||||
{
|
||||
|
@ -231,7 +242,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={errors.user.length > 0 && user !== undefined}
|
||||
isInvalid={isUserInvalid}
|
||||
name="user"
|
||||
readOnly={readOnly}
|
||||
value={user || ''}
|
||||
|
@ -252,7 +263,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
id="emailPassword"
|
||||
fullWidth
|
||||
error={errors.password}
|
||||
isInvalid={errors.password.length > 0 && password !== undefined}
|
||||
isInvalid={isPasswordInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel',
|
||||
{
|
||||
|
@ -263,7 +274,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
<EuiFieldPassword
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={errors.password.length > 0 && password !== undefined}
|
||||
isInvalid={isPasswordInvalid}
|
||||
name="password"
|
||||
value={password || ''}
|
||||
data-test-subj="emailPasswordInput"
|
||||
|
|
|
@ -44,13 +44,18 @@ export const EmailParamsFields = ({
|
|||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultMessage]);
|
||||
|
||||
const isToInvalid: boolean = to !== undefined && errors.to !== undefined && errors.to.length > 0;
|
||||
const isSubjectInvalid: boolean =
|
||||
subject !== undefined && errors.subject !== undefined && errors.subject.length > 0;
|
||||
const isCCInvalid: boolean = errors.cc !== undefined && errors.cc.length > 0 && cc !== undefined;
|
||||
const isBCCInvalid: boolean =
|
||||
errors.bcc !== undefined && errors.bcc.length > 0 && bcc !== undefined;
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors.to}
|
||||
isInvalid={errors.to.length > 0 && to !== undefined}
|
||||
isInvalid={isToInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientTextFieldLabel',
|
||||
{
|
||||
|
@ -82,7 +87,7 @@ export const EmailParamsFields = ({
|
|||
>
|
||||
<EuiComboBox
|
||||
noSuggestions
|
||||
isInvalid={errors.to.length > 0 && to !== undefined}
|
||||
isInvalid={isToInvalid}
|
||||
fullWidth
|
||||
data-test-subj="toEmailAddressInput"
|
||||
selectedOptions={toOptions}
|
||||
|
@ -112,7 +117,7 @@ export const EmailParamsFields = ({
|
|||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors.cc}
|
||||
isInvalid={errors.cc.length > 0 && cc !== undefined}
|
||||
isInvalid={isCCInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientCopyTextFieldLabel',
|
||||
{
|
||||
|
@ -122,7 +127,7 @@ export const EmailParamsFields = ({
|
|||
>
|
||||
<EuiComboBox
|
||||
noSuggestions
|
||||
isInvalid={errors.cc.length > 0 && cc !== undefined}
|
||||
isInvalid={isCCInvalid}
|
||||
fullWidth
|
||||
data-test-subj="ccEmailAddressInput"
|
||||
selectedOptions={ccOptions}
|
||||
|
@ -153,7 +158,7 @@ export const EmailParamsFields = ({
|
|||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors.bcc}
|
||||
isInvalid={errors.bcc.length > 0 && bcc !== undefined}
|
||||
isInvalid={isBCCInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientBccTextFieldLabel',
|
||||
{
|
||||
|
@ -163,7 +168,7 @@ export const EmailParamsFields = ({
|
|||
>
|
||||
<EuiComboBox
|
||||
noSuggestions
|
||||
isInvalid={errors.bcc.length > 0 && bcc !== undefined}
|
||||
isInvalid={isBCCInvalid}
|
||||
fullWidth
|
||||
data-test-subj="bccEmailAddressInput"
|
||||
selectedOptions={bccOptions}
|
||||
|
@ -193,7 +198,7 @@ export const EmailParamsFields = ({
|
|||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors.subject}
|
||||
isInvalid={errors.subject.length > 0 && subject !== undefined}
|
||||
isInvalid={isSubjectInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel',
|
||||
{
|
||||
|
@ -207,7 +212,7 @@ export const EmailParamsFields = ({
|
|||
messageVariables={messageVariables}
|
||||
paramsProperty={'subject'}
|
||||
inputTargetValue={subject}
|
||||
errors={errors.subject as string[]}
|
||||
errors={(errors.subject ?? []) as string[]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<TextAreaWithMessageVariables
|
||||
|
@ -222,7 +227,7 @@ export const EmailParamsFields = ({
|
|||
defaultMessage: 'Message',
|
||||
}
|
||||
)}
|
||||
errors={errors.message as string[]}
|
||||
errors={(errors.message ?? []) as string[]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SENDER_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText',
|
||||
{
|
||||
defaultMessage: 'Sender is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SENDER_NOT_VALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText',
|
||||
{
|
||||
defaultMessage: 'Sender is not a valid email address.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PORT_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText',
|
||||
{
|
||||
defaultMessage: 'Port is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const HOST_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText',
|
||||
{
|
||||
defaultMessage: 'Host is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const USERNAME_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthUserNameText',
|
||||
{
|
||||
defaultMessage: 'Username is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText',
|
||||
{
|
||||
defaultMessage: 'Password is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_REQUIRED_FOR_USER_USED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText',
|
||||
{
|
||||
defaultMessage: 'Password is required when username is used.',
|
||||
}
|
||||
);
|
||||
|
||||
export const TO_CC_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText',
|
||||
{
|
||||
defaultMessage: 'No To, Cc, or Bcc entry. At least one entry is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const MESSAGE_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText',
|
||||
{
|
||||
defaultMessage: 'Message is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SUBJECT_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText',
|
||||
{
|
||||
defaultMessage: 'Subject is required.',
|
||||
}
|
||||
);
|
|
@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
|
||||
describe('index connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
|
@ -43,7 +43,7 @@ describe('index connector validation', () => {
|
|||
},
|
||||
} as EsIndexActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
index: [],
|
||||
|
@ -57,7 +57,7 @@ describe('index connector validation', () => {
|
|||
});
|
||||
|
||||
describe('index connector validation with minimal config', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
|
@ -68,7 +68,7 @@ describe('index connector validation with minimal config', () => {
|
|||
},
|
||||
} as EsIndexActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
index: [],
|
||||
|
@ -82,9 +82,9 @@ describe('index connector validation with minimal config', () => {
|
|||
});
|
||||
|
||||
describe('action params validation', () => {
|
||||
test('action params validation succeeds when action params are valid', () => {
|
||||
test('action params validation succeeds when action params are valid', async () => {
|
||||
expect(
|
||||
actionTypeModel.validateParams({
|
||||
await actionTypeModel.validateParams({
|
||||
documents: [{ test: 1234 }],
|
||||
})
|
||||
).toEqual({
|
||||
|
@ -95,7 +95,7 @@ describe('action params validation', () => {
|
|||
});
|
||||
|
||||
expect(
|
||||
actionTypeModel.validateParams({
|
||||
await actionTypeModel.validateParams({
|
||||
documents: [{ test: 1234 }],
|
||||
indexOverride: 'kibana-alert-history-anything',
|
||||
})
|
||||
|
@ -107,8 +107,8 @@ describe('action params validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('action params validation fails when action params are invalid', () => {
|
||||
expect(actionTypeModel.validateParams({})).toEqual({
|
||||
test('action params validation fails when action params are invalid', async () => {
|
||||
expect(await actionTypeModel.validateParams({})).toEqual({
|
||||
errors: {
|
||||
documents: ['Document is required and should be a valid JSON object.'],
|
||||
indexOverride: [],
|
||||
|
@ -116,7 +116,7 @@ describe('action params validation', () => {
|
|||
});
|
||||
|
||||
expect(
|
||||
actionTypeModel.validateParams({
|
||||
await actionTypeModel.validateParams({
|
||||
documents: [{}],
|
||||
})
|
||||
).toEqual({
|
||||
|
@ -127,7 +127,7 @@ describe('action params validation', () => {
|
|||
});
|
||||
|
||||
expect(
|
||||
actionTypeModel.validateParams({
|
||||
await actionTypeModel.validateParams({
|
||||
documents: [{}],
|
||||
indexOverride: 'kibana-alert-history-',
|
||||
})
|
||||
|
@ -139,7 +139,7 @@ describe('action params validation', () => {
|
|||
});
|
||||
|
||||
expect(
|
||||
actionTypeModel.validateParams({
|
||||
await actionTypeModel.validateParams({
|
||||
documents: [{}],
|
||||
indexOverride: 'this.is-a_string',
|
||||
})
|
||||
|
|
|
@ -31,44 +31,32 @@ export function getActionType(): ActionTypeModel<EsIndexConfig, unknown, IndexAc
|
|||
defaultMessage: 'Index data',
|
||||
}
|
||||
),
|
||||
validateConnector: (
|
||||
validateConnector: async (
|
||||
action: EsIndexActionConnector
|
||||
): ConnectorValidationResult<Pick<EsIndexConfig, 'index'>, unknown> => {
|
||||
): Promise<ConnectorValidationResult<Pick<EsIndexConfig, 'index'>, unknown>> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
index: new Array<string>(),
|
||||
};
|
||||
const validationResult = { config: { errors: configErrors }, secrets: { errors: {} } };
|
||||
if (!action.config.index) {
|
||||
configErrors.index.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText',
|
||||
{
|
||||
defaultMessage: 'Index is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
configErrors.index.push(translations.INDEX_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
actionConnectorFields: lazy(() => import('./es_index_connector')),
|
||||
actionParamsFields: lazy(() => import('./es_index_params')),
|
||||
validateParams: (
|
||||
validateParams: async (
|
||||
actionParams: IndexActionParams
|
||||
): GenericValidationResult<IndexActionParams> => {
|
||||
): Promise<GenericValidationResult<IndexActionParams>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
documents: new Array<string>(),
|
||||
indexOverride: new Array<string>(),
|
||||
};
|
||||
const validationResult = { errors };
|
||||
if (!actionParams.documents?.length || Object.keys(actionParams.documents[0]).length === 0) {
|
||||
errors.documents.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson',
|
||||
{
|
||||
defaultMessage: 'Document is required and should be a valid JSON object.',
|
||||
}
|
||||
)
|
||||
);
|
||||
errors.documents.push(translations.DOCUMENT_NOT_VALID);
|
||||
}
|
||||
if (actionParams.indexOverride) {
|
||||
if (!actionParams.indexOverride.startsWith(ALERT_HISTORY_PREFIX)) {
|
||||
|
@ -85,14 +73,7 @@ export function getActionType(): ActionTypeModel<EsIndexConfig, unknown, IndexAc
|
|||
|
||||
const indexSuffix = actionParams.indexOverride.replace(ALERT_HISTORY_PREFIX, '');
|
||||
if (indexSuffix.length === 0) {
|
||||
errors.indexOverride.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideSuffix',
|
||||
{
|
||||
defaultMessage: 'Alert history index must contain valid suffix.',
|
||||
}
|
||||
)
|
||||
);
|
||||
errors.indexOverride.push(translations.HISTORY_NOT_VALID);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,6 +74,8 @@ const IndexActionConnectorFields: React.FunctionComponent<
|
|||
indexPatternsFunction();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const isIndexInvalid: boolean =
|
||||
errors.index !== undefined && errors.index.length > 0 && index !== undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -95,7 +97,7 @@ const IndexActionConnectorFields: React.FunctionComponent<
|
|||
defaultMessage="Index"
|
||||
/>
|
||||
}
|
||||
isInvalid={errors.index.length > 0 && index !== undefined}
|
||||
isInvalid={isIndexInvalid}
|
||||
error={errors.index}
|
||||
helpText={
|
||||
<>
|
||||
|
@ -118,7 +120,7 @@ const IndexActionConnectorFields: React.FunctionComponent<
|
|||
singleSelection={{ asPlainText: true }}
|
||||
async
|
||||
isLoading={isIndiciesLoading}
|
||||
isInvalid={errors.index.length > 0 && index !== undefined}
|
||||
isInvalid={isIndexInvalid}
|
||||
noSuggestions={!indexOptions.length}
|
||||
options={indexOptions}
|
||||
data-test-subj="connectorIndexesComboBox"
|
||||
|
|
|
@ -117,7 +117,11 @@ export const IndexParamsFields = ({
|
|||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors.indexOverride as string[]}
|
||||
isInvalid={(errors.indexOverride as string[]) && errors.indexOverride.length > 0}
|
||||
isInvalid={
|
||||
errors.indexOverride !== undefined &&
|
||||
(errors.indexOverride as string[]) &&
|
||||
errors.indexOverride.length > 0
|
||||
}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.preconfiguredIndex',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const INDEX_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText',
|
||||
{
|
||||
defaultMessage: 'Index is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const DOCUMENT_NOT_VALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson',
|
||||
{
|
||||
defaultMessage: 'Document is required and should be a valid JSON object.',
|
||||
}
|
||||
);
|
||||
|
||||
export const HISTORY_NOT_VALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideSuffix',
|
||||
{
|
||||
defaultMessage: 'Alert history index must contain valid suffix.',
|
||||
}
|
||||
);
|
|
@ -29,7 +29,7 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
|
||||
describe('jira connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
email: 'email',
|
||||
|
@ -45,7 +45,7 @@ describe('jira connector validation', () => {
|
|||
},
|
||||
} as JiraActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
|
@ -61,7 +61,7 @@ describe('jira connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid', () => {
|
||||
test('connector validation fails when connector config is not valid', async () => {
|
||||
const actionConnector = ({
|
||||
secrets: {
|
||||
email: 'user',
|
||||
|
@ -72,7 +72,7 @@ describe('jira connector validation', () => {
|
|||
config: {},
|
||||
} as unknown) as JiraActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: ['URL is required.'],
|
||||
|
@ -90,22 +90,22 @@ describe('jira connector validation', () => {
|
|||
});
|
||||
|
||||
describe('jira action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
subActionParams: { incident: { summary: 'some title {{test}}' }, comments: [] },
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { 'subActionParams.incident.summary': [], 'subActionParams.incident.labels': [] },
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when body is not valid', () => {
|
||||
test('params validation fails when body is not valid', async () => {
|
||||
const actionParams = {
|
||||
subActionParams: { incident: { summary: '' }, comments: [] },
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
'subActionParams.incident.summary': ['Summary is required.'],
|
||||
'subActionParams.incident.labels': [],
|
||||
|
@ -113,7 +113,7 @@ describe('jira action params validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('params validation fails when labels contain spaces', () => {
|
||||
test('params validation fails when labels contain spaces', async () => {
|
||||
const actionParams = {
|
||||
subActionParams: {
|
||||
incident: { summary: 'some title', labels: ['label with spaces'] },
|
||||
|
@ -121,7 +121,7 @@ describe('jira action params validation', () => {
|
|||
},
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
'subActionParams.incident.summary': [],
|
||||
'subActionParams.incident.labels': ['Labels cannot contain spaces.'],
|
||||
|
|
|
@ -6,18 +6,19 @@
|
|||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
GenericValidationResult,
|
||||
ActionTypeModel,
|
||||
ConnectorValidationResult,
|
||||
} from '../../../../types';
|
||||
import { JiraActionConnector, JiraConfig, JiraSecrets, JiraActionParams } from './types';
|
||||
import * as i18n from './translations';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
|
||||
const validateConnector = (
|
||||
const validateConnector = async (
|
||||
action: JiraActionConnector
|
||||
): ConnectorValidationResult<JiraConfig, JiraSecrets> => {
|
||||
): Promise<ConnectorValidationResult<JiraConfig, JiraSecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
apiUrl: new Array<string>(),
|
||||
projectKey: new Array<string>(),
|
||||
|
@ -33,41 +34,58 @@ const validateConnector = (
|
|||
};
|
||||
|
||||
if (!action.config.apiUrl) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRED];
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED];
|
||||
}
|
||||
|
||||
if (action.config.apiUrl) {
|
||||
if (!isValidUrl(action.config.apiUrl)) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_INVALID];
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID];
|
||||
} else if (!isValidUrl(action.config.apiUrl, 'https:')) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRE_HTTPS];
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS];
|
||||
}
|
||||
}
|
||||
|
||||
if (!action.config.projectKey) {
|
||||
configErrors.projectKey = [...configErrors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED];
|
||||
configErrors.projectKey = [...configErrors.projectKey, translations.JIRA_PROJECT_KEY_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.email) {
|
||||
secretsErrors.email = [...secretsErrors.email, i18n.JIRA_EMAIL_REQUIRED];
|
||||
secretsErrors.email = [...secretsErrors.email, translations.JIRA_EMAIL_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.apiToken) {
|
||||
secretsErrors.apiToken = [...secretsErrors.apiToken, i18n.JIRA_API_TOKEN_REQUIRED];
|
||||
secretsErrors.apiToken = [...secretsErrors.apiToken, translations.JIRA_API_TOKEN_REQUIRED];
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
};
|
||||
|
||||
export const JIRA_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create an incident in Jira.',
|
||||
}
|
||||
);
|
||||
|
||||
export const JIRA_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Jira',
|
||||
}
|
||||
);
|
||||
|
||||
export function getActionType(): ActionTypeModel<JiraConfig, JiraSecrets, JiraActionParams> {
|
||||
return {
|
||||
id: '.jira',
|
||||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: i18n.JIRA_DESC,
|
||||
actionTypeTitle: i18n.JIRA_TITLE,
|
||||
selectMessage: JIRA_DESC,
|
||||
actionTypeTitle: JIRA_TITLE,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./jira_connectors')),
|
||||
validateParams: (actionParams: JiraActionParams): GenericValidationResult<unknown> => {
|
||||
validateParams: async (
|
||||
actionParams: JiraActionParams
|
||||
): Promise<GenericValidationResult<unknown>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
'subActionParams.incident.summary': new Array<string>(),
|
||||
'subActionParams.incident.labels': new Array<string>(),
|
||||
|
@ -80,13 +98,13 @@ export function getActionType(): ActionTypeModel<JiraConfig, JiraSecrets, JiraAc
|
|||
actionParams.subActionParams.incident &&
|
||||
!actionParams.subActionParams.incident.summary?.length
|
||||
) {
|
||||
errors['subActionParams.incident.summary'].push(i18n.SUMMARY_REQUIRED);
|
||||
errors['subActionParams.incident.summary'].push(translations.SUMMARY_REQUIRED);
|
||||
}
|
||||
|
||||
if (actionParams.subActionParams?.incident?.labels?.length) {
|
||||
// Jira do not allows empty spaces on labels. If the label includes a whitespace show an error.
|
||||
if (actionParams.subActionParams.incident.labels.some((label) => label.match(/\s/g)))
|
||||
errors['subActionParams.incident.labels'].push(i18n.LABELS_WHITE_SPACES);
|
||||
errors['subActionParams.incident.labels'].push(translations.LABELS_WHITE_SPACES);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
|
|
|
@ -32,13 +32,17 @@ const JiraConnectorFields: React.FC<ActionConnectorFieldsProps<JiraActionConnect
|
|||
}) => {
|
||||
const { apiUrl, projectKey } = action.config;
|
||||
|
||||
const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl !== undefined;
|
||||
const isApiUrlInvalid: boolean =
|
||||
apiUrl !== undefined && errors.apiUrl !== undefined && errors.apiUrl.length > 0;
|
||||
|
||||
const { email, apiToken } = action.secrets;
|
||||
|
||||
const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey !== undefined;
|
||||
const isEmailInvalid: boolean = errors.email.length > 0 && email !== undefined;
|
||||
const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken !== undefined;
|
||||
const isProjectKeyInvalid: boolean =
|
||||
projectKey !== undefined && errors.projectKey !== undefined && errors.projectKey.length > 0;
|
||||
const isEmailInvalid: boolean =
|
||||
email !== undefined && errors.email !== undefined && errors.email.length > 0;
|
||||
const isApiTokenInvalid: boolean =
|
||||
apiToken !== undefined && errors.apiToken !== undefined && errors.apiToken.length > 0;
|
||||
|
||||
const handleOnChangeActionConfig = useCallback(
|
||||
(key: string, value: string) => editActionConfig(key, value),
|
||||
|
|
|
@ -186,6 +186,7 @@ const JiraParamsFields: React.FunctionComponent<ActionParamsProps<JiraActionPara
|
|||
|
||||
const areLabelsInvalid =
|
||||
errors['subActionParams.incident.labels'] != null &&
|
||||
errors['subActionParams.incident.labels'] !== undefined &&
|
||||
errors['subActionParams.incident.labels'].length > 0 &&
|
||||
incident.labels !== undefined;
|
||||
|
||||
|
@ -277,6 +278,7 @@ const JiraParamsFields: React.FunctionComponent<ActionParamsProps<JiraActionPara
|
|||
fullWidth
|
||||
error={errors['subActionParams.incident.summary']}
|
||||
isInvalid={
|
||||
errors['subActionParams.incident.summary'] !== undefined &&
|
||||
errors['subActionParams.incident.summary'].length > 0 &&
|
||||
incident.summary !== undefined
|
||||
}
|
||||
|
|
|
@ -7,20 +7,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const JIRA_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create an incident in Jira.',
|
||||
}
|
||||
);
|
||||
|
||||
export const JIRA_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Jira',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel',
|
||||
{
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
|
||||
describe('pagerduty connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
routingKey: 'test',
|
||||
|
@ -43,7 +43,7 @@ describe('pagerduty connector validation', () => {
|
|||
},
|
||||
} as PagerDutyActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
secrets: {
|
||||
errors: {
|
||||
routingKey: [],
|
||||
|
@ -53,7 +53,7 @@ describe('pagerduty connector validation', () => {
|
|||
|
||||
delete actionConnector.config.apiUrl;
|
||||
actionConnector.secrets.routingKey = 'test1';
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
secrets: {
|
||||
errors: {
|
||||
routingKey: [],
|
||||
|
@ -62,7 +62,7 @@ describe('pagerduty connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid', () => {
|
||||
test('connector validation fails when connector config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
|
@ -73,7 +73,7 @@ describe('pagerduty connector validation', () => {
|
|||
},
|
||||
} as PagerDutyActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
secrets: {
|
||||
errors: {
|
||||
routingKey: ['An integration key / routing key is required.'],
|
||||
|
@ -84,7 +84,7 @@ describe('pagerduty connector validation', () => {
|
|||
});
|
||||
|
||||
describe('pagerduty action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
eventAction: 'trigger',
|
||||
dedupKey: 'test',
|
||||
|
@ -97,7 +97,7 @@ describe('pagerduty action params validation', () => {
|
|||
class: 'test class',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
dedupKey: [],
|
||||
summary: [],
|
||||
|
|
|
@ -42,9 +42,10 @@ export function getActionType(): ActionTypeModel<
|
|||
defaultMessage: 'Send to PagerDuty',
|
||||
}
|
||||
),
|
||||
validateConnector: (
|
||||
validateConnector: async (
|
||||
action: PagerDutyActionConnector
|
||||
): ConnectorValidationResult<PagerDutyConfig, PagerDutySecrets> => {
|
||||
): Promise<ConnectorValidationResult<PagerDutyConfig, PagerDutySecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const secretsErrors = {
|
||||
routingKey: new Array<string>(),
|
||||
};
|
||||
|
@ -53,22 +54,16 @@ export function getActionType(): ActionTypeModel<
|
|||
};
|
||||
|
||||
if (!action.secrets.routingKey) {
|
||||
secretsErrors.routingKey.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText',
|
||||
{
|
||||
defaultMessage: 'An integration key / routing key is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.routingKey.push(translations.INTEGRATION_KEY_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: (
|
||||
validateParams: async (
|
||||
actionParams: PagerDutyActionParams
|
||||
): GenericValidationResult<
|
||||
Pick<PagerDutyActionParams, 'summary' | 'timestamp' | 'dedupKey'>
|
||||
): Promise<
|
||||
GenericValidationResult<Pick<PagerDutyActionParams, 'summary' | 'timestamp' | 'dedupKey'>>
|
||||
> => {
|
||||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
summary: new Array<string>(),
|
||||
timestamp: new Array<string>(),
|
||||
|
@ -79,27 +74,13 @@ export function getActionType(): ActionTypeModel<
|
|||
!actionParams.dedupKey?.length &&
|
||||
(actionParams.eventAction === 'resolve' || actionParams.eventAction === 'acknowledge')
|
||||
) {
|
||||
errors.dedupKey.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredDedupKeyText',
|
||||
{
|
||||
defaultMessage: 'DedupKey is required when resolving or acknowledging an incident.',
|
||||
}
|
||||
)
|
||||
);
|
||||
errors.dedupKey.push(translations.DEDUP_KEY_REQUIRED);
|
||||
}
|
||||
if (
|
||||
actionParams.eventAction === EventActionOptions.TRIGGER &&
|
||||
!actionParams.summary?.length
|
||||
) {
|
||||
errors.summary.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText',
|
||||
{
|
||||
defaultMessage: 'Summary is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
errors.summary.push(translations.SUMMARY_REQUIRED);
|
||||
}
|
||||
if (actionParams.timestamp && !hasMustacheTokens(actionParams.timestamp)) {
|
||||
if (isNaN(Date.parse(actionParams.timestamp))) {
|
||||
|
|
|
@ -20,6 +20,9 @@ const PagerDutyActionConnectorFields: React.FunctionComponent<
|
|||
const { docLinks } = useKibana().services;
|
||||
const { apiUrl } = action.config;
|
||||
const { routingKey } = action.secrets;
|
||||
const isRoutingKeyInvalid: boolean =
|
||||
routingKey !== undefined && errors.routingKey !== undefined && errors.routingKey.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
|
@ -60,7 +63,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent<
|
|||
</EuiLink>
|
||||
}
|
||||
error={errors.routingKey}
|
||||
isInvalid={errors.routingKey.length > 0 && routingKey !== undefined}
|
||||
isInvalid={isRoutingKeyInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel',
|
||||
{
|
||||
|
@ -80,7 +83,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent<
|
|||
)}
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={errors.routingKey.length > 0 && routingKey !== undefined}
|
||||
isInvalid={isRoutingKeyInvalid}
|
||||
name="routingKey"
|
||||
readOnly={readOnly}
|
||||
value={routingKey || ''}
|
||||
|
|
|
@ -101,6 +101,12 @@ const PagerDutyParamsFields: React.FunctionComponent<ActionParamsProps<PagerDuty
|
|||
const isDedupeKeyRequired = eventAction !== 'trigger';
|
||||
const isTriggerPagerDutyEvent = eventAction === 'trigger';
|
||||
|
||||
const isDedupKeyInvalid: boolean = errors.dedupKey !== undefined && errors.dedupKey.length > 0;
|
||||
const isSummaryInvalid: boolean =
|
||||
errors.summary !== undefined && errors.summary.length > 0 && summary !== undefined;
|
||||
const isTimestampInvalid: boolean =
|
||||
errors.timestamp !== undefined && errors.timestamp.length > 0 && timestamp !== undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
|
@ -132,7 +138,7 @@ const PagerDutyParamsFields: React.FunctionComponent<ActionParamsProps<PagerDuty
|
|||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors.dedupKey}
|
||||
isInvalid={errors.dedupKey.length > 0}
|
||||
isInvalid={isDedupKeyInvalid}
|
||||
label={
|
||||
isDedupeKeyRequired
|
||||
? i18n.translate(
|
||||
|
@ -166,7 +172,7 @@ const PagerDutyParamsFields: React.FunctionComponent<ActionParamsProps<PagerDuty
|
|||
id="pagerDutySummary"
|
||||
fullWidth
|
||||
error={errors.summary}
|
||||
isInvalid={errors.summary.length > 0 && summary !== undefined}
|
||||
isInvalid={isSummaryInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel',
|
||||
{
|
||||
|
@ -180,7 +186,7 @@ const PagerDutyParamsFields: React.FunctionComponent<ActionParamsProps<PagerDuty
|
|||
messageVariables={messageVariables}
|
||||
paramsProperty={'summary'}
|
||||
inputTargetValue={summary}
|
||||
errors={errors.summary as string[]}
|
||||
errors={(errors.summary ?? []) as string[]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
|
@ -211,7 +217,7 @@ const PagerDutyParamsFields: React.FunctionComponent<ActionParamsProps<PagerDuty
|
|||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors.timestamp}
|
||||
isInvalid={errors.timestamp.length > 0 && timestamp !== undefined}
|
||||
isInvalid={isTimestampInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SUMMARY_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText',
|
||||
{
|
||||
defaultMessage: 'Summary is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const DEDUP_KEY_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredDedupKeyText',
|
||||
{
|
||||
defaultMessage: 'DedupKey is required when resolving or acknowledging an incident.',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATION_KEY_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText',
|
||||
{
|
||||
defaultMessage: 'An integration key / routing key is required.',
|
||||
}
|
||||
);
|
|
@ -29,7 +29,7 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
|
||||
describe('resilient connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiKeyId: 'email',
|
||||
|
@ -45,7 +45,7 @@ describe('resilient connector validation', () => {
|
|||
},
|
||||
} as ResilientActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
|
@ -61,7 +61,7 @@ describe('resilient connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid', () => {
|
||||
test('connector validation fails when connector config is not valid', async () => {
|
||||
const actionConnector = ({
|
||||
secrets: {
|
||||
apiKeyId: 'user',
|
||||
|
@ -72,7 +72,7 @@ describe('resilient connector validation', () => {
|
|||
config: {},
|
||||
} as unknown) as ResilientActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: ['URL is required.'],
|
||||
|
@ -90,22 +90,22 @@ describe('resilient connector validation', () => {
|
|||
});
|
||||
|
||||
describe('resilient action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
subActionParams: { incident: { name: 'some title {{test}}' }, comments: [] },
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { 'subActionParams.incident.name': [] },
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when body is not valid', () => {
|
||||
test('params validation fails when body is not valid', async () => {
|
||||
const actionParams = {
|
||||
subActionParams: { incident: { name: '' }, comments: [] },
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
'subActionParams.incident.name': ['Name is required.'],
|
||||
},
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
GenericValidationResult,
|
||||
ActionTypeModel,
|
||||
|
@ -17,12 +18,12 @@ import {
|
|||
ResilientSecrets,
|
||||
ResilientActionParams,
|
||||
} from './types';
|
||||
import * as i18n from './translations';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
|
||||
const validateConnector = (
|
||||
const validateConnector = async (
|
||||
action: ResilientActionConnector
|
||||
): ConnectorValidationResult<ResilientConfig, ResilientSecrets> => {
|
||||
): Promise<ConnectorValidationResult<ResilientConfig, ResilientSecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
apiUrl: new Array<string>(),
|
||||
orgId: new Array<string>(),
|
||||
|
@ -38,32 +39,49 @@ const validateConnector = (
|
|||
};
|
||||
|
||||
if (!action.config.apiUrl) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRED];
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED];
|
||||
}
|
||||
|
||||
if (action.config.apiUrl) {
|
||||
if (!isValidUrl(action.config.apiUrl)) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_INVALID];
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID];
|
||||
} else if (!isValidUrl(action.config.apiUrl, 'https:')) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRE_HTTPS];
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS];
|
||||
}
|
||||
}
|
||||
|
||||
if (!action.config.orgId) {
|
||||
configErrors.orgId = [...configErrors.orgId, i18n.ORG_ID_REQUIRED];
|
||||
configErrors.orgId = [...configErrors.orgId, translations.ORG_ID_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.apiKeyId) {
|
||||
secretsErrors.apiKeyId = [...secretsErrors.apiKeyId, i18n.API_KEY_ID_REQUIRED];
|
||||
secretsErrors.apiKeyId = [...secretsErrors.apiKeyId, translations.API_KEY_ID_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.apiKeySecret) {
|
||||
secretsErrors.apiKeySecret = [...secretsErrors.apiKeySecret, i18n.API_KEY_SECRET_REQUIRED];
|
||||
secretsErrors.apiKeySecret = [
|
||||
...secretsErrors.apiKeySecret,
|
||||
translations.API_KEY_SECRET_REQUIRED,
|
||||
];
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
};
|
||||
|
||||
export const DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create an incident in IBM Resilient.',
|
||||
}
|
||||
);
|
||||
|
||||
export const TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Resilient',
|
||||
}
|
||||
);
|
||||
|
||||
export function getActionType(): ActionTypeModel<
|
||||
ResilientConfig,
|
||||
ResilientSecrets,
|
||||
|
@ -72,11 +90,14 @@ export function getActionType(): ActionTypeModel<
|
|||
return {
|
||||
id: '.resilient',
|
||||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: i18n.DESC,
|
||||
actionTypeTitle: i18n.TITLE,
|
||||
selectMessage: DESC,
|
||||
actionTypeTitle: TITLE,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./resilient_connectors')),
|
||||
validateParams: (actionParams: ResilientActionParams): GenericValidationResult<unknown> => {
|
||||
validateParams: async (
|
||||
actionParams: ResilientActionParams
|
||||
): Promise<GenericValidationResult<unknown>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
'subActionParams.incident.name': new Array<string>(),
|
||||
};
|
||||
|
@ -88,7 +109,7 @@ export function getActionType(): ActionTypeModel<
|
|||
actionParams.subActionParams.incident &&
|
||||
!actionParams.subActionParams.incident.name?.length
|
||||
) {
|
||||
errors['subActionParams.incident.name'].push(i18n.NAME_REQUIRED);
|
||||
errors['subActionParams.incident.name'].push(translations.NAME_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
|
|
|
@ -30,14 +30,19 @@ const ResilientConnectorFields: React.FC<ActionConnectorFieldsProps<ResilientAct
|
|||
readOnly,
|
||||
}) => {
|
||||
const { apiUrl, orgId } = action.config;
|
||||
const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl !== undefined;
|
||||
const isApiUrlInvalid: boolean =
|
||||
apiUrl !== undefined && errors.apiUrl !== undefined && errors.apiUrl.length > 0;
|
||||
|
||||
const { apiKeyId, apiKeySecret } = action.secrets;
|
||||
|
||||
const isOrgIdInvalid: boolean = errors.orgId.length > 0 && orgId !== undefined;
|
||||
const isApiKeyInvalid: boolean = errors.apiKeyId.length > 0 && apiKeyId !== undefined;
|
||||
const isOrgIdInvalid: boolean =
|
||||
orgId !== undefined && errors.orgId !== undefined && errors.orgId.length > 0;
|
||||
const isApiKeyInvalid: boolean =
|
||||
apiKeyId !== undefined && errors.apiKeyId !== undefined && errors.apiKeyId.length > 0;
|
||||
const isApiKeySecretInvalid: boolean =
|
||||
errors.apiKeySecret.length > 0 && apiKeySecret !== undefined;
|
||||
apiKeySecret !== undefined &&
|
||||
errors.apiKeySecret !== undefined &&
|
||||
errors.apiKeySecret.length > 0;
|
||||
|
||||
const handleOnChangeActionConfig = useCallback(
|
||||
(key: string, value: string) => editActionConfig(key, value),
|
||||
|
|
|
@ -213,7 +213,9 @@ const ResilientParamsFields: React.FunctionComponent<ActionParamsProps<Resilient
|
|||
fullWidth
|
||||
error={errors['subActionParams.incident.name']}
|
||||
isInvalid={
|
||||
errors['subActionParams.incident.name'].length > 0 && incident.name !== undefined
|
||||
errors['subActionParams.incident.name'] !== undefined &&
|
||||
errors['subActionParams.incident.name'].length > 0 &&
|
||||
incident.name !== undefined
|
||||
}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.nameFieldLabel',
|
||||
|
@ -226,7 +228,7 @@ const ResilientParamsFields: React.FunctionComponent<ActionParamsProps<Resilient
|
|||
messageVariables={messageVariables}
|
||||
paramsProperty={'name'}
|
||||
inputTargetValue={incident.name ?? undefined}
|
||||
errors={errors['subActionParams.incident.name'] as string[]}
|
||||
errors={(errors['subActionParams.incident.name'] ?? []) as string[]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<TextAreaWithMessageVariables
|
||||
|
|
|
@ -7,20 +7,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create an incident in IBM Resilient.',
|
||||
}
|
||||
);
|
||||
|
||||
export const TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Resilient',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel',
|
||||
{
|
||||
|
|
|
@ -29,7 +29,7 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
|
||||
describe('server-log connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector: UserConfiguredActionConnector<{}, {}> = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
|
@ -39,7 +39,7 @@ describe('server-log connector validation', () => {
|
|||
isPreconfigured: false,
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
|
@ -51,23 +51,23 @@ describe('server-log connector validation', () => {
|
|||
});
|
||||
|
||||
describe('action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
message: 'test message',
|
||||
level: 'trace',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { message: [] },
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when message is not valid', () => {
|
||||
test('params validation fails when message is not valid', async () => {
|
||||
const actionParams = {
|
||||
message: '',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
message: ['Message is required.'],
|
||||
},
|
||||
|
|
|
@ -30,12 +30,12 @@ export function getActionType(): ActionTypeModel<unknown, unknown, ServerLogActi
|
|||
defaultMessage: 'Send to Server log',
|
||||
}
|
||||
),
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return { config: { errors: {} }, secrets: { errors: {} } };
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({ config: { errors: {} }, secrets: { errors: {} } });
|
||||
},
|
||||
validateParams: (
|
||||
actionParams: ServerLogActionParams
|
||||
): GenericValidationResult<Pick<ServerLogActionParams, 'message'>> => {
|
||||
): Promise<GenericValidationResult<Pick<ServerLogActionParams, 'message'>>> => {
|
||||
const errors = {
|
||||
message: new Array<string>(),
|
||||
};
|
||||
|
@ -50,7 +50,7 @@ export function getActionType(): ActionTypeModel<unknown, unknown, ServerLogActi
|
|||
)
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: lazy(() => import('./server_log_params')),
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
|
||||
describe('servicenow connector validation', () => {
|
||||
[SERVICENOW_ITSM_ACTION_TYPE_ID, SERVICENOW_SIR_ACTION_TYPE_ID].forEach((id) => {
|
||||
test(`${id}: connector validation succeeds when connector config is valid`, () => {
|
||||
test(`${id}: connector validation succeeds when connector config is valid`, async () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
|
@ -46,7 +46,7 @@ describe('servicenow connector validation', () => {
|
|||
},
|
||||
} as ServiceNowActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
|
@ -61,7 +61,7 @@ describe('servicenow connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test(`${id}: connector validation fails when connector config is not valid`, () => {
|
||||
test(`${id}: connector validation fails when connector config is not valid`, async () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionConnector = ({
|
||||
secrets: {
|
||||
|
@ -73,7 +73,7 @@ describe('servicenow connector validation', () => {
|
|||
config: {},
|
||||
} as unknown) as ServiceNowActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: ['URL is required.'],
|
||||
|
@ -92,24 +92,24 @@ describe('servicenow connector validation', () => {
|
|||
|
||||
describe('servicenow action params validation', () => {
|
||||
[SERVICENOW_ITSM_ACTION_TYPE_ID, SERVICENOW_SIR_ACTION_TYPE_ID].forEach((id) => {
|
||||
test(`${id}: action params validation succeeds when action params is valid`, () => {
|
||||
test(`${id}: action params validation succeeds when action params is valid`, async () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionParams = {
|
||||
subActionParams: { incident: { short_description: 'some title {{test}}' }, comments: [] },
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { ['subActionParams.incident.short_description']: [] },
|
||||
});
|
||||
});
|
||||
|
||||
test(`${id}: params validation fails when body is not valid`, () => {
|
||||
test(`${id}: params validation fails when body is not valid`, async () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionParams = {
|
||||
subActionParams: { incident: { short_description: '' }, comments: [] },
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
['subActionParams.incident.short_description']: ['Short description is required.'],
|
||||
},
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
GenericValidationResult,
|
||||
ActionTypeModel,
|
||||
|
@ -18,12 +19,12 @@ import {
|
|||
ServiceNowITSMActionParams,
|
||||
ServiceNowSIRActionParams,
|
||||
} from './types';
|
||||
import * as i18n from './translations';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
|
||||
const validateConnector = (
|
||||
const validateConnector = async (
|
||||
action: ServiceNowActionConnector
|
||||
): ConnectorValidationResult<ServiceNowConfig, ServiceNowSecrets> => {
|
||||
): Promise<ConnectorValidationResult<ServiceNowConfig, ServiceNowSecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
apiUrl: new Array<string>(),
|
||||
};
|
||||
|
@ -38,28 +39,56 @@ const validateConnector = (
|
|||
};
|
||||
|
||||
if (!action.config.apiUrl) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRED];
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED];
|
||||
}
|
||||
|
||||
if (action.config.apiUrl) {
|
||||
if (!isValidUrl(action.config.apiUrl)) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_INVALID];
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID];
|
||||
} else if (!isValidUrl(action.config.apiUrl, 'https:')) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRE_HTTPS];
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS];
|
||||
}
|
||||
}
|
||||
|
||||
if (!action.secrets.username) {
|
||||
secretsErrors.username = [...secretsErrors.username, i18n.USERNAME_REQUIRED];
|
||||
secretsErrors.username = [...secretsErrors.username, translations.USERNAME_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.password) {
|
||||
secretsErrors.password = [...secretsErrors.password, i18n.PASSWORD_REQUIRED];
|
||||
secretsErrors.password = [...secretsErrors.password, translations.PASSWORD_REQUIRED];
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
};
|
||||
|
||||
export const SERVICENOW_ITSM_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create an incident in ServiceNow ITSM.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICENOW_SIR_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create an incident in ServiceNow SecOps.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICENOW_ITSM_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'ServiceNow ITSM',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICENOW_SIR_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'ServiceNow SecOps',
|
||||
}
|
||||
);
|
||||
|
||||
export function getServiceNowITSMActionType(): ActionTypeModel<
|
||||
ServiceNowConfig,
|
||||
ServiceNowSecrets,
|
||||
|
@ -68,13 +97,14 @@ export function getServiceNowITSMActionType(): ActionTypeModel<
|
|||
return {
|
||||
id: '.servicenow',
|
||||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: i18n.SERVICENOW_ITSM_DESC,
|
||||
actionTypeTitle: i18n.SERVICENOW_ITSM_TITLE,
|
||||
selectMessage: SERVICENOW_ITSM_DESC,
|
||||
actionTypeTitle: SERVICENOW_ITSM_TITLE,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./servicenow_connectors')),
|
||||
validateParams: (
|
||||
validateParams: async (
|
||||
actionParams: ServiceNowITSMActionParams
|
||||
): GenericValidationResult<unknown> => {
|
||||
): Promise<GenericValidationResult<unknown>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'subActionParams.incident.short_description': new Array<string>(),
|
||||
|
@ -87,7 +117,7 @@ export function getServiceNowITSMActionType(): ActionTypeModel<
|
|||
actionParams.subActionParams.incident &&
|
||||
!actionParams.subActionParams.incident.short_description?.length
|
||||
) {
|
||||
errors['subActionParams.incident.short_description'].push(i18n.TITLE_REQUIRED);
|
||||
errors['subActionParams.incident.short_description'].push(translations.TITLE_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
|
@ -103,11 +133,14 @@ export function getServiceNowSIRActionType(): ActionTypeModel<
|
|||
return {
|
||||
id: '.servicenow-sir',
|
||||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: i18n.SERVICENOW_SIR_DESC,
|
||||
actionTypeTitle: i18n.SERVICENOW_SIR_TITLE,
|
||||
selectMessage: SERVICENOW_SIR_DESC,
|
||||
actionTypeTitle: SERVICENOW_SIR_TITLE,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./servicenow_connectors')),
|
||||
validateParams: (actionParams: ServiceNowSIRActionParams): GenericValidationResult<unknown> => {
|
||||
validateParams: async (
|
||||
actionParams: ServiceNowSIRActionParams
|
||||
): Promise<GenericValidationResult<unknown>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'subActionParams.incident.short_description': new Array<string>(),
|
||||
|
@ -120,7 +153,7 @@ export function getServiceNowSIRActionType(): ActionTypeModel<
|
|||
actionParams.subActionParams.incident &&
|
||||
!actionParams.subActionParams.incident.short_description?.length
|
||||
) {
|
||||
errors['subActionParams.incident.short_description'].push(i18n.TITLE_REQUIRED);
|
||||
errors['subActionParams.incident.short_description'].push(translations.TITLE_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
|
|
|
@ -32,12 +32,15 @@ const ServiceNowConnectorFields: React.FC<
|
|||
const { docLinks } = useKibana().services;
|
||||
const { apiUrl } = action.config;
|
||||
|
||||
const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl !== undefined;
|
||||
const isApiUrlInvalid: boolean =
|
||||
errors.apiUrl !== undefined && errors.apiUrl.length > 0 && apiUrl !== undefined;
|
||||
|
||||
const { username, password } = action.secrets;
|
||||
|
||||
const isUsernameInvalid: boolean = errors.username.length > 0 && username !== undefined;
|
||||
const isPasswordInvalid: boolean = errors.password.length > 0 && password !== undefined;
|
||||
const isUsernameInvalid: boolean =
|
||||
errors.username !== undefined && errors.username.length > 0 && username !== undefined;
|
||||
const isPasswordInvalid: boolean =
|
||||
errors.password !== undefined && errors.password.length > 0 && password !== undefined;
|
||||
|
||||
const handleOnChangeActionConfig = useCallback(
|
||||
(key: string, value: string) => editActionConfig(key, value),
|
||||
|
|
|
@ -240,6 +240,7 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
fullWidth
|
||||
error={errors['subActionParams.incident.short_description']}
|
||||
isInvalid={
|
||||
errors['subActionParams.incident.short_description'] !== undefined &&
|
||||
errors['subActionParams.incident.short_description'].length > 0 &&
|
||||
incident.short_description !== undefined
|
||||
}
|
||||
|
|
|
@ -151,6 +151,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent<
|
|||
fullWidth
|
||||
error={errors['subActionParams.incident.short_description']}
|
||||
isInvalid={
|
||||
errors['subActionParams.incident.short_description'] !== undefined &&
|
||||
errors['subActionParams.incident.short_description'].length > 0 &&
|
||||
incident.short_description !== undefined
|
||||
}
|
||||
|
|
|
@ -7,34 +7,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SERVICENOW_ITSM_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create an incident in ServiceNow ITSM.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICENOW_SIR_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Create an incident in ServiceNow SecOps.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICENOW_ITSM_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'ServiceNow ITSM',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICENOW_SIR_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'ServiceNow SecOps',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel',
|
||||
{
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
|
||||
describe('slack connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'https:\\test',
|
||||
|
@ -41,7 +41,7 @@ describe('slack connector validation', () => {
|
|||
config: {},
|
||||
} as SlackActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
|
@ -53,7 +53,7 @@ describe('slack connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - no webhook url', () => {
|
||||
test('connector validation fails when connector config is not valid - no webhook url', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
|
@ -62,7 +62,7 @@ describe('slack connector validation', () => {
|
|||
config: {},
|
||||
} as SlackActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
|
@ -74,7 +74,7 @@ describe('slack connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - invalid webhook protocol', () => {
|
||||
test('connector validation fails when connector config is not valid - invalid webhook protocol', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http:\\test',
|
||||
|
@ -85,7 +85,7 @@ describe('slack connector validation', () => {
|
|||
config: {},
|
||||
} as SlackActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
|
@ -97,7 +97,7 @@ describe('slack connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - invalid webhook url', () => {
|
||||
test('connector validation fails when connector config is not valid - invalid webhook url', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'h',
|
||||
|
@ -108,7 +108,7 @@ describe('slack connector validation', () => {
|
|||
config: {},
|
||||
} as SlackActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
|
@ -122,22 +122,22 @@ describe('slack connector validation', () => {
|
|||
});
|
||||
|
||||
describe('slack action params validation', () => {
|
||||
test('if action params validation succeeds when action params is valid', () => {
|
||||
test('if action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
message: 'message {test}',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { message: [] },
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when message is not valid', () => {
|
||||
test('params validation fails when message is not valid', async () => {
|
||||
const actionParams = {
|
||||
message: '',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
message: ['Message is required.'],
|
||||
},
|
||||
|
|
|
@ -31,61 +31,35 @@ export function getActionType(): ActionTypeModel<unknown, SlackSecrets, SlackAct
|
|||
defaultMessage: 'Send to Slack',
|
||||
}
|
||||
),
|
||||
validateConnector: (
|
||||
validateConnector: async (
|
||||
action: SlackActionConnector
|
||||
): ConnectorValidationResult<unknown, SlackSecrets> => {
|
||||
): Promise<ConnectorValidationResult<unknown, SlackSecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const secretsErrors = {
|
||||
webhookUrl: new Array<string>(),
|
||||
};
|
||||
const validationResult = { config: { errors: {} }, secrets: { errors: secretsErrors } };
|
||||
if (!action.secrets.webhookUrl) {
|
||||
secretsErrors.webhookUrl.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_REQUIRED);
|
||||
} else if (action.secrets.webhookUrl) {
|
||||
if (!isValidUrl(action.secrets.webhookUrl)) {
|
||||
secretsErrors.webhookUrl.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.invalidWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL is invalid.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_INVALID);
|
||||
} else if (!isValidUrl(action.secrets.webhookUrl, 'https:')) {
|
||||
secretsErrors.webhookUrl.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requireHttpsWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL must start with https://.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_HTTP_INVALID);
|
||||
}
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: (
|
||||
validateParams: async (
|
||||
actionParams: SlackActionParams
|
||||
): GenericValidationResult<SlackActionParams> => {
|
||||
): Promise<GenericValidationResult<SlackActionParams>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
message: new Array<string>(),
|
||||
};
|
||||
const validationResult = { errors };
|
||||
if (!actionParams.message?.length) {
|
||||
errors.message.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText',
|
||||
{
|
||||
defaultMessage: 'Message is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
errors.message.push(translations.MESSAGE_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
|
|
|
@ -19,6 +19,8 @@ const SlackActionFields: React.FunctionComponent<
|
|||
> = ({ action, editActionSecrets, errors, readOnly }) => {
|
||||
const { docLinks } = useKibana().services;
|
||||
const { webhookUrl } = action.secrets;
|
||||
const isWebhookUrlInvalid: boolean =
|
||||
errors.webhookUrl !== undefined && errors.webhookUrl.length > 0 && webhookUrl !== undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -34,7 +36,7 @@ const SlackActionFields: React.FunctionComponent<
|
|||
</EuiLink>
|
||||
}
|
||||
error={errors.webhookUrl}
|
||||
isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined}
|
||||
isInvalid={isWebhookUrlInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel',
|
||||
{
|
||||
|
@ -54,7 +56,7 @@ const SlackActionFields: React.FunctionComponent<
|
|||
)}
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined}
|
||||
isInvalid={isWebhookUrlInvalid}
|
||||
name="webhookUrl"
|
||||
readOnly={readOnly}
|
||||
value={webhookUrl || ''}
|
||||
|
|
|
@ -49,7 +49,7 @@ const SlackParamsFields: React.FunctionComponent<ActionParamsProps<SlackActionPa
|
|||
defaultMessage: 'Message',
|
||||
}
|
||||
)}
|
||||
errors={errors.message as string[]}
|
||||
errors={(errors.message ?? []) as string[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const WEBHOOK_URL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const WEBHOOK_URL_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.invalidWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL is invalid.',
|
||||
}
|
||||
);
|
||||
|
||||
export const WEBHOOK_URL_HTTP_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requireHttpsWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL must start with https://.',
|
||||
}
|
||||
);
|
||||
|
||||
export const MESSAGE_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText',
|
||||
{
|
||||
defaultMessage: 'Message is required.',
|
||||
}
|
||||
);
|
|
@ -29,7 +29,7 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
|
||||
describe('teams connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'https:\\test',
|
||||
|
@ -40,7 +40,7 @@ describe('teams connector validation', () => {
|
|||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
|
@ -52,7 +52,7 @@ describe('teams connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - empty webhook url', () => {
|
||||
test('connector validation fails when connector config is not valid - empty webhook url', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
|
@ -61,7 +61,7 @@ describe('teams connector validation', () => {
|
|||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
|
@ -73,7 +73,7 @@ describe('teams connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - invalid webhook url', () => {
|
||||
test('connector validation fails when connector config is not valid - invalid webhook url', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'h',
|
||||
|
@ -84,7 +84,7 @@ describe('teams connector validation', () => {
|
|||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
|
@ -96,7 +96,7 @@ describe('teams connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - invalid webhook url protocol', () => {
|
||||
test('connector validation fails when connector config is not valid - invalid webhook url protocol', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http://insecure',
|
||||
|
@ -107,7 +107,7 @@ describe('teams connector validation', () => {
|
|||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
|
@ -121,22 +121,22 @@ describe('teams connector validation', () => {
|
|||
});
|
||||
|
||||
describe('teams action params validation', () => {
|
||||
test('if action params validation succeeds when action params is valid', () => {
|
||||
test('if action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
message: 'message {test}',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { message: [] },
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when message is not valid', () => {
|
||||
test('params validation fails when message is not valid', async () => {
|
||||
const actionParams = {
|
||||
message: '',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
message: ['Message is required.'],
|
||||
},
|
||||
|
|
|
@ -31,61 +31,35 @@ export function getActionType(): ActionTypeModel<unknown, TeamsSecrets, TeamsAct
|
|||
defaultMessage: 'Send a message to a Microsoft Teams channel.',
|
||||
}
|
||||
),
|
||||
validateConnector: (
|
||||
validateConnector: async (
|
||||
action: TeamsActionConnector
|
||||
): ConnectorValidationResult<unknown, TeamsSecrets> => {
|
||||
): Promise<ConnectorValidationResult<unknown, TeamsSecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const secretsErrors = {
|
||||
webhookUrl: new Array<string>(),
|
||||
};
|
||||
const validationResult = { config: { errors: {} }, secrets: { errors: secretsErrors } };
|
||||
if (!action.secrets.webhookUrl) {
|
||||
secretsErrors.webhookUrl.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_REQUIRED);
|
||||
} else if (action.secrets.webhookUrl) {
|
||||
if (!isValidUrl(action.secrets.webhookUrl)) {
|
||||
secretsErrors.webhookUrl.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL is invalid.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_INVALID);
|
||||
} else if (!isValidUrl(action.secrets.webhookUrl, 'https:')) {
|
||||
secretsErrors.webhookUrl.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL must start with https://.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_HTTP_INVALID);
|
||||
}
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: (
|
||||
validateParams: async (
|
||||
actionParams: TeamsActionParams
|
||||
): GenericValidationResult<TeamsActionParams> => {
|
||||
): Promise<GenericValidationResult<TeamsActionParams>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
message: new Array<string>(),
|
||||
};
|
||||
const validationResult = { errors };
|
||||
if (!actionParams.message?.length) {
|
||||
errors.message.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText',
|
||||
{
|
||||
defaultMessage: 'Message is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
errors.message.push(translations.MESSAGE_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
|
|
|
@ -20,6 +20,9 @@ const TeamsActionFields: React.FunctionComponent<
|
|||
const { webhookUrl } = action.secrets;
|
||||
const { docLinks } = useKibana().services;
|
||||
|
||||
const isWebhookUrlInvalid: boolean =
|
||||
errors.webhookUrl !== undefined && errors.webhookUrl.length > 0 && webhookUrl !== undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
|
@ -34,7 +37,7 @@ const TeamsActionFields: React.FunctionComponent<
|
|||
</EuiLink>
|
||||
}
|
||||
error={errors.webhookUrl}
|
||||
isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined}
|
||||
isInvalid={isWebhookUrlInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel',
|
||||
{
|
||||
|
@ -54,7 +57,7 @@ const TeamsActionFields: React.FunctionComponent<
|
|||
)}
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined}
|
||||
isInvalid={isWebhookUrlInvalid}
|
||||
name="webhookUrl"
|
||||
readOnly={readOnly}
|
||||
value={webhookUrl || ''}
|
||||
|
|
|
@ -40,7 +40,7 @@ const TeamsParamsFields: React.FunctionComponent<ActionParamsProps<TeamsActionPa
|
|||
defaultMessage: 'Message',
|
||||
}
|
||||
)}
|
||||
errors={errors.message as string[]}
|
||||
errors={(errors.message ?? []) as string[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const WEBHOOK_URL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const WEBHOOK_URL_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL is invalid.',
|
||||
}
|
||||
);
|
||||
|
||||
export const WEBHOOK_URL_HTTP_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL must start with https://.',
|
||||
}
|
||||
);
|
||||
|
||||
export const MESSAGE_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText',
|
||||
{
|
||||
defaultMessage: 'Message is required.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const URL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText',
|
||||
{
|
||||
defaultMessage: 'URL is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const URL_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is invalid.',
|
||||
}
|
||||
);
|
||||
|
||||
export const METHOD_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText',
|
||||
{
|
||||
defaultMessage: 'Method is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const USERNAME_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText',
|
||||
{
|
||||
defaultMessage: 'Username is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText',
|
||||
{
|
||||
defaultMessage: 'Password is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_REQUIRED_FOR_USER = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText',
|
||||
{
|
||||
defaultMessage: 'Password is required when username is used.',
|
||||
}
|
||||
);
|
||||
|
||||
export const USERNAME_REQUIRED_FOR_PASSWORD = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText',
|
||||
{
|
||||
defaultMessage: 'Username is required when password is used.',
|
||||
}
|
||||
);
|
||||
|
||||
export const BODY_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText',
|
||||
{
|
||||
defaultMessage: 'Body is required.',
|
||||
}
|
||||
);
|
|
@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
|
||||
describe('webhook connector validation', () => {
|
||||
test('connector validation succeeds when hasAuth is true and connector config is valid', () => {
|
||||
test('connector validation succeeds when hasAuth is true and connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
|
@ -48,7 +48,7 @@ describe('webhook connector validation', () => {
|
|||
},
|
||||
} as WebhookActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
url: [],
|
||||
|
@ -64,7 +64,7 @@ describe('webhook connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation succeeds when hasAuth is false and connector config is valid', () => {
|
||||
test('connector validation succeeds when hasAuth is false and connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: '',
|
||||
|
@ -82,7 +82,7 @@ describe('webhook connector validation', () => {
|
|||
},
|
||||
} as WebhookActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
url: [],
|
||||
|
@ -98,7 +98,7 @@ describe('webhook connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid', () => {
|
||||
test('connector validation fails when connector config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
|
@ -112,7 +112,7 @@ describe('webhook connector validation', () => {
|
|||
},
|
||||
} as WebhookActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
url: ['URL is required.'],
|
||||
|
@ -128,7 +128,7 @@ describe('webhook connector validation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when url in config is not valid', () => {
|
||||
test('connector validation fails when url in config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
|
@ -144,7 +144,7 @@ describe('webhook connector validation', () => {
|
|||
},
|
||||
} as WebhookActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
url: ['URL is invalid.'],
|
||||
|
@ -162,22 +162,22 @@ describe('webhook connector validation', () => {
|
|||
});
|
||||
|
||||
describe('webhook action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
body: 'message {test}',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { body: [] },
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when body is not valid', () => {
|
||||
test('params validation fails when body is not valid', async () => {
|
||||
const actionParams = {
|
||||
body: '',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
body: ['Body is required.'],
|
||||
},
|
||||
|
|
|
@ -40,9 +40,12 @@ export function getActionType(): ActionTypeModel<
|
|||
defaultMessage: 'Webhook data',
|
||||
}
|
||||
),
|
||||
validateConnector: (
|
||||
validateConnector: async (
|
||||
action: WebhookActionConnector
|
||||
): ConnectorValidationResult<Pick<WebhookConfig, 'url' | 'method'>, WebhookSecrets> => {
|
||||
): Promise<
|
||||
ConnectorValidationResult<Pick<WebhookConfig, 'url' | 'method'>, WebhookSecrets>
|
||||
> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
url: new Array<string>(),
|
||||
method: new Array<string>(),
|
||||
|
@ -56,95 +59,39 @@ export function getActionType(): ActionTypeModel<
|
|||
secrets: { errors: secretsErrors },
|
||||
};
|
||||
if (!action.config.url) {
|
||||
configErrors.url.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText',
|
||||
{
|
||||
defaultMessage: 'URL is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
configErrors.url.push(translations.URL_REQUIRED);
|
||||
}
|
||||
if (action.config.url && !isValidUrl(action.config.url)) {
|
||||
configErrors.url = [
|
||||
...configErrors.url,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is invalid.',
|
||||
}
|
||||
),
|
||||
];
|
||||
configErrors.url = [...configErrors.url, translations.URL_INVALID];
|
||||
}
|
||||
if (!action.config.method) {
|
||||
configErrors.method.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText',
|
||||
{
|
||||
defaultMessage: 'Method is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
configErrors.method.push(translations.METHOD_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.user.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText',
|
||||
{
|
||||
defaultMessage: 'Username is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.user.push(translations.USERNAME_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.password.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText',
|
||||
{
|
||||
defaultMessage: 'Password is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED);
|
||||
}
|
||||
if (action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.password.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText',
|
||||
{
|
||||
defaultMessage: 'Password is required when username is used.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED_FOR_USER);
|
||||
}
|
||||
if (!action.secrets.user && action.secrets.password) {
|
||||
secretsErrors.user.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText',
|
||||
{
|
||||
defaultMessage: 'Username is required when password is used.',
|
||||
}
|
||||
)
|
||||
);
|
||||
secretsErrors.user.push(translations.USERNAME_REQUIRED_FOR_PASSWORD);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: (
|
||||
validateParams: async (
|
||||
actionParams: WebhookActionParams
|
||||
): GenericValidationResult<WebhookActionParams> => {
|
||||
): Promise<GenericValidationResult<WebhookActionParams>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors = {
|
||||
body: new Array<string>(),
|
||||
};
|
||||
const validationResult = { errors };
|
||||
validationResult.errors = errors;
|
||||
if (!actionParams.body?.length) {
|
||||
errors.body.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText',
|
||||
{
|
||||
defaultMessage: 'Body is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
errors.body.push(translations.BODY_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
|
|
|
@ -76,7 +76,11 @@ const WebhookActionConnectorFields: React.FunctionComponent<
|
|||
)
|
||||
);
|
||||
}
|
||||
const hasHeaderErrors = headerErrors.keyHeader.length > 0 || headerErrors.valueHeader.length > 0;
|
||||
const hasHeaderErrors: boolean =
|
||||
(headerErrors.keyHeader !== undefined &&
|
||||
headerErrors.valueHeader !== undefined &&
|
||||
headerErrors.keyHeader.length > 0) ||
|
||||
headerErrors.valueHeader.length > 0;
|
||||
|
||||
function addHeader() {
|
||||
if (headers && !!Object.keys(headers).find((key) => key === httpHeaderKey)) {
|
||||
|
@ -219,6 +223,13 @@ const WebhookActionConnectorFields: React.FunctionComponent<
|
|||
);
|
||||
});
|
||||
|
||||
const isUrlInvalid: boolean =
|
||||
errors.url !== undefined && errors.url.length > 0 && url !== undefined;
|
||||
const isPasswordInvalid: boolean =
|
||||
password !== undefined && errors.password !== undefined && errors.password.length > 0;
|
||||
const isUserInvalid: boolean =
|
||||
user !== undefined && errors.user !== undefined && errors.user.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
|
@ -248,7 +259,7 @@ const WebhookActionConnectorFields: React.FunctionComponent<
|
|||
id="url"
|
||||
fullWidth
|
||||
error={errors.url}
|
||||
isInvalid={errors.url.length > 0 && url !== undefined}
|
||||
isInvalid={isUrlInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.urlTextFieldLabel',
|
||||
{
|
||||
|
@ -258,7 +269,7 @@ const WebhookActionConnectorFields: React.FunctionComponent<
|
|||
>
|
||||
<EuiFieldText
|
||||
name="url"
|
||||
isInvalid={errors.url.length > 0 && url !== undefined}
|
||||
isInvalid={isUrlInvalid}
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
value={url || ''}
|
||||
|
@ -326,7 +337,7 @@ const WebhookActionConnectorFields: React.FunctionComponent<
|
|||
id="webhookUser"
|
||||
fullWidth
|
||||
error={errors.user}
|
||||
isInvalid={errors.user.length > 0 && user !== undefined}
|
||||
isInvalid={isUserInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel',
|
||||
{
|
||||
|
@ -336,7 +347,7 @@ const WebhookActionConnectorFields: React.FunctionComponent<
|
|||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={errors.user.length > 0 && user !== undefined}
|
||||
isInvalid={isUserInvalid}
|
||||
name="user"
|
||||
readOnly={readOnly}
|
||||
value={user || ''}
|
||||
|
@ -357,7 +368,7 @@ const WebhookActionConnectorFields: React.FunctionComponent<
|
|||
id="webhookPassword"
|
||||
fullWidth
|
||||
error={errors.password}
|
||||
isInvalid={errors.password.length > 0 && password !== undefined}
|
||||
isInvalid={isPasswordInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel',
|
||||
{
|
||||
|
@ -369,7 +380,7 @@ const WebhookActionConnectorFields: React.FunctionComponent<
|
|||
fullWidth
|
||||
name="password"
|
||||
readOnly={readOnly}
|
||||
isInvalid={errors.password.length > 0 && password !== undefined}
|
||||
isInvalid={isPasswordInvalid}
|
||||
value={password || ''}
|
||||
data-test-subj="webhookPasswordInput"
|
||||
onChange={(e) => {
|
||||
|
|
|
@ -23,12 +23,12 @@ describe('action_connector_form', () => {
|
|||
id: 'my-action-type',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
});
|
||||
actionTypeRegistry.get.mockReturnValue(actionType);
|
||||
|
|
|
@ -51,11 +51,11 @@ export function validateBaseProperties<ConnectorConfig, ConnectorSecrets>(
|
|||
return validationResult;
|
||||
}
|
||||
|
||||
export function getConnectorErrors<ConnectorConfig, ConnectorSecrets>(
|
||||
export async function getConnectorErrors<ConnectorConfig, ConnectorSecrets>(
|
||||
connector: UserConfiguredActionConnector<ConnectorConfig, ConnectorSecrets>,
|
||||
actionTypeModel: ActionTypeModel
|
||||
) {
|
||||
const connectorValidationResult = actionTypeModel?.validateConnector(connector);
|
||||
const connectorValidationResult = await actionTypeModel?.validateConnector(connector);
|
||||
const configErrors = (connectorValidationResult.config
|
||||
? connectorValidationResult.config.errors
|
||||
: {}) as IErrorObject;
|
||||
|
@ -173,7 +173,8 @@ export const ActionConnectorForm = ({
|
|||
);
|
||||
|
||||
const FieldsComponent = actionTypeRegistered.actionConnectorFields;
|
||||
|
||||
const isNameInvalid: boolean =
|
||||
connector.name !== undefined && errors.name !== undefined && errors.name.length > 0;
|
||||
return (
|
||||
<EuiForm isInvalid={!!serverError} error={serverError?.body.message}>
|
||||
<EuiFormRow
|
||||
|
@ -185,13 +186,13 @@ export const ActionConnectorForm = ({
|
|||
defaultMessage="Connector name"
|
||||
/>
|
||||
}
|
||||
isInvalid={errors.name.length > 0 && connector.name !== undefined}
|
||||
isInvalid={isNameInvalid}
|
||||
error={errors.name}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
readOnly={!canSave}
|
||||
isInvalid={errors.name.length > 0 && connector.name !== undefined}
|
||||
isInvalid={isNameInvalid}
|
||||
name="name"
|
||||
placeholder="Untitled"
|
||||
data-test-subj="nameInput"
|
||||
|
|
|
@ -53,12 +53,12 @@ describe('action_form', () => {
|
|||
id: 'my-action-type',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
|
@ -68,12 +68,12 @@ describe('action_form', () => {
|
|||
id: 'disabled-by-config',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
|
@ -83,12 +83,12 @@ describe('action_form', () => {
|
|||
id: '.jira',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): ValidationResult => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
|
@ -98,12 +98,12 @@ describe('action_form', () => {
|
|||
id: 'disabled-by-license',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
|
@ -113,12 +113,12 @@ describe('action_form', () => {
|
|||
id: 'preconfigured',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
|
@ -30,7 +30,7 @@ import {
|
|||
ActionTypeRegistryContract,
|
||||
} from '../../../types';
|
||||
import { SectionLoading } from '../../components/section_loading';
|
||||
import { ActionTypeForm, ActionTypeFormProps } from './action_type_form';
|
||||
import { ActionTypeForm } from './action_type_form';
|
||||
import { AddConnectorInline } from './connector_add_inline';
|
||||
import { actionTypeCompare } from '../../lib/action_type_compare';
|
||||
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
|
||||
|
@ -357,49 +357,42 @@ export const ActionForm = ({
|
|||
);
|
||||
}
|
||||
|
||||
const actionParamsErrors: ActionTypeFormProps['actionParamsErrors'] = actionTypeRegistry
|
||||
.get(actionItem.actionTypeId)
|
||||
?.validateParams(actionItem.params);
|
||||
|
||||
return (
|
||||
<Fragment key={`action-form-action-at-${index}`}>
|
||||
<ActionTypeForm
|
||||
actionItem={actionItem}
|
||||
actionConnector={actionConnector}
|
||||
actionParamsErrors={actionParamsErrors}
|
||||
index={index}
|
||||
setActionParamsProperty={setActionParamsProperty}
|
||||
actionTypesIndex={actionTypesIndex}
|
||||
connectors={connectors}
|
||||
defaultActionGroupId={defaultActionGroupId}
|
||||
messageVariables={messageVariables}
|
||||
actionGroups={actionGroups}
|
||||
defaultActionMessage={defaultActionMessage}
|
||||
defaultParams={getDefaultActionParams?.(actionItem.actionTypeId, actionItem.group)}
|
||||
isActionGroupDisabledForActionType={isActionGroupDisabledForActionType}
|
||||
setActionGroupIdByIndex={setActionGroupIdByIndex}
|
||||
onAddConnector={() => {
|
||||
setActiveActionItem({ actionTypeId: actionItem.actionTypeId, indices: [index] });
|
||||
setAddModalVisibility(true);
|
||||
}}
|
||||
onConnectorSelected={(id: string) => {
|
||||
setActionIdByIndex(id, index);
|
||||
}}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
onDeleteAction={() => {
|
||||
const updatedActions = actions.filter(
|
||||
(_item: AlertAction, i: number) => i !== index
|
||||
);
|
||||
setActions(updatedActions);
|
||||
setIsAddActionPanelOpen(
|
||||
updatedActions.filter((item: AlertAction) => item.id !== actionItem.id)
|
||||
.length === 0
|
||||
);
|
||||
setActiveActionItem(undefined);
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
<ActionTypeForm
|
||||
actionItem={actionItem}
|
||||
actionConnector={actionConnector}
|
||||
index={index}
|
||||
key={`action-form-action-at-${index}`}
|
||||
setActionParamsProperty={setActionParamsProperty}
|
||||
actionTypesIndex={actionTypesIndex}
|
||||
connectors={connectors}
|
||||
defaultActionGroupId={defaultActionGroupId}
|
||||
messageVariables={messageVariables}
|
||||
actionGroups={actionGroups}
|
||||
defaultActionMessage={defaultActionMessage}
|
||||
defaultParams={getDefaultActionParams?.(actionItem.actionTypeId, actionItem.group)}
|
||||
isActionGroupDisabledForActionType={isActionGroupDisabledForActionType}
|
||||
setActionGroupIdByIndex={setActionGroupIdByIndex}
|
||||
onAddConnector={() => {
|
||||
setActiveActionItem({ actionTypeId: actionItem.actionTypeId, indices: [index] });
|
||||
setAddModalVisibility(true);
|
||||
}}
|
||||
onConnectorSelected={(id: string) => {
|
||||
setActionIdByIndex(id, index);
|
||||
}}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
onDeleteAction={() => {
|
||||
const updatedActions = actions.filter(
|
||||
(_item: AlertAction, i: number) => i !== index
|
||||
);
|
||||
setActions(updatedActions);
|
||||
setIsAddActionPanelOpen(
|
||||
updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
|
||||
0
|
||||
);
|
||||
setActiveActionItem(undefined);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<EuiSpacer size="m" />
|
||||
|
|
|
@ -43,12 +43,12 @@ describe('action_type_form', () => {
|
|||
id: '.pagerduty',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
|
@ -92,12 +92,12 @@ describe('action_type_form', () => {
|
|||
id: '.pagerduty',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
|
@ -220,7 +220,6 @@ function getActionTypeForm(
|
|||
onAddConnector={onAddConnector ?? jest.fn()}
|
||||
onDeleteAction={onDeleteAction ?? jest.fn()}
|
||||
onConnectorSelected={onConnectorSelected ?? jest.fn()}
|
||||
actionParamsErrors={{ errors: { summary: [], timestamp: [], dedupKey: [] } }}
|
||||
defaultActionGroupId={defaultActionGroupId ?? 'default'}
|
||||
setActionParamsProperty={jest.fn()}
|
||||
index={index ?? 1}
|
||||
|
|
|
@ -47,9 +47,6 @@ import { DefaultActionParams } from '../../lib/get_defaults_for_action_params';
|
|||
export type ActionTypeFormProps = {
|
||||
actionItem: AlertAction;
|
||||
actionConnector: ActionConnector;
|
||||
actionParamsErrors: {
|
||||
errors: IErrorObject;
|
||||
};
|
||||
index: number;
|
||||
onAddConnector: () => void;
|
||||
onConnectorSelected: (id: string) => void;
|
||||
|
@ -80,7 +77,6 @@ const preconfiguredMessage = i18n.translate(
|
|||
export const ActionTypeForm = ({
|
||||
actionItem,
|
||||
actionConnector,
|
||||
actionParamsErrors,
|
||||
index,
|
||||
onAddConnector,
|
||||
onConnectorSelected,
|
||||
|
@ -106,6 +102,9 @@ export const ActionTypeForm = ({
|
|||
const selectedActionGroup =
|
||||
actionGroups?.find(({ id }) => id === actionItem.group) ?? defaultActionGroup;
|
||||
const [actionGroup, setActionGroup] = useState<string>();
|
||||
const [actionParamsErrors, setActionParamsErrors] = useState<{ errors: IErrorObject }>({
|
||||
errors: {},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setAvailableActionVariables(
|
||||
|
@ -130,6 +129,16 @@ export const ActionTypeForm = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionGroup]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res: { errors: IErrorObject } = await actionTypeRegistry
|
||||
.get(actionItem.actionTypeId)
|
||||
?.validateParams(actionItem.params);
|
||||
setActionParamsErrors(res);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionItem]);
|
||||
|
||||
const canSave = hasSaveActionsCapability(capabilities);
|
||||
const getSelectedOptions = (actionItemId: string) => {
|
||||
const selectedConnector = connectors.find((connector) => connector.id === actionItemId);
|
||||
|
|
|
@ -40,12 +40,12 @@ describe('connector_add_flyout', () => {
|
|||
id: 'my-action-type',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
});
|
||||
|
@ -77,12 +77,12 @@ describe('connector_add_flyout', () => {
|
|||
id: 'my-action-type',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
});
|
||||
|
@ -114,12 +114,12 @@ describe('connector_add_flyout', () => {
|
|||
id: 'my-action-type',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
});
|
||||
|
|
|
@ -198,12 +198,12 @@ function createActionType() {
|
|||
id: `my-action-type-${++count}`,
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState, useReducer } from 'react';
|
||||
import React, { useCallback, useState, useReducer, useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiTitle,
|
||||
|
@ -30,7 +30,9 @@ import {
|
|||
ActionType,
|
||||
ActionConnector,
|
||||
UserConfiguredActionConnector,
|
||||
IErrorObject,
|
||||
ConnectorAddFlyoutProps,
|
||||
ActionTypeModel,
|
||||
} from '../../../types';
|
||||
import { hasSaveActionsCapability } from '../../lib/capabilities';
|
||||
import { createActionConnector } from '../../lib/action_connector_api';
|
||||
|
@ -38,6 +40,7 @@ import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants';
|
|||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { createConnectorReducer, InitialConnector, ConnectorReducer } from './connector_reducer';
|
||||
import { getConnectorWithInvalidatedFields } from '../../lib/value_validators';
|
||||
import { CenterJustifiedSpinner } from '../../components/center_justified_spinner';
|
||||
|
||||
const ConnectorAddFlyout: React.FunctionComponent<ConnectorAddFlyoutProps> = ({
|
||||
onClose,
|
||||
|
@ -47,7 +50,9 @@ const ConnectorAddFlyout: React.FunctionComponent<ConnectorAddFlyoutProps> = ({
|
|||
consumer,
|
||||
actionTypeRegistry,
|
||||
}) => {
|
||||
let hasErrors = false;
|
||||
const [hasErrors, setHasErrors] = useState<boolean>(true);
|
||||
let actionTypeModel: ActionTypeModel | undefined;
|
||||
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
|
@ -55,7 +60,17 @@ const ConnectorAddFlyout: React.FunctionComponent<ConnectorAddFlyoutProps> = ({
|
|||
} = useKibana().services;
|
||||
const [actionType, setActionType] = useState<ActionType | undefined>(undefined);
|
||||
const [hasActionsUpgradeableByTrial, setHasActionsUpgradeableByTrial] = useState<boolean>(false);
|
||||
|
||||
const [errors, setErrors] = useState<{
|
||||
configErrors: IErrorObject;
|
||||
connectorBaseErrors: IErrorObject;
|
||||
connectorErrors: IErrorObject;
|
||||
secretsErrors: IErrorObject;
|
||||
}>({
|
||||
configErrors: {},
|
||||
connectorBaseErrors: {},
|
||||
connectorErrors: {},
|
||||
secretsErrors: {},
|
||||
});
|
||||
// hooks
|
||||
const initialConnector: InitialConnector<Record<string, unknown>, Record<string, unknown>> = {
|
||||
actionTypeId: actionType?.id ?? '',
|
||||
|
@ -73,6 +88,24 @@ const ConnectorAddFlyout: React.FunctionComponent<ConnectorAddFlyoutProps> = ({
|
|||
Record<string, unknown>
|
||||
>,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (actionTypeModel) {
|
||||
setIsLoading(true);
|
||||
const res = await getConnectorErrors(connector, actionTypeModel);
|
||||
setHasErrors(
|
||||
!!Object.keys(res.connectorErrors).find(
|
||||
(errorKey) => (res.connectorErrors as IErrorObject)[errorKey].length >= 1
|
||||
)
|
||||
);
|
||||
setIsLoading(false);
|
||||
setErrors({ ...res });
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [connector, actionType]);
|
||||
|
||||
const setActionProperty = <Key extends keyof ActionConnector>(
|
||||
key: Key,
|
||||
|
@ -101,7 +134,6 @@ const ConnectorAddFlyout: React.FunctionComponent<ConnectorAddFlyoutProps> = ({
|
|||
}
|
||||
|
||||
let currentForm;
|
||||
let actionTypeModel;
|
||||
let saveButton;
|
||||
if (!actionType) {
|
||||
currentForm = (
|
||||
|
@ -115,22 +147,12 @@ const ConnectorAddFlyout: React.FunctionComponent<ConnectorAddFlyoutProps> = ({
|
|||
} else {
|
||||
actionTypeModel = actionTypeRegistry.get(actionType.id);
|
||||
|
||||
const {
|
||||
configErrors,
|
||||
connectorBaseErrors,
|
||||
connectorErrors,
|
||||
secretsErrors,
|
||||
} = getConnectorErrors(connector, actionTypeModel);
|
||||
hasErrors = !!Object.keys(connectorErrors).find(
|
||||
(errorKey) => connectorErrors[errorKey].length >= 1
|
||||
);
|
||||
|
||||
currentForm = (
|
||||
<ActionConnectorForm
|
||||
actionTypeName={actionType.name}
|
||||
connector={connector}
|
||||
dispatch={dispatch}
|
||||
errors={connectorErrors}
|
||||
errors={errors.connectorErrors}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
consumer={consumer}
|
||||
/>
|
||||
|
@ -170,9 +192,9 @@ const ConnectorAddFlyout: React.FunctionComponent<ConnectorAddFlyoutProps> = ({
|
|||
setConnector(
|
||||
getConnectorWithInvalidatedFields(
|
||||
connector,
|
||||
configErrors,
|
||||
secretsErrors,
|
||||
connectorBaseErrors
|
||||
errors.configErrors,
|
||||
errors.secretsErrors,
|
||||
errors.connectorBaseErrors
|
||||
)
|
||||
);
|
||||
return;
|
||||
|
@ -235,13 +257,13 @@ const ConnectorAddFlyout: React.FunctionComponent<ConnectorAddFlyoutProps> = ({
|
|||
<EuiFlyout onClose={closeFlyout} aria-labelledby="flyoutActionAddTitle" size="m">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center">
|
||||
{actionTypeModel && actionTypeModel.iconClass ? (
|
||||
{!!actionTypeModel && actionTypeModel.iconClass ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={actionTypeModel.iconClass} size="xl" />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem>
|
||||
{actionTypeModel && actionType ? (
|
||||
{!!actionTypeModel && actionType ? (
|
||||
<>
|
||||
<EuiTitle size="s">
|
||||
<h3 id="flyoutTitle">
|
||||
|
@ -280,7 +302,17 @@ const ConnectorAddFlyout: React.FunctionComponent<ConnectorAddFlyoutProps> = ({
|
|||
)
|
||||
}
|
||||
>
|
||||
{currentForm}
|
||||
<>
|
||||
{currentForm}
|
||||
{isLoading ? (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<CenterJustifiedSpinner size="l" />{' '}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
|
@ -314,7 +346,7 @@ const ConnectorAddFlyout: React.FunctionComponent<ConnectorAddFlyoutProps> = ({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
{canSave && actionTypeModel && actionType ? saveButton : null}
|
||||
{canSave && !!actionTypeModel && actionType ? saveButton : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -39,12 +39,12 @@ describe('connector_add_modal', () => {
|
|||
id: 'my-action-type',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useReducer, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiModal,
|
||||
|
@ -19,6 +19,7 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiFlexGroup,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionConnectorForm, getConnectorErrors } from './action_connector_form';
|
||||
|
@ -31,9 +32,11 @@ import {
|
|||
ActionConnector,
|
||||
ActionTypeRegistryContract,
|
||||
UserConfiguredActionConnector,
|
||||
IErrorObject,
|
||||
} from '../../../types';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { getConnectorWithInvalidatedFields } from '../../lib/value_validators';
|
||||
import { CenterJustifiedSpinner } from '../../components/center_justified_spinner';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
type ConnectorAddModalProps = {
|
||||
|
@ -56,7 +59,7 @@ const ConnectorAddModal = ({
|
|||
notifications: { toasts },
|
||||
application: { capabilities },
|
||||
} = useKibana().services;
|
||||
let hasErrors = false;
|
||||
const [hasErrors, setHasErrors] = useState<boolean>(true);
|
||||
const initialConnector: InitialConnector<
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>
|
||||
|
@ -69,6 +72,7 @@ const ConnectorAddModal = ({
|
|||
[actionType.id]
|
||||
);
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const canSave = hasSaveActionsCapability(capabilities);
|
||||
|
||||
const reducer: ConnectorReducer<
|
||||
|
@ -81,6 +85,34 @@ const ConnectorAddModal = ({
|
|||
Record<string, unknown>
|
||||
>,
|
||||
});
|
||||
const [errors, setErrors] = useState<{
|
||||
configErrors: IErrorObject;
|
||||
connectorBaseErrors: IErrorObject;
|
||||
connectorErrors: IErrorObject;
|
||||
secretsErrors: IErrorObject;
|
||||
}>({
|
||||
configErrors: {},
|
||||
connectorBaseErrors: {},
|
||||
connectorErrors: {},
|
||||
secretsErrors: {},
|
||||
});
|
||||
|
||||
const actionTypeModel = actionTypeRegistry.get(actionType.id);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
const res = await getConnectorErrors(connector, actionTypeModel);
|
||||
setHasErrors(
|
||||
!!Object.keys(res.connectorErrors).find(
|
||||
(errorKey) => (res.connectorErrors as IErrorObject)[errorKey].length >= 1
|
||||
)
|
||||
);
|
||||
setIsLoading(false);
|
||||
setErrors({ ...res });
|
||||
})();
|
||||
}, [connector, actionTypeModel]);
|
||||
|
||||
const setConnector = (value: any) => {
|
||||
dispatch({ command: { type: 'setConnector' }, payload: { key: 'connector', value } });
|
||||
};
|
||||
|
@ -97,15 +129,6 @@ const ConnectorAddModal = ({
|
|||
onClose();
|
||||
}, [initialConnector, onClose]);
|
||||
|
||||
const actionTypeModel = actionTypeRegistry.get(actionType.id);
|
||||
const { configErrors, connectorBaseErrors, connectorErrors, secretsErrors } = getConnectorErrors(
|
||||
connector,
|
||||
actionTypeModel
|
||||
);
|
||||
hasErrors = !!Object.keys(connectorErrors).find(
|
||||
(errorKey) => connectorErrors[errorKey].length >= 1
|
||||
);
|
||||
|
||||
const onActionConnectorSave = async (): Promise<ActionConnector | undefined> =>
|
||||
await createActionConnector({ http, connector })
|
||||
.then((savedConnector) => {
|
||||
|
@ -157,15 +180,25 @@ const ConnectorAddModal = ({
|
|||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<ActionConnectorForm
|
||||
connector={connector}
|
||||
actionTypeName={actionType.name}
|
||||
dispatch={dispatch}
|
||||
serverError={serverError}
|
||||
errors={connectorErrors}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
consumer={consumer}
|
||||
/>
|
||||
<>
|
||||
<ActionConnectorForm
|
||||
connector={connector}
|
||||
actionTypeName={actionType.name}
|
||||
dispatch={dispatch}
|
||||
serverError={serverError}
|
||||
errors={errors.connectorErrors}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
consumer={consumer}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<CenterJustifiedSpinner size="l" />{' '}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={closeModal}>
|
||||
|
@ -189,9 +222,9 @@ const ConnectorAddModal = ({
|
|||
setConnector(
|
||||
getConnectorWithInvalidatedFields(
|
||||
connector,
|
||||
configErrors,
|
||||
secretsErrors,
|
||||
connectorBaseErrors
|
||||
errors.configErrors,
|
||||
errors.secretsErrors,
|
||||
errors.connectorBaseErrors
|
||||
)
|
||||
);
|
||||
return;
|
||||
|
|
|
@ -51,12 +51,12 @@ describe('connector_edit_flyout', () => {
|
|||
id: 'test-action-type-id',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
});
|
||||
|
@ -95,12 +95,12 @@ describe('connector_edit_flyout', () => {
|
|||
id: 'test-action-type-id',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useReducer, useState } from 'react';
|
||||
import React, { useCallback, useReducer, useState, useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiTitle,
|
||||
|
@ -23,6 +23,7 @@ import {
|
|||
EuiLink,
|
||||
EuiTabs,
|
||||
EuiTab,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Option, none, some } from 'fp-ts/lib/Option';
|
||||
|
@ -31,6 +32,7 @@ import { TestConnectorForm } from './test_connector_form';
|
|||
import {
|
||||
ActionConnector,
|
||||
ConnectorEditFlyoutProps,
|
||||
IErrorObject,
|
||||
EditConectorTabs,
|
||||
UserConfiguredActionConnector,
|
||||
} from '../../../types';
|
||||
|
@ -44,6 +46,7 @@ import {
|
|||
import './connector_edit_flyout.scss';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { getConnectorWithInvalidatedFields } from '../../lib/value_validators';
|
||||
import { CenterJustifiedSpinner } from '../../components/center_justified_spinner';
|
||||
|
||||
const ConnectorEditFlyout = ({
|
||||
initialConnector,
|
||||
|
@ -53,12 +56,14 @@ const ConnectorEditFlyout = ({
|
|||
consumer,
|
||||
actionTypeRegistry,
|
||||
}: ConnectorEditFlyoutProps) => {
|
||||
const [hasErrors, setHasErrors] = useState<boolean>(true);
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
docLinks,
|
||||
application: { capabilities },
|
||||
} = useKibana().services;
|
||||
|
||||
const getConnectorWithoutSecrets = () => ({
|
||||
...(initialConnector as UserConfiguredActionConnector<
|
||||
Record<string, unknown>,
|
||||
|
@ -75,6 +80,35 @@ const ConnectorEditFlyout = ({
|
|||
const [{ connector }, dispatch] = useReducer(reducer, {
|
||||
connector: getConnectorWithoutSecrets(),
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [errors, setErrors] = useState<{
|
||||
configErrors: IErrorObject;
|
||||
connectorBaseErrors: IErrorObject;
|
||||
connectorErrors: IErrorObject;
|
||||
secretsErrors: IErrorObject;
|
||||
}>({
|
||||
configErrors: {},
|
||||
connectorBaseErrors: {},
|
||||
connectorErrors: {},
|
||||
secretsErrors: {},
|
||||
});
|
||||
|
||||
const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
const res = await getConnectorErrors(connector, actionTypeModel);
|
||||
setHasErrors(
|
||||
!!Object.keys(res.connectorErrors).find(
|
||||
(errorKey) => (res.connectorErrors as IErrorObject)[errorKey].length >= 1
|
||||
)
|
||||
);
|
||||
setIsLoading(false);
|
||||
setErrors({ ...res });
|
||||
})();
|
||||
}, [connector, actionTypeModel]);
|
||||
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [selectedTab, setTab] = useState<EditConectorTabs>(tab);
|
||||
|
||||
|
@ -113,25 +147,6 @@ const ConnectorEditFlyout = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onClose]);
|
||||
|
||||
const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId);
|
||||
const {
|
||||
configErrors,
|
||||
connectorBaseErrors,
|
||||
connectorErrors,
|
||||
secretsErrors,
|
||||
} = !connector.isPreconfigured
|
||||
? getConnectorErrors(connector, actionTypeModel)
|
||||
: {
|
||||
configErrors: {},
|
||||
connectorBaseErrors: {},
|
||||
connectorErrors: {},
|
||||
secretsErrors: {},
|
||||
};
|
||||
|
||||
const hasErrors = !!Object.keys(connectorErrors).find(
|
||||
(errorKey) => connectorErrors[errorKey].length >= 1
|
||||
);
|
||||
|
||||
const onActionConnectorSave = async (): Promise<ActionConnector | undefined> =>
|
||||
await updateActionConnector({ http, connector, id: connector.id })
|
||||
.then((savedConnector) => {
|
||||
|
@ -227,9 +242,9 @@ const ConnectorEditFlyout = ({
|
|||
setConnector(
|
||||
getConnectorWithInvalidatedFields(
|
||||
connector,
|
||||
configErrors,
|
||||
secretsErrors,
|
||||
connectorBaseErrors
|
||||
errors.configErrors,
|
||||
errors.secretsErrors,
|
||||
errors.connectorBaseErrors
|
||||
)
|
||||
);
|
||||
return;
|
||||
|
@ -286,19 +301,29 @@ const ConnectorEditFlyout = ({
|
|||
<EuiFlyoutBody>
|
||||
{selectedTab === EditConectorTabs.Configuration ? (
|
||||
!connector.isPreconfigured ? (
|
||||
<ActionConnectorForm
|
||||
connector={connector}
|
||||
errors={connectorErrors}
|
||||
dispatch={(changes) => {
|
||||
setHasChanges(true);
|
||||
// if the user changes the connector, "forget" the last execution
|
||||
// so the user comes back to a clean form ready to run a fresh test
|
||||
setTestExecutionResult(none);
|
||||
dispatch(changes);
|
||||
}}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
consumer={consumer}
|
||||
/>
|
||||
<>
|
||||
<ActionConnectorForm
|
||||
connector={connector}
|
||||
errors={errors.connectorErrors}
|
||||
dispatch={(changes) => {
|
||||
setHasChanges(true);
|
||||
// if the user changes the connector, "forget" the last execution
|
||||
// so the user comes back to a clean form ready to run a fresh test
|
||||
setTestExecutionResult(none);
|
||||
dispatch(changes);
|
||||
}}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
consumer={consumer}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<CenterJustifiedSpinner size="l" />{' '}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EuiText>
|
||||
|
|
|
@ -53,12 +53,12 @@ const actionType = {
|
|||
id: 'my-action-type',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Suspense } from 'react';
|
||||
import React, { Suspense, useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -46,11 +46,18 @@ export const TestConnectorForm = ({
|
|||
isExecutingAction,
|
||||
actionTypeRegistry,
|
||||
}: ConnectorAddFlyoutProps) => {
|
||||
const [actionErrors, setActionErrors] = useState<IErrorObject>({});
|
||||
const [hasErrors, setHasErrors] = useState<boolean>(false);
|
||||
const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId);
|
||||
const ParamsFieldsComponent = actionTypeModel.actionParamsFields;
|
||||
|
||||
const actionErrors = actionTypeModel?.validateParams(actionParams).errors as IErrorObject;
|
||||
const hasErrors = !!Object.values(actionErrors).find((errors) => errors.length > 0);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = (await actionTypeModel?.validateParams(actionParams)).errors as IErrorObject;
|
||||
setActionErrors({ ...res });
|
||||
setHasErrors(!!Object.values(res).find((errors) => errors.length > 0));
|
||||
})();
|
||||
}, [actionTypeModel, actionParams]);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
|
|
|
@ -162,12 +162,12 @@ describe('actions_connectors_list component with items', () => {
|
|||
id: 'test',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
|
|
|
@ -135,12 +135,12 @@ describe('alert_add', () => {
|
|||
id: 'my-action-type',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
});
|
||||
|
|
|
@ -15,9 +15,10 @@ import {
|
|||
AlertTypeParams,
|
||||
AlertUpdates,
|
||||
AlertFlyoutCloseReason,
|
||||
IErrorObject,
|
||||
AlertAddProps,
|
||||
} from '../../../types';
|
||||
import { AlertForm, getAlertErrors, isValidAlert } from './alert_form';
|
||||
import { AlertForm, getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_form';
|
||||
import { alertReducer, InitialAlert, InitialAlertReducer } from './alert_reducer';
|
||||
import { createAlert } from '../../lib/alert_api';
|
||||
import { HealthCheck } from '../../components/health_check';
|
||||
|
@ -102,6 +103,18 @@ const AlertAdd = ({
|
|||
}
|
||||
}, [alert.params, initialAlertParams, setInitialAlertParams]);
|
||||
|
||||
const [alertActionsErrors, setAlertActionsErrors] = useState<IErrorObject[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
const res = await getAlertActionErrors(alert as Alert, actionTypeRegistry);
|
||||
setIsLoading(false);
|
||||
setAlertActionsErrors([...res]);
|
||||
})();
|
||||
}, [alert, actionTypeRegistry]);
|
||||
|
||||
const checkForChangesAndCloseFlyout = () => {
|
||||
if (
|
||||
hasAlertChanged(alert, initialAlert, false) ||
|
||||
|
@ -125,9 +138,8 @@ const AlertAdd = ({
|
|||
};
|
||||
|
||||
const alertType = alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null;
|
||||
const { alertActionsErrors, alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors(
|
||||
const { alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors(
|
||||
alert as Alert,
|
||||
actionTypeRegistry,
|
||||
alertType
|
||||
);
|
||||
|
||||
|
@ -195,9 +207,10 @@ const AlertAdd = ({
|
|||
</EuiFlyoutBody>
|
||||
<AlertAddFooter
|
||||
isSaving={isSaving}
|
||||
isFormLoading={isLoading}
|
||||
onSave={async () => {
|
||||
setIsSaving(true);
|
||||
if (!isValidAlert(alert, alertErrors, alertActionsErrors)) {
|
||||
if (isLoading || !isValidAlert(alert, alertErrors, alertActionsErrors)) {
|
||||
setAlert(
|
||||
getAlertWithInvalidatedFields(
|
||||
alert as Alert,
|
||||
|
|
|
@ -13,17 +13,25 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiLoadingSpinner,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useHealthContext } from '../../context/health_context';
|
||||
|
||||
interface AlertAddFooterProps {
|
||||
isSaving: boolean;
|
||||
isFormLoading: boolean;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const AlertAddFooter = ({ isSaving, onSave, onCancel }: AlertAddFooterProps) => {
|
||||
export const AlertAddFooter = ({
|
||||
isSaving,
|
||||
onSave,
|
||||
onCancel,
|
||||
isFormLoading,
|
||||
}: AlertAddFooterProps) => {
|
||||
const { loadingHealthCheck } = useHealthContext();
|
||||
|
||||
return (
|
||||
|
@ -36,6 +44,14 @@ export const AlertAddFooter = ({ isSaving, onSave, onCancel }: AlertAddFooterPro
|
|||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
{isFormLoading ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLoadingSpinner size="l" />
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
|
|
|
@ -106,12 +106,12 @@ describe('alert_edit', () => {
|
|||
id: 'my-action-type',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useReducer, useState } from 'react';
|
||||
import React, { useReducer, useState, useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiTitle,
|
||||
|
@ -20,11 +20,12 @@ import {
|
|||
EuiPortal,
|
||||
EuiCallOut,
|
||||
EuiSpacer,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Alert, AlertEditProps, AlertFlyoutCloseReason } from '../../../types';
|
||||
import { AlertForm, getAlertErrors, isValidAlert } from './alert_form';
|
||||
import { Alert, AlertFlyoutCloseReason, AlertEditProps, IErrorObject } from '../../../types';
|
||||
import { AlertForm, getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_form';
|
||||
import { alertReducer, ConcreteAlertReducer } from './alert_reducer';
|
||||
import { updateAlert } from '../../lib/alert_api';
|
||||
import { HealthCheck } from '../../components/health_check';
|
||||
|
@ -53,6 +54,8 @@ export const AlertEdit = ({
|
|||
false
|
||||
);
|
||||
const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState<boolean>(false);
|
||||
const [alertActionsErrors, setAlertActionsErrors] = useState<IErrorObject[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
http,
|
||||
|
@ -64,9 +67,17 @@ export const AlertEdit = ({
|
|||
|
||||
const alertType = alertTypeRegistry.get(alert.alertTypeId);
|
||||
|
||||
const { alertActionsErrors, alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors(
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
const res = await getAlertActionErrors(alert as Alert, actionTypeRegistry);
|
||||
setAlertActionsErrors([...res]);
|
||||
setIsLoading(false);
|
||||
})();
|
||||
}, [alert, actionTypeRegistry]);
|
||||
|
||||
const { alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors(
|
||||
alert as Alert,
|
||||
actionTypeRegistry,
|
||||
alertType
|
||||
);
|
||||
|
||||
|
@ -80,7 +91,11 @@ export const AlertEdit = ({
|
|||
|
||||
async function onSaveAlert(): Promise<Alert | undefined> {
|
||||
try {
|
||||
if (isValidAlert(alert, alertErrors, alertActionsErrors) && !hasActionsWithBrokenConnector) {
|
||||
if (
|
||||
!isLoading &&
|
||||
isValidAlert(alert, alertErrors, alertActionsErrors) &&
|
||||
!hasActionsWithBrokenConnector
|
||||
) {
|
||||
const newAlert = await updateAlert({ http, alert, id: alert.id });
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText', {
|
||||
|
@ -177,6 +192,14 @@ export const AlertEdit = ({
|
|||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
{isLoading ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLoadingSpinner size="l" />
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
|
|
|
@ -47,19 +47,19 @@ describe('alert_form', () => {
|
|||
id: 'my-action-type',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
secrets: {
|
||||
errors: {},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
});
|
||||
|
|
|
@ -121,11 +121,7 @@ export function validateBaseProperties(alertObject: InitialAlert): ValidationRes
|
|||
return validationResult;
|
||||
}
|
||||
|
||||
export function getAlertErrors(
|
||||
alert: Alert,
|
||||
actionTypeRegistry: ActionTypeRegistryContract,
|
||||
alertTypeModel: AlertTypeModel | null
|
||||
) {
|
||||
export function getAlertErrors(alert: Alert, alertTypeModel: AlertTypeModel | null) {
|
||||
const alertParamsErrors: IErrorObject = alertTypeModel
|
||||
? alertTypeModel.validate(alert.params).errors
|
||||
: [];
|
||||
|
@ -135,18 +131,26 @@ export function getAlertErrors(
|
|||
...alertBaseErrors,
|
||||
} as IErrorObject;
|
||||
|
||||
const alertActionsErrors = alert.actions.map((alertAction: AlertAction) => {
|
||||
return actionTypeRegistry.get(alertAction.actionTypeId)?.validateParams(alertAction.params)
|
||||
.errors;
|
||||
});
|
||||
return {
|
||||
alertParamsErrors,
|
||||
alertBaseErrors,
|
||||
alertActionsErrors,
|
||||
alertErrors,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAlertActionErrors(
|
||||
alert: Alert,
|
||||
actionTypeRegistry: ActionTypeRegistryContract
|
||||
): Promise<IErrorObject[]> {
|
||||
return await Promise.all(
|
||||
alert.actions.map(
|
||||
async (alertAction: AlertAction) =>
|
||||
(await actionTypeRegistry.get(alertAction.actionTypeId)?.validateParams(alertAction.params))
|
||||
.errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const hasObjectErrors: (errors: IErrorObject) => boolean = (errors) =>
|
||||
!!Object.values(errors).find((errorList) => {
|
||||
if (isObject(errorList)) return hasObjectErrors(errorList as IErrorObject);
|
||||
|
|
|
@ -42,12 +42,12 @@ const getTestActionType = (
|
|||
id: id || 'my-action-type',
|
||||
iconClass: iconClass || 'test',
|
||||
selectMessage: selectedMessage || 'test',
|
||||
validateConnector: (): ConnectorValidationResult<unknown, unknown> => {
|
||||
return {};
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): GenericValidationResult<unknown> => {
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
});
|
||||
|
|
|
@ -109,10 +109,10 @@ export interface ActionTypeModel<ActionConfig = any, ActionSecrets = any, Action
|
|||
actionTypeTitle?: string;
|
||||
validateConnector: (
|
||||
connector: UserConfiguredActionConnector<ActionConfig, ActionSecrets>
|
||||
) => ConnectorValidationResult<Partial<ActionConfig>, Partial<ActionSecrets>>;
|
||||
) => Promise<ConnectorValidationResult<Partial<ActionConfig>, Partial<ActionSecrets>>>;
|
||||
validateParams: (
|
||||
actionParams: ActionParams
|
||||
) => GenericValidationResult<Partial<ActionParams> | unknown>;
|
||||
) => Promise<GenericValidationResult<Partial<ActionParams> | unknown>>;
|
||||
actionConnectorFields: React.LazyExoticComponent<
|
||||
ComponentType<
|
||||
ActionConnectorFieldsProps<UserConfiguredActionConnector<ActionConfig, ActionSecrets>>
|
||||
|
|
Loading…
Reference in a new issue