mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ResponseOps] Sub actions framework: Create connector forms (#131718)
* Create useSubAction hook * Create SimpleConnectorForm * Init create connector form * Show action types * Add banner to flyout * Return back to action types * Create useCreateConnector * Use useCreateConnector * POC: IBM resilient * Refinements * Show flyout header * Support password text field * Validate url * Change connector types * Jira * Teams * Pagerduty * ITOM * Server log * Slack * ES index draft * ES index fix * Password field * SN * Fix ES index * Webhoo draft * Webhook fixes * Index fixes * Email connector * Fix disabled * Xmatters connector * Swimlane connector * Swimlane fixes * Improve form * Presubmit validator * SN connector * Hidden field * Rename * fixes * Edit flyout * Test connector * Improve structure * Improve edit flyout structure * Improvements * Remove old files and fix tabs * Modal * Common label for encrypted fields * First tests * More tests * Fix types * Fix i18n * Fixes * Dynamic encrypted values callout * Add tests * Add flyout tests * Fix cypress test * Add README * Fix tests * Webhook serializer & deserializer * Validate email * Fix itom * Fix validators * Fix types * Add more tests * Add more tests * Add more tests * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Pass connector's services with context * Clean up translations * Pass readOnly to all fields * Fix bug with connectors provider * PR feedback * Fix i18n * Fix types * Fix subaction hook * Add hook tests * Fix bug with port Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ce980d4507
commit
c7262544b5
170 changed files with 9897 additions and 9902 deletions
|
@ -553,7 +553,7 @@ describe('ConfigureCases', () => {
|
|||
expect(wrapper.find('[data-test-subj="add-connector-flyout"]').exists()).toBe(true);
|
||||
expect(getAddConnectorFlyoutMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
actionTypes: [
|
||||
supportedActionTypes: [
|
||||
expect.objectContaining({
|
||||
id: '.servicenow',
|
||||
}),
|
||||
|
@ -575,9 +575,6 @@ describe('ConfigureCases', () => {
|
|||
test('it show the edit flyout when pressing the update connector button', async () => {
|
||||
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
|
||||
id: '.resilient',
|
||||
validateConnector: () => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: () => {
|
||||
const validationResult = { errors: {} };
|
||||
return Promise.resolve(validationResult);
|
||||
|
@ -603,7 +600,7 @@ describe('ConfigureCases', () => {
|
|||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="edit-connector-flyout"]').exists()).toBe(true);
|
||||
expect(getEditConnectorFlyoutMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ initialConnector: connectors[1] })
|
||||
expect.objectContaining({ connector: connectors[1] })
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ export const ConfigureCases: React.FC = React.memo(() => {
|
|||
[actionTypes]
|
||||
);
|
||||
|
||||
const onConnectorUpdate = useCallback(async () => {
|
||||
const onConnectorUpdated = useCallback(async () => {
|
||||
refetchConnectors();
|
||||
refetchActionTypes();
|
||||
refetchCaseConfigure();
|
||||
|
@ -168,10 +168,9 @@ export const ConfigureCases: React.FC = React.memo(() => {
|
|||
() =>
|
||||
addFlyoutVisible
|
||||
? triggersActionsUi.getAddConnectorFlyout({
|
||||
consumer: 'case',
|
||||
onClose: onCloseAddFlyout,
|
||||
actionTypes: supportedActionTypes,
|
||||
reloadConnectors: onConnectorUpdate,
|
||||
supportedActionTypes,
|
||||
onConnectorCreated: onConnectorUpdated,
|
||||
})
|
||||
: null,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@ -182,10 +181,9 @@ export const ConfigureCases: React.FC = React.memo(() => {
|
|||
() =>
|
||||
editedConnectorItem && editFlyoutVisible
|
||||
? triggersActionsUi.getEditConnectorFlyout({
|
||||
initialConnector: editedConnectorItem,
|
||||
consumer: 'case',
|
||||
connector: editedConnectorItem,
|
||||
onClose: onCloseEditFlyout,
|
||||
reloadConnectors: onConnectorUpdate,
|
||||
onConnectorUpdated,
|
||||
})
|
||||
: null,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
|
@ -19,7 +19,6 @@ import { ruleTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/app
|
|||
import {
|
||||
ValidationResult,
|
||||
Rule,
|
||||
ConnectorValidationResult,
|
||||
GenericValidationResult,
|
||||
RuleTypeModel,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
|
@ -91,9 +90,6 @@ describe('alert_form', () => {
|
|||
id: 'alert-action-type',
|
||||
iconClass: '',
|
||||
selectMessage: '',
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
validateParams: (): Promise<GenericValidationResult<unknown>> => {
|
||||
const validationResult = { errors: {} };
|
||||
return Promise.resolve(validationResult);
|
||||
|
|
|
@ -18,7 +18,7 @@ export const CONNECTORS_DROPDOWN = '[data-test-subj="dropdown-connectors"]';
|
|||
|
||||
export const PASSWORD = '[data-test-subj="connector-servicenow-password-form-input"]';
|
||||
|
||||
export const SAVE_BTN = '[data-test-subj="saveNewActionButton"]';
|
||||
export const SAVE_BTN = '[data-test-subj="create-connector-flyout-save-btn"]';
|
||||
|
||||
export const SERVICE_NOW_CONNECTOR_CARD = '[data-test-subj=".servicenow-card"]';
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import ReactMarkdown from 'react-markdown';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
ActionForm,
|
||||
ActionType,
|
||||
loadActionTypes,
|
||||
ActionVariables,
|
||||
|
@ -84,7 +83,7 @@ export const RuleActionsField: React.FC<Props> = ({
|
|||
const { isSubmitted, isSubmitting, isValid } = form;
|
||||
const {
|
||||
http,
|
||||
triggersActionsUi: { actionTypeRegistry },
|
||||
triggersActionsUi: { getActionForm },
|
||||
} = useKibana().services;
|
||||
|
||||
const actions: RuleAction[] = useMemo(
|
||||
|
@ -135,6 +134,29 @@ export const RuleActionsField: React.FC<Props> = ({
|
|||
[field.setValue, actions]
|
||||
);
|
||||
|
||||
const actionForm = useMemo(
|
||||
() =>
|
||||
getActionForm({
|
||||
actions,
|
||||
messageVariables,
|
||||
defaultActionGroupId: DEFAULT_ACTION_GROUP_ID,
|
||||
setActionIdByIndex,
|
||||
setActions: setAlertActionsProperty,
|
||||
setActionParamsProperty,
|
||||
actionTypes: supportedActionTypes,
|
||||
defaultActionMessage: DEFAULT_ACTION_MESSAGE,
|
||||
}),
|
||||
[
|
||||
actions,
|
||||
getActionForm,
|
||||
messageVariables,
|
||||
setActionIdByIndex,
|
||||
setActionParamsProperty,
|
||||
setAlertActionsProperty,
|
||||
supportedActionTypes,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const actionTypes = convertArrayToCamelCase(await loadActionTypes({ http })) as ActionType[];
|
||||
|
@ -168,17 +190,7 @@ export const RuleActionsField: React.FC<Props> = ({
|
|||
<EuiSpacer />
|
||||
</>
|
||||
) : null}
|
||||
<ActionForm
|
||||
actions={actions}
|
||||
messageVariables={messageVariables}
|
||||
defaultActionGroupId={DEFAULT_ACTION_GROUP_ID}
|
||||
setActionIdByIndex={setActionIdByIndex}
|
||||
setActions={setAlertActionsProperty}
|
||||
setActionParamsProperty={setActionParamsProperty}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
actionTypes={supportedActionTypes}
|
||||
defaultActionMessage={DEFAULT_ACTION_MESSAGE}
|
||||
/>
|
||||
{actionForm}
|
||||
</ContainerActions>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -62,7 +62,7 @@ journey('DefaultEmailSettings', async ({ page, params }) => {
|
|||
await page.fill(byTestId('emailHostInput'), 'test');
|
||||
await page.fill(byTestId('emailPortInput'), '1025');
|
||||
await page.click('text=Require authentication for this server');
|
||||
await page.click(byTestId('saveNewActionButton'));
|
||||
await page.click(byTestId('create-connector-flyout-save-btn'));
|
||||
});
|
||||
|
||||
step('Select email connector', async () => {
|
||||
|
|
|
@ -56,13 +56,12 @@ export const AddConnectorFlyout = ({ focusInput, isDisabled }: Props) => {
|
|||
const ConnectorAddFlyout = useMemo(
|
||||
() =>
|
||||
getAddConnectorFlyout({
|
||||
consumer: 'uptime',
|
||||
onClose: () => {
|
||||
dispatch(getConnectorsAction.get());
|
||||
setAddFlyoutVisibility(false);
|
||||
focusInput();
|
||||
},
|
||||
actionTypes: (actionTypes ?? []).filter((actionType) =>
|
||||
supportedActionTypes: (actionTypes ?? []).filter((actionType) =>
|
||||
ALLOWED_ACTION_TYPES.includes(actionType.id as ActionTypeId)
|
||||
),
|
||||
}),
|
||||
|
|
|
@ -28448,7 +28448,6 @@
|
|||
"xpack.triggersActionsUI.actionVariables.ruleTagsLabel": "Balises de la règle.",
|
||||
"xpack.triggersActionsUI.actionVariables.ruleTypeLabel": "Type de règle.",
|
||||
"xpack.triggersActionsUI.appName": "Règles et connecteurs",
|
||||
"xpack.triggersActionsUI.cases.configureCases.mappingFieldSummary": "Résumé",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "Ce connecteur est désactivé par la configuration de Kibana.",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "Ce connecteur requiert une licence {minimumLicenseRequired}.",
|
||||
"xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage": "Ce type de règle requiert une licence {minimumLicenseRequired}.",
|
||||
|
@ -28486,29 +28485,22 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.gmailServerTypeLabel": "Gmail",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.otherServerTypeLabel": "Autre",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.outlookServerTypeLabel": "Outlook",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel": "L'identifiant client secret est chiffré. Veuillez entrer à nouveau une valeur pour ce champ.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel": "Le nom d'utilisateur et le mot de passe sont chiffrés. Veuillez entrer à nouveau les valeurs de ces champs.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "Envoyez un e-mail à partir de votre serveur.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideSuffix": "L'index d'historique d'alertes doit contenir un suffixe valide.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideValue": "L'index d'historique d'alertes doit commencer par \"{alertHistoryPrefix}\".",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "L'expéditeur n'est pas une adresse e-mail valide.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText": "Le mot de passe est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthUserNameText": "Le nom d'utilisateur est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientIdText": "L'ID client est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText": "L'identifiant client secret est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson": "Le document est requis et doit être un objet JSON valide.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "Aucune entrée À, Cc ou Cci. Au moins une entrée est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText": "L'expéditeur est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText": "L'hôte est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText": "Le message est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText": "Le mot de passe est requis lorsque le nom d'utilisateur est utilisé.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText": "Le port est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText": "Le message est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServiceText": "Le service est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText": "Le message est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText": "Le sujet est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredTenantIdText": "L'ID locataire est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText": "Le nom d'utilisateur est requis lorsque le mot de passe est utilisé.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText": "Le corps est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle": "Données d'index",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "Choisir…",
|
||||
|
@ -28517,7 +28509,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.definedateFieldTooltip": "Définissez ce champ temporel sur l'heure à laquelle le document a été indexé.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.defineTimeFieldLabel": "Définissez l'heure pour chaque document",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel": "Document à indexer",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText": "L'index est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.executionTimeFieldLabel": "Champ temporel",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.howToBroadenSearchQueryDescription": "Utilisez le caractère * pour élargir votre recherche.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indexDocumentHelpLabel": "Exemple de document d'index.",
|
||||
|
@ -28533,25 +28524,14 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle": "Jira",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel": "Token d'API",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel": "URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.authenticationLabel": "Authentification",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel": "Commentaires supplémentaires",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.descriptionTextAreaFieldLabel": "Description",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel": "Adresse e-mail",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.impactSelectFieldLabel": "Étiquettes",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField": "L'URL n'est pas valide.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.labelsSpacesErrorMessage": "Les étiquettes ne peuvent pas contenir d'espaces.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments": "Commentaires",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription": "Description",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.parentIssueSearchLabel": "Problème lié au parent",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey": "Clé de projet",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.reenterValuesLabel": "Les informations d'authentification sont chiffrées. Veuillez entrer à nouveau les valeurs de ces champs.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField": "Le token d'API est requis",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField": "L'URL est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "La description est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "L'adresse e-mail est requise",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "La clé de projet est requise",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredSummaryTextField": "Le résumé est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requireHttpsApiUrlTextField": "L'URL doit commencer par https://.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "Taper pour rechercher",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder": "Taper pour rechercher",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesLoading": "Chargement...",
|
||||
|
@ -28563,7 +28543,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssuesMessage": "Impossible d'obtenir les problèmes",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage": "Impossible d'obtenir les types d'erreurs",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.urgencySelectFieldLabel": "Type d'erreur",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.missingSecretsValuesLabel": "Les informations sensibles ne sont pas importées. Veuillez entrer {encryptedFieldsLength, plural, one {la valeur} other {les valeurs}} pour {encryptedFieldsLength, plural, one {le champ suivant} other {les champs suivants}}.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "Envoyer à PagerDuty",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "URL de l'API (facultative)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "Classe (facultative)",
|
||||
|
@ -28579,7 +28558,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectResolveOptionLabel": "Résoudre",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectTriggerOptionLabel": "Déclencher",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.groupTextFieldLabel": "Regrouper (facultatif)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.reenterValueLabel": "Cette clé est chiffrée. Veuillez entrer à nouveau une valeur pour ce champ.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyNameHelpLabel": "Configurer un compte PagerDuty",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel": "Clé d'intégration",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText": "Envoyez un événement dans PagerDuty.",
|
||||
|
@ -28591,29 +28569,15 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.sourceTextFieldLabel": "Source (facultative)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "Résumé",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "Horodatage (facultatif)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.rememberValueLabel": "Mémorisez {encryptedFieldsLength, plural, one {cette valeur} other {ces valeurs}}. Vous devrez {encryptedFieldsLength, plural, one {l'entrer} other {les entrer}} à nouveau chaque fois que vous modifierez le connecteur.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle": "Résilient",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKey": "Clé d'API",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId": "ID",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret": "Secret",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel": "URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel": "Commentaires supplémentaires",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.descriptionTextAreaFieldLabel": "Description",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField": "L'URL n'est pas valide.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments": "Commentaires",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription": "Description",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription": "Nom",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.nameFieldLabel": "Nom (requis)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId": "ID d'organisation",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.reenterValuesLabel": "L'ID et le secret sont chiffrés. Veuillez entrer à nouveau les valeurs de ces champs.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.rememberValuesLabel": "Mémorisez ces valeurs. Vous devrez les entrer à nouveau chaque fois que vous modifierez le connecteur.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField": "L'ID est requis",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField": "Le secret est requis",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField": "L'URL est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredDescriptionTextField": "La description est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredNameTextField": "Le nom est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField": "L'ID d'organisation est requis",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requireHttpsApiUrlTextField": "L'URL doit commencer par https://.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText": "Créez un incident dans IBM Resilient.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.severity": "Sévérité",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage": "Impossible d'obtenir les types d'incidents",
|
||||
|
@ -28624,7 +28588,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "Message",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "Ajouter un message au log Kibana.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiInfoError": "Statut reçu : {status} lors de la tentative d'obtention d'informations sur l'application",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlHelpText": "Spécifiez l'URL complète.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel": "URL d'instance ServiceNow",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.appInstallationInfo": "{update} {create} ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.applicationRequiredCallout": "Application Elastic ServiceNow non installée",
|
||||
|
@ -28643,20 +28606,14 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.descriptionTextAreaFieldLabel": "Description",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.eventClassTextAreaFieldLabel": "Instance source",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.impactSelectFieldLabel": "Impact",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.install": "installer",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutTitle": "Pour utiliser ce connecteur, installez d'abord l'application Elastic à partir de l'app store ServiceNow.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField": "L'URL n'est pas valide.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.messageKeyTextAreaFieldLabel": "Clé de message",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.metricNameTextAreaFieldLabel": "Nom de l'indicateur",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.nodeTextAreaFieldLabel": "Nœud",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel": "Mot de passe",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.prioritySelectFieldLabel": "Priorité",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel": "Vous devrez vous authentifier chaque fois que vous modifierez le connecteur.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiUrlTextField": "L'URL est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "Le mot de passe est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredSeverityTextField": "La sévérité est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "Le nom d'utilisateur est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField": "L'URL doit commencer par https://.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.resourceTextAreaFieldLabel": "Ressource",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.setupDevInstance": "configurer une instance de développeur",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severityRequiredSelectFieldLabel": "Sévérité (requise)",
|
||||
|
@ -28693,10 +28650,7 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIRAction.correlationIDHelpLabel": "Identificateur pour les incidents de mise à jour",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "Envoyer vers Slack",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.invalidWebhookUrlText": "L'URL de webhook n'est pas valide.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "L'URL de webhook est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requireHttpsWebhookUrlText": "L'URL de webhook doit commencer par https://.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "Message",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.reenterValueLabel": "Cette URL est chiffrée. Veuillez entrer à nouveau une valeur pour ce champ.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "Envoyez un message à un canal ou à un utilisateur Slack.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlHelpLabel": "Créer une URL de webhook Slack",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel": "URL de webhook",
|
||||
|
@ -28704,7 +28658,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage": "Impossible d'obtenir l'application avec l'ID {id}",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.actionTypeTitle": "Créer l'enregistrement Swimlane",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertIdFieldLabel": "ID de l'alerte",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel": "Source de l'alerte",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenNameHelpLabel": "Fournir un token d'API Swimlane",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenTextFieldLabel": "Token d'API",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel": "URL d'API",
|
||||
|
@ -28718,71 +28671,47 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningDesc": "Ce connecteur ne peut pas être sélectionné, car il ne possède pas les mappings de champs d'alerte requis. Vous pouvez modifier ce connecteur pour ajouter les mappings de champs requis ou sélectionner un connecteur de type Alertes.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle": "Ce connecteur ne possède pas de mappings de champs",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertID": "L'ID d'alerte est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource": "La source de l'alerte est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredApiTokenText": "Un token d'API est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText": "Un ID d'application est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseID": "L'ID de cas est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseName": "Le nom de cas est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredComments": "Les commentaires sont requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredDescription": "La description est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredFieldMappingsText": "Les mappings de champs sont requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredRuleName": "Le nom de règle est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredSeverity": "La sévérité est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.invalidApiUrlTextField": "L'URL n'est pas valide.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingDescriptionTextFieldLabel": "Utilisé pour spécifier les noms de champs dans l'application Swimlane",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingFieldRequired": "Le mapping de champs est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingTitleTextFieldLabel": "Configurer les mappings de champs",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStep": "Suivant",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStepHelpText": "Si les mappings de champs ne sont pas configurés, le type de connecteur Swimlane sera défini sur Tous.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.prevStep": "Précédent",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.reenterValueLabel": "Cette clé est chiffrée. Veuillez entrer à nouveau une valeur pour ce champ.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel": "Mémorisez cette valeur. Vous devrez l'entrer à nouveau chaque fois que vous modifierez le connecteur.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.requiredApiUrlTextField": "L'URL est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.retrieveConfigurationLabel": "Configurer les champs",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.ruleNameFieldLabel": "Nom de règle",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText": "Créer un enregistrement dans Swimlane",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.severityFieldLabel": "Sévérité",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.actionTypeTitle": "Envoyer un message à un canal Microsoft Teams.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText": "L'URL de webhook n'est pas valide.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText": "Le message est requis.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText": "L'URL de webhook est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText": "L'URL de webhook doit commencer par https://.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.messageTextAreaFieldLabel": "Message",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.reenterValueLabel": "Cette URL est chiffrée. Veuillez entrer à nouveau une valeur pour ce champ.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.selectMessageText": "Envoyer un message à un canal Microsoft Teams.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel": "Créer une URL de webhook Microsoft Teams",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel": "URL de webhook",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Données de webhook",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "Ajouter un en-tête",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "Ajouter",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.authenticationLabel": "Authentification",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyCodeEditorAriaLabel": "Éditeur de code",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyFieldLabel": "Corps",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.deleteHeaderButton": "Supprimer",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField": "L'URL n'est pas valide.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText": "L'URL est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.hasAuthSwitchLabel": "Demander une authentification pour ce webhook",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.httpHeadersTitle": "En-têtes utilisés",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.keyTextFieldLabel": "Clé",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.methodTextFieldLabel": "Méthode",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel": "Mot de passe",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.reenterValuesLabel": "Le nom d'utilisateur et le mot de passe sont chiffrés. Veuillez entrer à nouveau les valeurs de ces champs.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText": "Envoyer une requête à un service Web.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.urlTextFieldLabel": "URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel": "Nom d'utilisateur",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.valueTextFieldLabel": "Valeur",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "Ajouter un en-tête HTTP",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.actionTypeTitle": "Données xMatters",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.authenticationLabel": "Authentification",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthLabel": "Authentification de base",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsFieldLabel": "URL d'initiation",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsLabel": "Sélectionnez la méthode d'authentification utilisée pour la configuration du déclencheur xMatters.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.invalidUrlTextField": "L'URL n'est pas valide.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.requiredUrlText": "L'URL est requise.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.initiationUrlHelpText": "Spécifiez l'URL xMatters complète.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.passwordTextFieldLabel": "Mot de passe",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterBasicAuthValuesLabel": "Le nom d'utilisateur et le mot de passe sont chiffrés. Veuillez entrer à nouveau les valeurs de ces champs.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterUrlAuthValuesLabel": "L'URL est chiffrée. Veuillez entrer à nouveau des valeurs pour ce champ.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.selectMessageText": "Déclenchez un workflow xMatters.",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severity": "Sévérité",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severitySelectCriticalOptionLabel": "Critique",
|
||||
|
@ -28855,10 +28784,6 @@
|
|||
"xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerLinkTitle": "Plans d'abonnement",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerMessage": "Mettez à niveau votre licence ou démarrez un essai gratuit de 30 jours pour obtenir un accès immédiat à tous les connecteurs tiers.",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerTitle": "Mettre à niveau votre licence pour accéder à tous les connecteurs",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actionNameLabel": "Nom du connecteur",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningHelpLinkText": "En savoir plus.",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningDescriptionText": "Pour créer ce connecteur, vous devez configurer au moins un compte {connectorType}. {docLink}",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningTitleText": "Type de connecteur non enregistré",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.connectorSettingsLabel": "Paramètres du connecteur",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "Le nom est requis.",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.loadingConnectorSettingsDescription": "Chargement des paramètres du connecteur…",
|
||||
|
@ -28906,25 +28831,14 @@
|
|||
"xpack.triggersActionsUI.sections.actionTypeForm.addNewActionConnectorActionGroup.display": "{actionGroupName} (non pris en charge actuellement)",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.addNewConnectorEmptyButton": "Ajouter un connecteur",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.existingAlertActionTypeEditTitle": "{actionConnectorName}",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText": "Le mot de passe est requis.",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText": "Le nom d'utilisateur est requis.",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderKeyText": "La clé est requise.",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "La valeur est requise.",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "La méthode est requise.",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "Le mot de passe est requis lorsque le nom d'utilisateur est utilisé.",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText": "Le nom d'utilisateur est requis lorsque le mot de passe est utilisé.",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthPasswordText": "Le mot de passe est requis.",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthUserNameText": "Le nom d'utilisateur est requis.",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredPasswordText": "Le mot de passe est requis lorsque le nom d'utilisateur est utilisé.",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredUserText": "Le nom d'utilisateur est requis lorsque le mot de passe est utilisé.",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "Connecteur {actionTypeName}",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "Sélectionner un connecteur",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "Impossible de créer un connecteur.",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "Création de \"{connectorName}\" effectuée",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "Annuler",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "Connecteur {actionTypeName}",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "Enregistrer",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "Création de \"{connectorName}\" effectuée",
|
||||
"xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "Cci",
|
||||
"xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "Cc",
|
||||
"xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "Authentification",
|
||||
|
@ -28962,7 +28876,6 @@
|
|||
"xpack.triggersActionsUI.sections.connectorAddInline.unableToLoadConnectorTitle'": "Impossible de charger le connecteur",
|
||||
"xpack.triggersActionsUI.sections.connectorAddInline.unauthorizedToCreateForEmptyConnectors": "Seuls les utilisateurs autorisés peuvent configurer un connecteur. Contactez votre administrateur.",
|
||||
"xpack.triggersActionsUI.sections.deprecatedTitleMessage": "(déclassé)",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{actionDescription}",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel": "Annuler",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "Ce connecteur est en lecture seule.",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "Modifier un connecteur",
|
||||
|
|
|
@ -28609,7 +28609,6 @@
|
|||
"xpack.triggersActionsUI.actionVariables.ruleTagsLabel": "ルールのタグ。",
|
||||
"xpack.triggersActionsUI.actionVariables.ruleTypeLabel": "ルールのタイプ。",
|
||||
"xpack.triggersActionsUI.appName": "ルールとコネクター",
|
||||
"xpack.triggersActionsUI.cases.configureCases.mappingFieldSummary": "まとめ",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "このコネクターは Kibana の構成で無効になっています。",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "このコネクターには {minimumLicenseRequired} ライセンスが必要です。",
|
||||
"xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage": "このルールタイプには{minimumLicenseRequired}ライセンスが必要です。",
|
||||
|
@ -28647,29 +28646,22 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.gmailServerTypeLabel": "Gmail",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.otherServerTypeLabel": "その他",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.outlookServerTypeLabel": "Outlook",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel": "クライアントシークレットは暗号化されています。このフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel": "ユーザー名とパスワードは暗号化されます。これらのフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "サーバーからメールを送信します。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideSuffix": "アラート履歴インデックスには有効なサフィックスを含める必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideValue": "アラート履歴インデックスの先頭は\"{alertHistoryPrefix}\"でなければなりません。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "送信元は有効なメールアドレスではありません。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText": "パスワードが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthUserNameText": "ユーザー名が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientIdText": "クライアントIDは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText": "クライアントシークレットは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson": "ドキュメントが必要です。有効なJSONオブジェクトにしてください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "To、Cc、または Bcc のエントリーがありません。 1 つ以上のエントリーが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText": "送信元が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText": "ホストが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText": "メッセージが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText": "ユーザー名の使用時にはパスワードが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText": "ポートが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText": "メッセージが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServiceText": "サービスは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText": "メッセージが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText": "件名が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredTenantIdText": "テナントIDは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText": "パスワードの使用時にはユーザー名が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText": "本文が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle": "データをインデックスする",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "選択…",
|
||||
|
@ -28678,7 +28670,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.definedateFieldTooltip": "この時間フィールドをドキュメントにインデックスが作成された時刻に設定します。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.defineTimeFieldLabel": "各ドキュメントの時刻フィールドを定義",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel": "インデックスするドキュメント",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText": "インデックスが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.executionTimeFieldLabel": "時間フィールド",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indexDocumentHelpLabel": "インデックスドキュメントの例。",
|
||||
|
@ -28694,25 +28685,14 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle": "Jira",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel": "APIトークン",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel": "URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.authenticationLabel": "認証",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel": "追加のコメント",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.descriptionTextAreaFieldLabel": "説明",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel": "メールアドレス",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.impactSelectFieldLabel": "ラベル",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField": "URL が無効です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.labelsSpacesErrorMessage": "ラベルにはスペースを使用できません。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments": "コメント",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription": "説明",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.parentIssueSearchLabel": "親問題",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey": "プロジェクトキー",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.reenterValuesLabel": "認証資格情報は暗号化されます。これらのフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField": "APIトークンが必要です",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField": "URL が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "説明が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "メールアドレスが必要です",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "プロジェクトキーが必要です",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredSummaryTextField": "概要が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requireHttpsApiUrlTextField": "URL は https:// から始める必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "入力して検索",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder": "入力して検索",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesLoading": "読み込み中...",
|
||||
|
@ -28724,7 +28704,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssuesMessage": "問題を取得できません",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage": "問題タイプを取得できません",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.urgencySelectFieldLabel": "問題タイプ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.missingSecretsValuesLabel": "機密情報はインポートされません。次のフィールド{encryptedFieldsLength, plural, other {}}の値{encryptedFieldsLength, plural, other {}}入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "PagerDuty に送信",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "API URL(任意)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "クラス(任意)",
|
||||
|
@ -28740,7 +28719,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectResolveOptionLabel": "解決",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectTriggerOptionLabel": "トリガー",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.groupTextFieldLabel": "グループ(任意)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.reenterValueLabel": "このキーは暗号化されています。このフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyNameHelpLabel": "PagerDuty アカウントを構成します",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel": "統合キー",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText": "PagerDuty でイベントを送信します。",
|
||||
|
@ -28752,29 +28730,15 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.sourceTextFieldLabel": "ソース(任意)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "まとめ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "タイムスタンプ(任意)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.rememberValueLabel": "{encryptedFieldsLength, plural, one {この} other {これらの}}値を記憶しておいてください。コネクターを編集するたびに、{encryptedFieldsLength, plural, one {それを} other {それらを}}再入力する必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle": "Resilient",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKey": "API キー",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId": "ID",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret": "シークレット",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel": "URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel": "追加のコメント",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.descriptionTextAreaFieldLabel": "説明",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField": "URL が無効です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments": "コメント",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription": "説明",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription": "名前",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.nameFieldLabel": "名前(必須)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId": "組織 ID",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.reenterValuesLabel": "ID とシークレットは暗号化されます。これらのフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.rememberValuesLabel": "これらの値を覚えておいてください。コネクターを編集するたびに再入力する必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField": "IDが必要です",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField": "シークレットが必要です",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField": "URL が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredDescriptionTextField": "説明が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredNameTextField": "名前が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField": "組織 ID が必要です",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requireHttpsApiUrlTextField": "URL は https:// から始める必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText": "IBM Resilient でインシデントを作成します。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.severity": "深刻度",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage": "インシデントタイプを取得できません",
|
||||
|
@ -28785,7 +28749,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "メッセージ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "Kibana ログにメッセージを追加します。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiInfoError": "アプリケーション情報の取得を試みるときの受信ステータス:{status}",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlHelpText": "完全なURLを含めます。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel": "ServiceNowインスタンスURL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.appInstallationInfo": "{update} {create} ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.applicationRequiredCallout": "Elastic ServiceNowアプリがインストールされていません",
|
||||
|
@ -28804,20 +28767,14 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.descriptionTextAreaFieldLabel": "説明",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.eventClassTextAreaFieldLabel": "ソースインスタンス",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.impactSelectFieldLabel": "インパクト",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.install": "インストール",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutTitle": "このコネクターを使用するには、まずServiceNowアプリストアからElasticアプリをインストールします。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField": "URL が無効です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.messageKeyTextAreaFieldLabel": "メッセージキー",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.metricNameTextAreaFieldLabel": "メトリック名",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.nodeTextAreaFieldLabel": "ノード",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel": "パスワード",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.prioritySelectFieldLabel": "優先度",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel": "コネクターを編集するたびに認証する必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiUrlTextField": "URL が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "パスワードが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredSeverityTextField": "重要度は必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "ユーザー名が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField": "URL は https:// から始める必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.resourceTextAreaFieldLabel": "リソース",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.setupDevInstance": "開発者インスタンスを設定",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severityRequiredSelectFieldLabel": "重要度(必須)",
|
||||
|
@ -28854,10 +28811,7 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIRAction.correlationIDHelpLabel": "インシデントを更新するID",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "Slack に送信",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.invalidWebhookUrlText": "Web フック URL が無効です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "Web フック URL が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requireHttpsWebhookUrlText": "Web フック URL は https:// から始める必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "メッセージ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.reenterValueLabel": "この URL は暗号化されています。このフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "Slack チャネルにメッセージを送信します。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlHelpLabel": "Slack Web フック URL を作成",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel": "Web フック URL",
|
||||
|
@ -28865,7 +28819,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage": "id {id}のアプリケーションフィールドを取得できません",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.actionTypeTitle": "Swimlaneレコードを作成",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertIdFieldLabel": "アラートID",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel": "アラートソース",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenNameHelpLabel": "Swimlane APIトークンを指定",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenTextFieldLabel": "APIトークン",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel": "API Url",
|
||||
|
@ -28879,71 +28832,47 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningDesc": "このコネクターを選択できません。必要なアラートフィールドマッピングがありません。このコネクターを編集して、必要なフィールドマッピングを追加するか、タイプがアラートのコネクターを選択できます。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle": "このコネクターにはフィールドマッピングがありません。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertID": "アラートIDは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource": "アラートソースは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredApiTokenText": "API トークンは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText": "アプリIDは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseID": "ケースIDは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseName": "ケース名は必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredComments": "コメントは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredDescription": "説明が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredFieldMappingsText": "フィールドマッピングは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredRuleName": "ルール名は必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredSeverity": "重要度は必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.invalidApiUrlTextField": "URL が無効です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingDescriptionTextFieldLabel": "Swimlaneアプリケーションでフィールド名を指定するために使用されます",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingFieldRequired": "フィールドマッピングは必須です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingTitleTextFieldLabel": "フィールドマッピングを構成",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStep": "次へ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStepHelpText": "フィールドマッピングが構成されていない場合、Swimlaneコネクタータイプはすべてに設定されます。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.prevStep": "戻る",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.reenterValueLabel": "このキーは暗号化されています。このフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel": "この値を覚えておいてください。コネクターを編集するたびに再入力する必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.requiredApiUrlTextField": "URL が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.retrieveConfigurationLabel": "フィールドを構成",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.ruleNameFieldLabel": "ルール名",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText": "Swimlaneでレコードを作成",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.severityFieldLabel": "深刻度",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.actionTypeTitle": "メッセージを Microsoft Teams チャネルに送信します。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText": "Web フック URL が無効です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText": "メッセージが必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText": "Web フック URL が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText": "Web フック URL は https:// から始める必要があります。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.messageTextAreaFieldLabel": "メッセージ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.reenterValueLabel": "この URL は暗号化されています。このフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.selectMessageText": "メッセージを Microsoft Teams チャネルに送信します。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel": "Microsoft Teams Web フック URL を作成",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel": "Web フック URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Web フックデータ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "ヘッダーを追加",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "追加",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.authenticationLabel": "認証",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyCodeEditorAriaLabel": "コードエディター",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyFieldLabel": "本文",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.deleteHeaderButton": "削除",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField": "URL が無効です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText": "URL が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.hasAuthSwitchLabel": "この Web フックの認証が必要です",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.httpHeadersTitle": "使用中のヘッダー",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.keyTextFieldLabel": "キー",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.methodTextFieldLabel": "メソド",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel": "パスワード",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.reenterValuesLabel": "ユーザー名とパスワードは暗号化されます。これらのフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText": "Web サービスにリクエストを送信してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.urlTextFieldLabel": "URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel": "ユーザー名",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.valueTextFieldLabel": "値",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "HTTP ヘッダーを追加",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.actionTypeTitle": "xMattersデータ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.authenticationLabel": "認証",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthLabel": "基本認証",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsFieldLabel": "開始URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsLabel": "xMattersトリガーを設定するときに使用される認証方法を選択します。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.invalidUrlTextField": "URL が無効です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.requiredUrlText": "URL が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.initiationUrlHelpText": "完全なxMatters URLを含めます。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.passwordTextFieldLabel": "パスワード",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterBasicAuthValuesLabel": "ユーザー名とパスワードは暗号化されます。これらのフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterUrlAuthValuesLabel": "URLは暗号化されています。このフィールドの値を再入力してください。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.selectMessageText": "xMattersワークフローをトリガーします。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severity": "深刻度",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severitySelectCriticalOptionLabel": "重大",
|
||||
|
@ -29017,10 +28946,6 @@
|
|||
"xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerLinkTitle": "サブスクリプションオプション",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerMessage": "すべてのサードパーティコネクターにすぐにアクセスするには、ライセンスをアップグレードするか、30日間無料の試用版を開始してください。",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerTitle": "ライセンスをアップグレードしてすべてのコネクターにアクセス",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actionNameLabel": "コネクター名",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningHelpLinkText": "詳細情報",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningDescriptionText": "コネクターを作成するには、1 つ以上の{connectorType}アカウントを構成する必要があります。{docLink}",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningTitleText": "コネクタータイプが登録されていません",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.connectorSettingsLabel": "コネクター設定",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "名前が必要です。",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.loadingConnectorSettingsDescription": "コネクター設定を読み込んでいます...",
|
||||
|
@ -29068,25 +28993,14 @@
|
|||
"xpack.triggersActionsUI.sections.actionTypeForm.addNewActionConnectorActionGroup.display": "{actionGroupName}(現在サポートされていません)",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.addNewConnectorEmptyButton": "コネクターの追加",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.existingAlertActionTypeEditTitle": "{actionConnectorName}",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText": "パスワードが必要です。",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText": "ユーザー名が必要です。",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderKeyText": "キーが必要です。",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "値が必要です。",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "メソッドが必要です。",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "ユーザー名の使用時にはパスワードが必要です。",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText": "パスワードの使用時にはユーザー名が必要です。",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthPasswordText": "パスワードが必要です。",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthUserNameText": "ユーザー名が必要です。",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredPasswordText": "ユーザー名の使用時にはパスワードが必要です。",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredUserText": "パスワードの使用時にはユーザー名が必要です。",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName}コネクター",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "コネクターを選択",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "コネクターを作成できません。",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "キャンセル",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName}コネクター",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました",
|
||||
"xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "Bcc",
|
||||
"xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "Cc",
|
||||
"xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "認証",
|
||||
|
@ -29124,7 +29038,6 @@
|
|||
"xpack.triggersActionsUI.sections.connectorAddInline.unableToLoadConnectorTitle'": "コネクターを読み込めません",
|
||||
"xpack.triggersActionsUI.sections.connectorAddInline.unauthorizedToCreateForEmptyConnectors": "許可されたユーザーのみがコネクターを構成できます。管理者にお問い合わせください。",
|
||||
"xpack.triggersActionsUI.sections.deprecatedTitleMessage": "(非推奨)",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{actionDescription}",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel": "キャンセル",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "このコネクターは読み取り専用です。",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "コネクターを編集",
|
||||
|
|
|
@ -28643,7 +28643,6 @@
|
|||
"xpack.triggersActionsUI.actionVariables.ruleTagsLabel": "规则的标签。",
|
||||
"xpack.triggersActionsUI.actionVariables.ruleTypeLabel": "规则的类型。",
|
||||
"xpack.triggersActionsUI.appName": "规则和连接器",
|
||||
"xpack.triggersActionsUI.cases.configureCases.mappingFieldSummary": "摘要",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "连接器已由 Kibana 配置禁用。",
|
||||
"xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "此连接器需要{minimumLicenseRequired}许可证。",
|
||||
"xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage": "此规则类型需要{minimumLicenseRequired}许可证。",
|
||||
|
@ -28681,29 +28680,22 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.gmailServerTypeLabel": "Gmail",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.otherServerTypeLabel": "其他",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.outlookServerTypeLabel": "Outlook",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel": "客户端密钥已加密。请为此字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel": "用户名和密码已加密。请为这些字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "从您的服务器发送电子邮件。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideSuffix": "告警历史记录索引必须包含有效的后缀。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideValue": "告警历史记录索引必须以“{alertHistoryPrefix}”开头。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "发送者电子邮件地址无效。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText": "“密码”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthUserNameText": "“用户名”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientIdText": "“客户端 ID”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText": "“客户端密钥”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson": "“文档”必填,并且应为有效的 JSON 对象。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "未输入收件人、抄送、密送。 至少需要输入一个。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText": "“发送者”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText": "“主机”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText": "“消息”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText": "使用用户名时,“密码”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText": "“端口”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText": "“消息”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServiceText": "“服务”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText": "“消息”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText": "“主题”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredTenantIdText": "“租户 ID”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText": "使用密码时,“用户名”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText": "“正文”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle": "索引数据",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "选择……",
|
||||
|
@ -28712,7 +28704,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.definedateFieldTooltip": "将此时间字段设置为索引文档的时间。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.defineTimeFieldLabel": "为每个文档定义时间字段",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel": "要索引的文档",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText": "“索引”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.executionTimeFieldLabel": "时间字段",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indexDocumentHelpLabel": "索引文档示例。",
|
||||
|
@ -28728,25 +28719,14 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle": "Jira",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel": "API 令牌",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel": "URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.authenticationLabel": "身份验证",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel": "其他注释",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.descriptionTextAreaFieldLabel": "描述",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel": "电子邮件地址",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.impactSelectFieldLabel": "标签",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField": "URL 无效。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.labelsSpacesErrorMessage": "标签不能包含空格。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments": "注释",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription": "描述",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.parentIssueSearchLabel": "父问题",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey": "项目键",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.reenterValuesLabel": "验证凭据已加密。请为这些字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField": "“API 令牌”必填",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField": "“URL”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "“描述”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "电子邮件地址必填",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "“项目键”必填",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredSummaryTextField": "“摘要”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.requireHttpsApiUrlTextField": "URL 必须以 https:// 开头。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "键入内容进行搜索",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder": "键入内容进行搜索",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesLoading": "正在加载……",
|
||||
|
@ -28758,7 +28738,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssuesMessage": "无法获取问题",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage": "无法获取问题类型",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.jira.urgencySelectFieldLabel": "问题类型",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.missingSecretsValuesLabel": "未导入敏感信息。请为以下字段{encryptedFieldsLength, plural, other {}}输入值{encryptedFieldsLength, plural, other {}}。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "发送到 PagerDuty",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "API URL(可选)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "类(可选)",
|
||||
|
@ -28774,7 +28753,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectResolveOptionLabel": "解决",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectTriggerOptionLabel": "触发",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.groupTextFieldLabel": "组(可选)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.reenterValueLabel": "此密钥已加密。请为此字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyNameHelpLabel": "配置 PagerDuty 帐户",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel": "集成密钥",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText": "在 PagerDuty 中发送事件。",
|
||||
|
@ -28786,29 +28764,15 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.sourceTextFieldLabel": "源(可选)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "摘要",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "时间戳(可选)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.rememberValueLabel": "记住{encryptedFieldsLength, plural, one {此} other {这些}}值。每次编辑连接器时都必须重新输入{encryptedFieldsLength, plural, other {值}}。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle": "Resilient",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKey": "API 密钥",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId": "ID",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret": "机密",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel": "URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel": "其他注释",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.descriptionTextAreaFieldLabel": "描述",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField": "URL 无效。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments": "注释",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription": "描述",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription": "名称",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.nameFieldLabel": "名称(必填)",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId": "组织 ID",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.reenterValuesLabel": "ID 和密钥已加密。请为这些字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.rememberValuesLabel": "请记住这些值。每次编辑连接器时都必须重新输入。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField": "“ID”必填",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField": "“密钥”必填",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField": "“URL”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredDescriptionTextField": "“描述”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredNameTextField": "“名称”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField": "“组织 ID”必填",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.requireHttpsApiUrlTextField": "URL 必须以 https:// 开头。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText": "在 IBM Resilient 中创建事件。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.severity": "严重性",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage": "无法获取事件类型",
|
||||
|
@ -28819,7 +28783,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "消息",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "将消息添加到 Kibana 日志。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiInfoError": "尝试获取应用程序信息时收到的状态:{status}",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlHelpText": "包括完整 URL。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel": "ServiceNow 实例 URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.appInstallationInfo": "{update} {create} ",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.applicationRequiredCallout": "未安装 Elastic ServiceNow 应用",
|
||||
|
@ -28838,20 +28801,14 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.descriptionTextAreaFieldLabel": "描述",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.eventClassTextAreaFieldLabel": "源实例",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.impactSelectFieldLabel": "影响",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.install": "安装",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutTitle": "要使用此连接器,请先从 ServiceNow 应用商店安装 Elastic 应用。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField": "URL 无效。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.messageKeyTextAreaFieldLabel": "消息密钥",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.metricNameTextAreaFieldLabel": "指标名称",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.nodeTextAreaFieldLabel": "节点",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel": "密码",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.prioritySelectFieldLabel": "优先级",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel": "每次编辑连接器时都必须进行身份验证。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiUrlTextField": "“URL”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "“密码”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredSeverityTextField": "“严重性”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "“用户名”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField": "URL 必须以 https:// 开头。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.resourceTextAreaFieldLabel": "资源",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.setupDevInstance": "设置开发者实例",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severityRequiredSelectFieldLabel": "严重性(必需)",
|
||||
|
@ -28888,10 +28845,7 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIRAction.correlationIDHelpLabel": "用于更新事件的标识符",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "发送到 Slack",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.invalidWebhookUrlText": "Webhook URL 无效。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "“Webhook URL”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requireHttpsWebhookUrlText": "Webhook URL 必须以 https:// 开头。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "消息",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.reenterValueLabel": "此 URL 已加密。请为此字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "向 Slack 频道或用户发送消息。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlHelpLabel": "创建 Slack webhook URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel": "Webhook URL",
|
||||
|
@ -28899,7 +28853,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage": "无法获取 ID 为 {id} 的应用程序",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.actionTypeTitle": "创建泳道记录",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertIdFieldLabel": "告警 ID",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel": "告警源",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenNameHelpLabel": "提供泳道 API 令牌",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenTextFieldLabel": "API 令牌",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel": "API URL",
|
||||
|
@ -28913,71 +28866,47 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningDesc": "无法选择此连接器,因为其缺失所需的告警字段映射。您可以编辑此连接器以添加所需的字段映射或选择告警类型的连接器。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle": "此连接器缺失字段映射",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertID": "“告警 ID”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource": "“告警源”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredApiTokenText": "“API 令牌”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText": "“应用 ID”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseID": "“案例 ID”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseName": "“案例名称”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredComments": "“注释”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredDescription": "“描述”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredFieldMappingsText": "“字段映射”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredRuleName": "“规则名称”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredSeverity": "“严重性”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.invalidApiUrlTextField": "URL 无效。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingDescriptionTextFieldLabel": "用于指定泳道应用程序中的字段名称",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingFieldRequired": "“字段映射”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingTitleTextFieldLabel": "配置字段映射",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStep": "下一步",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStepHelpText": "如果未配置字段映射,泳道连接器类型将设置为 all。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.prevStep": "返回",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.reenterValueLabel": "此密钥已加密。请为此字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel": "请记住此值。每次编辑连接器时都必须重新输入。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.requiredApiUrlTextField": "“URL”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.retrieveConfigurationLabel": "配置字段",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.ruleNameFieldLabel": "规则名称",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText": "在泳道中创建记录",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.severityFieldLabel": "严重性",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.actionTypeTitle": "向 Microsoft Teams 频道发送消息。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText": "Webhook URL 无效。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText": "“消息”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText": "“Webhook URL”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText": "Webhook URL 必须以 https:// 开头。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.messageTextAreaFieldLabel": "消息",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.reenterValueLabel": "此 URL 已加密。请为此字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.selectMessageText": "向 Microsoft Teams 频道发送消息。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel": "创建 Microsoft Teams Webhook URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel": "Webhook URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Webhook 数据",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "添加标头",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "添加",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.authenticationLabel": "身份验证",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyCodeEditorAriaLabel": "代码编辑器",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyFieldLabel": "正文",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.deleteHeaderButton": "删除",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField": "URL 无效。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText": "“URL”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.hasAuthSwitchLabel": "此 Webhook 需要身份验证",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.httpHeadersTitle": "在用的标头",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.keyTextFieldLabel": "钥匙",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.methodTextFieldLabel": "方法",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel": "密码",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.reenterValuesLabel": "用户名和密码已加密。请为这些字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText": "将请求发送到 Web 服务。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.urlTextFieldLabel": "URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel": "用户名",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.valueTextFieldLabel": "值",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "添加 HTTP 标头",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.actionTypeTitle": "xMatters 数据",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.authenticationLabel": "身份验证",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthLabel": "基本身份验证",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsFieldLabel": "发起 URL",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsLabel": "选择在设置 xMatters 触发器时使用的身份验证方法。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.invalidUrlTextField": "URL 无效。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.requiredUrlText": "“URL”必填。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.initiationUrlHelpText": "包括完整 xMatters url。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.passwordTextFieldLabel": "密码",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterBasicAuthValuesLabel": "用户和密码已加密。请为这些字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterUrlAuthValuesLabel": "URL 已加密。请为此字段重新输入值。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.selectMessageText": "触发 xMatters 工作流。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severity": "严重性",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severitySelectCriticalOptionLabel": "紧急",
|
||||
|
@ -29050,10 +28979,6 @@
|
|||
"xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerLinkTitle": "订阅计划",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerMessage": "升级您的许可证或开始为期 30 天的免费试用,以便可以立即使用所有第三方连接器。",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerTitle": "升级您的许可证以访问所有连接器",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actionNameLabel": "连接器名称",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningHelpLinkText": "了解详情。",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningDescriptionText": "要创建此连接器,必须至少配置一个 {connectorType} 帐户。{docLink}",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningTitleText": "未注册连接器类型",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.connectorSettingsLabel": "连接器设置",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "“名称”必填。",
|
||||
"xpack.triggersActionsUI.sections.actionConnectorForm.loadingConnectorSettingsDescription": "正在加载连接器设置……",
|
||||
|
@ -29101,25 +29026,14 @@
|
|||
"xpack.triggersActionsUI.sections.actionTypeForm.addNewActionConnectorActionGroup.display": "{actionGroupName}(当前不支持)",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.addNewConnectorEmptyButton": "添加连接器",
|
||||
"xpack.triggersActionsUI.sections.actionTypeForm.existingAlertActionTypeEditTitle": "{actionConnectorName}",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText": "“密码”必填。",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText": "“用户名”必填。",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderKeyText": "“键”必填。",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "“值”必填。",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "“方法”必填",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "使用用户名时,“密码”必填。",
|
||||
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText": "使用密码时,“用户名”必填。",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthPasswordText": "“密码”必填。",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthUserNameText": "“用户名”必填。",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredPasswordText": "使用用户名时,“密码”必填。",
|
||||
"xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredUserText": "使用密码时,“用户名”必填。",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} 连接器",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "选择连接器",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "无法创建连接器。",
|
||||
"xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "取消",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} 连接器",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存",
|
||||
"xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”",
|
||||
"xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "密送",
|
||||
"xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "抄送",
|
||||
"xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "身份验证",
|
||||
|
@ -29157,7 +29071,6 @@
|
|||
"xpack.triggersActionsUI.sections.connectorAddInline.unableToLoadConnectorTitle'": "无法加载连接器",
|
||||
"xpack.triggersActionsUI.sections.connectorAddInline.unauthorizedToCreateForEmptyConnectors": "只有获得授权的用户才能配置连接器。请联系您的管理员。",
|
||||
"xpack.triggersActionsUI.sections.deprecatedTitleMessage": "(已过时)",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{actionDescription}",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel": "取消",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "此连接器为只读。",
|
||||
"xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "编辑连接器",
|
||||
|
|
|
@ -13,42 +13,43 @@ As a developer you can reuse and extend built-in alerts and actions UI functiona
|
|||
Table of Contents
|
||||
|
||||
- [Kibana Alerts and Actions UI](#kibana-alerts-and-actions-ui)
|
||||
- [Build and register Alert Types](#build-and-register-alert-types)
|
||||
- [Built-in Alert Types](#built-in-alert-types)
|
||||
- [Index Threshold Alert](#index-threshold-alert)
|
||||
- [Alert type model definition](#alert-type-model-definition)
|
||||
- [Register alert type model](#register-alert-type-model)
|
||||
- [Create and register new alert type UI example](#create-and-register-new-alert-type-ui-example)
|
||||
- [Common expression components](#common-expression-components)
|
||||
- [WHEN expression component](#when-expression-component)
|
||||
- [OF expression component](#of-expression-component)
|
||||
- [GROUPED BY expression component](#grouped-by-expression-component)
|
||||
- [FOR THE LAST expression component](#for-the-last-expression-component)
|
||||
- [THRESHOLD expression component](#threshold-expression-component)
|
||||
- [Alert Conditions Components](#alert-conditions-components)
|
||||
- [Embed the Create Alert flyout within any Kibana plugin](#embed-the-create-alert-flyout-within-any-kibana-plugin)
|
||||
- [Built-in Alert Types](#built-in-alert-types)
|
||||
- [Index Threshold Alert](#index-threshold-alert)
|
||||
- [Alert type model definition](#alert-type-model-definition)
|
||||
- [Register alert type model](#register-alert-type-model)
|
||||
- [Create and register new alert type UI example](#create-and-register-new-alert-type-ui-example)
|
||||
- [Common expression components](#common-expression-components)
|
||||
- [WHEN expression component](#when-expression-component)
|
||||
- [OF expression component](#of-expression-component)
|
||||
- [GROUPED BY expression component](#grouped-by-expression-component)
|
||||
- [FOR THE LAST expression component](#for-the-last-expression-component)
|
||||
- [THRESHOLD expression component](#threshold-expression-component)
|
||||
- [Alert Conditions Components](#alert-conditions-components)
|
||||
- [The AlertConditions component](#the-alertconditions-component)
|
||||
- [The AlertConditionsGroup component](#the-alertconditionsgroup-component)
|
||||
- [Embed the Create Alert flyout within any Kibana plugin](#embed-the-create-alert-flyout-within-any-kibana-plugin)
|
||||
- [Build and register Action Types](#build-and-register-action-types)
|
||||
- [Built-in Action Types](#built-in-action-types)
|
||||
- [Server log](#server-log)
|
||||
- [Email](#email)
|
||||
- [Slack](#slack)
|
||||
- [Index](#index)
|
||||
- [Webhook](#webhook)
|
||||
- [PagerDuty](#pagerduty)
|
||||
- [Action type model definition](#action-type-model-definition)
|
||||
- [Register action type model](#register-action-type-model)
|
||||
- [Create and register new action type UI example](#reate-and-register-new-action-type-ui-example)
|
||||
- [Embed the Alert Actions form within any Kibana plugin](#embed-the-alert-actions-form-within-any-kibana-plugin)
|
||||
- [Embed the Create Connector flyout within any Kibana plugin](#embed-the-create-connector-flyout-within-any-kibana-plugin)
|
||||
- [Embed the Edit Connector flyout within any Kibana plugin](#embed-the-edit-connector-flyout-within-any-kibana-plugin)
|
||||
- [Server log](#server-log)
|
||||
- [Email](#email)
|
||||
- [Slack](#slack)
|
||||
- [Index](#index)
|
||||
- [Webhook](#webhook)
|
||||
- [PagerDuty](#pagerduty)
|
||||
- [Action type model definition](#action-type-model-definition)
|
||||
- [CustomConnectorSelectionItem Properties](#customconnectorselectionitem-properties)
|
||||
- [Register action type model](#register-action-type-model)
|
||||
- [Create and register new action type UI](#create-and-register-new-action-type-ui)
|
||||
- [Embed the Alert Actions form within any Kibana plugin](#embed-the-alert-actions-form-within-any-kibana-plugin)
|
||||
- [Embed the Create Connector flyout within any Kibana plugin](#embed-the-create-connector-flyout-within-any-kibana-plugin)
|
||||
- [Embed the Edit Connector flyout within any Kibana plugin](#embed-the-edit-connector-flyout-within-any-kibana-plugin)
|
||||
|
||||
## Built-in Alert Types
|
||||
|
||||
Kibana ships with several built-in alert types:
|
||||
|
||||
|Type|Id|Description|
|
||||
|---|---|---|
|
||||
|[Index Threshold](#index-threshold-alert)|`threshold`|Index Threshold Alert|
|
||||
| Type | Id | Description |
|
||||
| ----------------------------------------- | ----------- | --------------------- |
|
||||
| [Index Threshold](#index-threshold-alert) | `threshold` | Index Threshold Alert |
|
||||
|
||||
Every alert type must be registered server side, and can optionally be registered client side.
|
||||
Only alert types registered on both client and server will be displayed in the Create Alert flyout, as a part of the UI.
|
||||
|
@ -89,12 +90,12 @@ interface IndexThresholdProps {
|
|||
}
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|ruleParams|Set of Alert params relevant for the index threshold alert type.|
|
||||
|setRuleParams|Alert reducer method, which is used to create a new copy of alert object with the changed params property any subproperty value.|
|
||||
|setRuleProperty|Alert reducer method, which is used to create a new copy of alert object with the changed any direct alert property value.|
|
||||
|errors|Alert level errors tracking object.|
|
||||
| Property | Description |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ruleParams | Set of Alert params relevant for the index threshold alert type. |
|
||||
| setRuleParams | Alert reducer method, which is used to create a new copy of alert object with the changed params property any subproperty value. |
|
||||
| setRuleProperty | Alert reducer method, which is used to create a new copy of alert object with the changed any direct alert property value. |
|
||||
| errors | Alert level errors tracking object. |
|
||||
|
||||
|
||||
Alert reducer is defined on the AlertAdd functional component level and passed down to the subcomponents to provide a new state of Alert object:
|
||||
|
@ -245,16 +246,16 @@ Each alert type should be defined as `RuleTypeModel` object with the these prope
|
|||
>;
|
||||
defaultActionMessage?: string;
|
||||
```
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|id|Alert type id. Should be the same as on the server side.|
|
||||
|name|Name of the alert type that will be displayed on the select card in the UI.|
|
||||
|iconClass|Icon of the alert type that will be displayed on the select card in the UI.|
|
||||
|validate|Validation function for the alert params.|
|
||||
|ruleParamsExpression| A lazy loaded React component for building UI of the current alert type params.|
|
||||
|defaultActionMessage|Optional property for providing default messages for all added actions, excluding the Recovery action group, with `message` property. |
|
||||
|defaultRecoveryMessage|Optional property for providing a default message for all added actions with `message` property for the Recovery action group.|
|
||||
|requiresAppContext|Define if alert type is enabled for create and edit in the alerting management UI.|
|
||||
| Property | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| id | Alert type id. Should be the same as on the server side. |
|
||||
| name | Name of the alert type that will be displayed on the select card in the UI. |
|
||||
| iconClass | Icon of the alert type that will be displayed on the select card in the UI. |
|
||||
| validate | Validation function for the alert params. |
|
||||
| ruleParamsExpression | A lazy loaded React component for building UI of the current alert type params. |
|
||||
| defaultActionMessage | Optional property for providing default messages for all added actions, excluding the Recovery action group, with `message` property. |
|
||||
| defaultRecoveryMessage | Optional property for providing a default message for all added actions with `message` property for the Recovery action group. |
|
||||
| requiresAppContext | Define if alert type is enabled for create and edit in the alerting management UI. |
|
||||
|
||||
IMPORTANT: The current UI supports a single action group only.
|
||||
Action groups are mapped from the server API result for [GET /api/alerts/list_alert_types: List alert types](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting#get-apialerttypes-list-alert-types).
|
||||
|
@ -445,12 +446,12 @@ interface WhenExpressionProps {
|
|||
}
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|aggType|Selected aggregation type that will be set as the alert type property.|
|
||||
|customAggTypesOptions|(Optional) List of aggregation types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/aggregation_types.ts`.|
|
||||
|onChangeSelectedAggType|event handler that will be executed when selected aggregation type is changed.|
|
||||
|popupPosition|(Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space.|
|
||||
| Property | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| aggType | Selected aggregation type that will be set as the alert type property. |
|
||||
| customAggTypesOptions | (Optional) List of aggregation types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/aggregation_types.ts`. |
|
||||
| onChangeSelectedAggType | event handler that will be executed when selected aggregation type is changed. |
|
||||
| popupPosition | (Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space. |
|
||||
|
||||
### OF expression component
|
||||
|
||||
|
@ -486,15 +487,15 @@ interface OfExpressionProps {
|
|||
}
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|aggType|Selected aggregation type that will be set as the alert type property.|
|
||||
|aggField|Selected aggregation field that will be set as the alert type property.|
|
||||
|errors|List of errors with proper messages for the alert params that should be validated. In current component is validated `aggField`.|
|
||||
|onChangeSelectedAggField|Event handler that will be excuted if selected aggregation field is changed.|
|
||||
|fields|Fields list that will be available in the OF `Select a field` dropdown.|
|
||||
|customAggTypesOptions|(Optional) List of aggregation types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/aggregation_types.ts`.|
|
||||
|popupPosition|(Optional) expression popup position. Default is `downRight`. Recommend changing it for a small parent window space.|
|
||||
| Property | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| aggType | Selected aggregation type that will be set as the alert type property. |
|
||||
| aggField | Selected aggregation field that will be set as the alert type property. |
|
||||
| errors | List of errors with proper messages for the alert params that should be validated. In current component is validated `aggField`. |
|
||||
| onChangeSelectedAggField | Event handler that will be excuted if selected aggregation field is changed. |
|
||||
| fields | Fields list that will be available in the OF `Select a field` dropdown. |
|
||||
| customAggTypesOptions | (Optional) List of aggregation types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/aggregation_types.ts`. |
|
||||
| popupPosition | (Optional) expression popup position. Default is `downRight`. Recommend changing it for a small parent window space. |
|
||||
|
||||
### GROUPED BY expression component
|
||||
|
||||
|
@ -536,18 +537,18 @@ interface GroupByExpressionProps {
|
|||
}
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|groupBy|Selected group by type that will be set as the alert type property.|
|
||||
|termSize|Selected term size that will be set as the alert type property.|
|
||||
|termField|Selected term field that will be set as the alert type property.|
|
||||
|errors|List of errors with proper messages for the alert params that should be validated. In current component is validated `termSize` and `termField`.|
|
||||
|onChangeSelectedTermSize|Event handler that will be excuted if selected term size is changed.|
|
||||
|onChangeSelectedTermField|Event handler that will be excuted if selected term field is changed.|
|
||||
|onChangeSelectedGroupBy|Event handler that will be excuted if selected group by is changed.|
|
||||
|fields|Fields list with options for the `termField` dropdown.|
|
||||
|customGroupByTypes|(Optional) List of group by types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/group_by_types.ts`.|
|
||||
|popupPosition|(Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space.|
|
||||
| Property | Description |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| groupBy | Selected group by type that will be set as the alert type property. |
|
||||
| termSize | Selected term size that will be set as the alert type property. |
|
||||
| termField | Selected term field that will be set as the alert type property. |
|
||||
| errors | List of errors with proper messages for the alert params that should be validated. In current component is validated `termSize` and `termField`. |
|
||||
| onChangeSelectedTermSize | Event handler that will be excuted if selected term size is changed. |
|
||||
| onChangeSelectedTermField | Event handler that will be excuted if selected term field is changed. |
|
||||
| onChangeSelectedGroupBy | Event handler that will be excuted if selected group by is changed. |
|
||||
| fields | Fields list with options for the `termField` dropdown. |
|
||||
| customGroupByTypes | (Optional) List of group by types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/group_by_types.ts`. |
|
||||
| popupPosition | (Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space. |
|
||||
|
||||
### FOR THE LAST expression component
|
||||
|
||||
|
@ -580,14 +581,14 @@ interface ForLastExpressionProps {
|
|||
}
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|timeWindowSize|Selected time window size that will be set as the alert type property.|
|
||||
|timeWindowUnit|Selected time window unit that will be set as the alert type property.|
|
||||
|errors|List of errors with proper messages for the alert params that should be validated. In current component is validated `termWindowSize`.|
|
||||
|onChangeWindowSize|Event handler that will be excuted if selected window size is changed.|
|
||||
|onChangeWindowUnit|Event handler that will be excuted if selected window unit is changed.|
|
||||
|popupPosition|(Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space.|
|
||||
| Property | Description |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| timeWindowSize | Selected time window size that will be set as the alert type property. |
|
||||
| timeWindowUnit | Selected time window unit that will be set as the alert type property. |
|
||||
| errors | List of errors with proper messages for the alert params that should be validated. In current component is validated `termWindowSize`. |
|
||||
| onChangeWindowSize | Event handler that will be excuted if selected window size is changed. |
|
||||
| onChangeWindowUnit | Event handler that will be excuted if selected window unit is changed. |
|
||||
| popupPosition | (Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space. |
|
||||
|
||||
### THRESHOLD expression component
|
||||
|
||||
|
@ -623,15 +624,15 @@ interface ThresholdExpressionProps {
|
|||
}
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|thresholdComparator|Selected time window size that will be set as the alert type property.|
|
||||
|threshold|Selected time window size that will be set as the alert type property.|
|
||||
|errors|List of errors with proper messages for the alert params that should be validated. In current component is validated `threshold0` and `threshold1`.|
|
||||
|onChangeSelectedThresholdComparator|Event handler that will be excuted if selected threshold comparator is changed.|
|
||||
|onChangeSelectedThreshold|Event handler that will be excuted if selected threshold is changed.|
|
||||
|customComparators|(Optional) List of comparators that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/comparators.ts`.|
|
||||
|popupPosition|(Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space.|
|
||||
| Property | Description |
|
||||
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| thresholdComparator | Selected time window size that will be set as the alert type property. |
|
||||
| threshold | Selected time window size that will be set as the alert type property. |
|
||||
| errors | List of errors with proper messages for the alert params that should be validated. In current component is validated `threshold0` and `threshold1`. |
|
||||
| onChangeSelectedThresholdComparator | Event handler that will be excuted if selected threshold comparator is changed. |
|
||||
| onChangeSelectedThreshold | Event handler that will be excuted if selected threshold is changed. |
|
||||
| customComparators | (Optional) List of comparators that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/comparators.ts`. |
|
||||
| popupPosition | (Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space. |
|
||||
|
||||
## Alert Conditions Components
|
||||
To aid in creating a uniform UX across Alert Types, we provide two components for specifying the conditions for detection of a certain alert under within any specific Action Groups:
|
||||
|
@ -767,19 +768,19 @@ const DEFAULT_THRESHOLDS: ThresholdAlertTypeParams['threshold] = {
|
|||
This component will render the `Conditions` header & headline, along with the selectors for adding every Action Group you specity.
|
||||
Additionally it will clone its `children` for _each_ action group which has a `condition` specified for it, passing in the appropriate `actionGroup` prop for each one.
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|headline|The headline title displayed above the fields |
|
||||
|actionGroups|A list of `ActionGroupWithCondition` which includes all the action group you wish to offer the user and what conditions they are already configured to follow|
|
||||
|onInitializeConditionsFor|A callback which is called when the user ask for a certain actionGroup to be initialized with an initial default condition. If you have no specific default, that's fine, as the component will render the action group's field even if the condition is empty (using a `null` or an `undefined`) and determines whether to render these fields by _the very presence_ of a `condition` field|
|
||||
| Property | Description |
|
||||
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| headline | The headline title displayed above the fields |
|
||||
| actionGroups | A list of `ActionGroupWithCondition` which includes all the action group you wish to offer the user and what conditions they are already configured to follow |
|
||||
| onInitializeConditionsFor | A callback which is called when the user ask for a certain actionGroup to be initialized with an initial default condition. If you have no specific default, that's fine, as the component will render the action group's field even if the condition is empty (using a `null` or an `undefined`) and determines whether to render these fields by _the very presence_ of a `condition` field |
|
||||
|
||||
### The AlertConditionsGroup component
|
||||
|
||||
This component renders a standard EuiTitle foe each action group, wrapping the Alert Type specific component, in addition to a "reset" button which allows the user to reset the condition for that action group. The definition of what a _reset_ actually means is Alert Type specific, and up to the implementor to decide. In some case it might mean removing the condition, in others it might mean to reset it to some default value on the server side. In either case, it should _delete_ the `condition` field from the appropriate `actionGroup` as per the above example.
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|onResetConditionsFor|A callback which is called when the user clicks the _reset_ button besides the action group's title. The implementor should use this to remove the `condition` from the specified actionGroup|
|
||||
| Property | Description |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| onResetConditionsFor | A callback which is called when the user clicks the _reset_ button besides the action group's title. The implementor should use this to remove the `condition` from the specified actionGroup |
|
||||
|
||||
|
||||
## Embed the Create Alert flyout within any Kibana plugin
|
||||
|
@ -839,29 +840,29 @@ interface AlertAddProps {
|
|||
}
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|consumer|Name of the plugin that creates an alert.|
|
||||
|addFlyoutVisible|Visibility state of the Create Alert flyout.|
|
||||
|setAddFlyoutVisibility|Function for changing visibility state of the Create Alert flyout.|
|
||||
|alertTypeId|Optional property to preselect alert type.|
|
||||
|canChangeTrigger|Optional property, that hides change alert type possibility.|
|
||||
|onSave|Optional function, which will be executed if alert was saved sucsessfuly.|
|
||||
|initialValues|Default values for Alert properties.|
|
||||
|metadata|Optional generic property, which allows to define component specific metadata. This metadata can be used for passing down preloaded data for Alert type expression component.|
|
||||
| Property | Description |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| consumer | Name of the plugin that creates an alert. |
|
||||
| addFlyoutVisible | Visibility state of the Create Alert flyout. |
|
||||
| setAddFlyoutVisibility | Function for changing visibility state of the Create Alert flyout. |
|
||||
| alertTypeId | Optional property to preselect alert type. |
|
||||
| canChangeTrigger | Optional property, that hides change alert type possibility. |
|
||||
| onSave | Optional function, which will be executed if alert was saved sucsessfuly. |
|
||||
| initialValues | Default values for Alert properties. |
|
||||
| metadata | Optional generic property, which allows to define component specific metadata. This metadata can be used for passing down preloaded data for Alert type expression component. |
|
||||
|
||||
## Build and register Action Types
|
||||
|
||||
Kibana ships with a set of built-in action types UI:
|
||||
|
||||
|Type|Id|Description|
|
||||
|---|---|---|
|
||||
|[Server log](#server-log)|`.log`|Logs messages to the Kibana log using `server.log()`|
|
||||
|[Email](#email)|`.email`|Sends an email using SMTP|
|
||||
|[Slack](#slack)|`.slack`|Posts a message to a Slack channel|
|
||||
|[Index](#index)|`.index`|Indexes document(s) into Elasticsearch|
|
||||
|[Webhook](#webhook)|`.webhook`|Sends a payload to a web service using HTTP POST or PUT|
|
||||
|[PagerDuty](#pagerduty)|`.pagerduty`|Triggers, resolves, or acknowledges an incident to a PagerDuty service|
|
||||
| Type | Id | Description |
|
||||
| ------------------------- | ------------ | ---------------------------------------------------------------------- |
|
||||
| [Server log](#server-log) | `.log` | Logs messages to the Kibana log using `server.log()` |
|
||||
| [Email](#email) | `.email` | Sends an email using SMTP |
|
||||
| [Slack](#slack) | `.slack` | Posts a message to a Slack channel |
|
||||
| [Index](#index) | `.index` | Indexes document(s) into Elasticsearch |
|
||||
| [Webhook](#webhook) | `.webhook` | Sends a payload to a web service using HTTP POST or PUT |
|
||||
| [PagerDuty](#pagerduty) | `.pagerduty` | Triggers, resolves, or acknowledges an incident to a PagerDuty service |
|
||||
|
||||
Every action type should be registered server side, and can be optionally registered client side.
|
||||
Only action types registered on both client and server will be displayed in the Alerts and Actions UI.
|
||||
|
@ -889,9 +890,6 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Send to Server log',
|
||||
}
|
||||
),
|
||||
validateConnector: (): Promise<ValidationResult> => {
|
||||
return { errors: {} };
|
||||
},
|
||||
validateParams: (actionParams: ServerLogActionParams): Promise<ValidationResult> => {
|
||||
// validation of action params implementation
|
||||
},
|
||||
|
@ -930,9 +928,6 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Send to email',
|
||||
}
|
||||
),
|
||||
validateConnector: (action: EmailActionConnector): Promise<ValidationResult> => {
|
||||
// validation of connector properties implementation
|
||||
},
|
||||
validateParams: (actionParams: EmailActionParams): Promise<ValidationResult> => {
|
||||
// validation of action params implementation
|
||||
},
|
||||
|
@ -968,9 +963,6 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Send to Slack',
|
||||
}
|
||||
),
|
||||
validateConnector: (action: SlackActionConnector): Promise<ValidationResult> => {
|
||||
// validation of connector properties implementation
|
||||
},
|
||||
validateParams: (actionParams: SlackActionParams): Promise<ValidationResult> => {
|
||||
// validation of action params implementation
|
||||
},
|
||||
|
@ -1001,9 +993,6 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Index data into Elasticsearch.',
|
||||
}
|
||||
),
|
||||
validateConnector: (): Promise<ValidationResult> => {
|
||||
return { errors: {} };
|
||||
},
|
||||
actionConnectorFields: IndexActionConnectorFields,
|
||||
actionParamsFields: IndexParamsFields,
|
||||
validateParams: (): Promise<ValidationResult> => {
|
||||
|
@ -1047,9 +1036,6 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Send a request to a web service.',
|
||||
}
|
||||
),
|
||||
validateConnector: (action: WebhookActionConnector): Promise<ValidationResult> => {
|
||||
// validation of connector properties implementation
|
||||
},
|
||||
validateParams: (actionParams: WebhookActionParams): Promise<ValidationResult> => {
|
||||
// validation of action params implementation
|
||||
},
|
||||
|
@ -1087,9 +1073,6 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Send to PagerDuty',
|
||||
}
|
||||
),
|
||||
validateConnector: (action: PagerDutyActionConnector): Promise<ValidationResult> => {
|
||||
// validation of connector properties implementation
|
||||
},
|
||||
validateParams: (actionParams: PagerDutyActionParams): Promise<ValidationResult> => {
|
||||
// validation of action params implementation
|
||||
},
|
||||
|
@ -1114,22 +1097,20 @@ Each action type should be defined as an `ActionTypeModel` object with the follo
|
|||
iconClass: IconType;
|
||||
selectMessage: string;
|
||||
actionTypeTitle?: string;
|
||||
validateConnector: (connector: any) => Promise<ValidationResult>;
|
||||
validateParams: (actionParams: any) => Promise<ValidationResult>;
|
||||
actionConnectorFields: React.FunctionComponent<any> | null;
|
||||
actionParamsFields: React.LazyExoticComponent<ComponentType<ActionParamsProps<ActionParams>>>;
|
||||
customConnectorSelectItem?: CustomConnectorSelectionItem;
|
||||
```
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|id|Action type id. Should be the same as on server side.|
|
||||
|iconClass|Setting for icon to be displayed to the user. EUI supports any known EUI icon, SVG URL, or a lazy loaded React component, ReactElement.|
|
||||
|selectMessage|Short description of action type responsibility, that will be displayed on the select card in UI.|
|
||||
|validateConnector|Validation function for action connector.|
|
||||
|validateParams|Validation function for action params.|
|
||||
|actionConnectorFields|A lazy loaded React component for building UI of current action type connector.|
|
||||
|actionParamsFields|A lazy loaded React component for building UI of current action type params. Displayed as a part of Create Alert flyout.|
|
||||
|customConnectorSelectItem|Optional, an object for customizing the selection row of the action connector form.|
|
||||
| Property | Description |
|
||||
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| id | Action type id. Should be the same as on server side. |
|
||||
| iconClass | Setting for icon to be displayed to the user. EUI supports any known EUI icon, SVG URL, or a lazy loaded React component, ReactElement. |
|
||||
| selectMessage | Short description of action type responsibility, that will be displayed on the select card in UI. |
|
||||
| validateParams | Validation function for action params. |
|
||||
| actionConnectorFields | A lazy loaded React component for building UI of current action type connector. |
|
||||
| actionParamsFields | A lazy loaded React component for building UI of current action type params. Displayed as a part of Create Alert flyout. |
|
||||
| customConnectorSelectItem | Optional, an object for customizing the selection row of the action connector form. |
|
||||
|
||||
### CustomConnectorSelectionItem Properties
|
||||
|
||||
|
@ -1139,10 +1120,10 @@ Each action type should be defined as an `ActionTypeModel` object with the follo
|
|||
LazyExoticComponent<ComponentType<{ actionConnector: ActionConnector }> | undefined;
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|getText|Function for returning the text to display for the row.|
|
||||
|getComponent|Function for returning a lazy loaded React component for customizing the selection row of the action connector form. Or undefined if if no customization is needed.|
|
||||
| Property | Description |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| getText | Function for returning the text to display for the row. |
|
||||
| getComponent | Function for returning a lazy loaded React component for customizing the selection row of the action connector form. Or undefined if if no customization is needed. |
|
||||
|
||||
## Register action type model
|
||||
|
||||
|
@ -1169,6 +1150,8 @@ Before starting the UI implementation, the [server side registration](https://gi
|
|||
|
||||
Action type UI is expected to be defined as `ActionTypeModel` object.
|
||||
|
||||
The framework uses the [Form lib](https://github.com/elastic/kibana/blob/main/src/plugins/es_ui_shared/static/forms/docs/welcome.mdx). Please refer to the documentation of the library to learn more.
|
||||
|
||||
Below is a list of steps that should be done to build and register a new action type with the name `Example Action Type`:
|
||||
|
||||
1. At any suitable place in Kibana, create a file, which will expose an object implementing interface [ActionTypeModel]:
|
||||
|
@ -1202,24 +1185,6 @@ export function getActionType(): ActionTypeModel {
|
|||
defaultMessage: 'Example Action',
|
||||
}
|
||||
),
|
||||
validateConnector: (action: ExampleActionConnector): Promise<ValidationResult> => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
someConnectorField: new Array<string>(),
|
||||
};
|
||||
validationResult.errors = errors;
|
||||
if (!action.config.someConnectorField) {
|
||||
errors.someConnectorField.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSomeConnectorFieldeText',
|
||||
{
|
||||
defaultMessage: 'SomeConnectorField is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: (actionParams: ExampleActionParams): Promise<ValidationResult> => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
|
@ -1248,42 +1213,39 @@ export function getActionType(): ActionTypeModel {
|
|||
```
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFieldText } from '@elastic/eui';
|
||||
import { EuiTextArea } from '@elastic/eui';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
ValidationResult,
|
||||
ActionConnectorFieldsProps,
|
||||
ActionParamsProps,
|
||||
} from '../../../types';
|
||||
import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { ActionConnectorFieldsProps } from '../../../types';
|
||||
|
||||
interface ExampleActionConnector {
|
||||
config: {
|
||||
someConnectorField: string;
|
||||
};
|
||||
}
|
||||
const { emptyField } = fieldValidators;
|
||||
|
||||
const ExampleConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps<
|
||||
ExampleActionConnector
|
||||
>> = ({ action, editActionConfig, errors }) => {
|
||||
const { someConnectorField } = action.config;
|
||||
return (
|
||||
<>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={errors.someConnectorField.length > 0 && someConnectorField !== undefined}
|
||||
name="someConnectorField"
|
||||
value={someConnectorField || ''}
|
||||
onChange={e => {
|
||||
editActionConfig('someConnectorField', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!someConnectorField) {
|
||||
editActionConfig('someConnectorField', '');
|
||||
const fieldConfig: FieldConfig = {
|
||||
label: 'My field',
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredField',
|
||||
{
|
||||
defaultMessage: 'Field is required.',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ExampleConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({ isEdit, readOnly, registerPreSubmitValidator }) => {
|
||||
return (
|
||||
<UseField
|
||||
path="config.someConnectorField"
|
||||
component={TextField}
|
||||
config={fieldConfig}
|
||||
componentProps={{
|
||||
euiFieldProps: { readOnly: !canSave, 'data-test-subj': 'someTestId', fullWidth: true },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1466,35 +1428,35 @@ interface ActionAccordionFormProps {
|
|||
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|actions|List of actions comes from alert.actions property.|
|
||||
|defaultActionGroupId|Default action group id to which each new action will belong by default.|
|
||||
|actionGroups|Optional. List of action groups to which new action can be assigned. The RunWhen field is only displayed when these action groups are specified|
|
||||
|setActionIdByIndex|Function for changing action 'id' by the proper index in alert.actions array.|
|
||||
|setActionGroupIdByIndex|Function for changing action 'group' by the proper index in alert.actions array.|
|
||||
|setRuleProperty|Function for changing alert property 'actions'. Used when deleting action from the array to reset it.|
|
||||
|setActionParamsProperty|Function for changing action key/value property by index in alert.actions array.|
|
||||
|http|HttpSetup needed for executing API calls.|
|
||||
|actionTypeRegistry|Registry for action types.|
|
||||
|toastNotifications|Toast messages Plugin Setup Contract.|
|
||||
|docLinks|Documentation links Plugin Start Contract.|
|
||||
|actionTypes|Optional property, which allows to define a list of available actions specific for a current plugin.|
|
||||
|messageVariables|Optional property, which allows to define a list of variables for action 'message' property. Set `useWithTripleBracesInTemplates` to true if you don't want the variable escaped when rendering.|
|
||||
|defaultActionMessage|Optional property, which allows to define a message value for action with 'message' property.|
|
||||
|capabilities|Kibana core's Capabilities ApplicationStart['capabilities'].|
|
||||
| Property | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| actions | List of actions comes from alert.actions property. |
|
||||
| defaultActionGroupId | Default action group id to which each new action will belong by default. |
|
||||
| actionGroups | Optional. List of action groups to which new action can be assigned. The RunWhen field is only displayed when these action groups are specified |
|
||||
| setActionIdByIndex | Function for changing action 'id' by the proper index in alert.actions array. |
|
||||
| setActionGroupIdByIndex | Function for changing action 'group' by the proper index in alert.actions array. |
|
||||
| setRuleProperty | Function for changing alert property 'actions'. Used when deleting action from the array to reset it. |
|
||||
| setActionParamsProperty | Function for changing action key/value property by index in alert.actions array. |
|
||||
| http | HttpSetup needed for executing API calls. |
|
||||
| actionTypeRegistry | Registry for action types. |
|
||||
| toastNotifications | Toast messages Plugin Setup Contract. |
|
||||
| docLinks | Documentation links Plugin Start Contract. |
|
||||
| actionTypes | Optional property, which allows to define a list of available actions specific for a current plugin. |
|
||||
| messageVariables | Optional property, which allows to define a list of variables for action 'message' property. Set `useWithTripleBracesInTemplates` to true if you don't want the variable escaped when rendering. |
|
||||
| defaultActionMessage | Optional property, which allows to define a message value for action with 'message' property. |
|
||||
| capabilities | Kibana core's Capabilities ApplicationStart['capabilities']. |
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|onSave|Optional function, which will be executed if alert was saved sucsessfuly.|
|
||||
|http|HttpSetup needed for executing API calls.|
|
||||
|ruleTypeRegistry|Registry for alert types.|
|
||||
|actionTypeRegistry|Registry for action types.|
|
||||
|uiSettings|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|
||||
|docLinks|Documentation Links, needed to link to the documentation from informational callouts.|
|
||||
|toastNotifications|Toast messages.|
|
||||
|charts|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|
||||
|dataFieldsFormats|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|
||||
| Property | Description |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| onSave | Optional function, which will be executed if alert was saved sucsessfuly. |
|
||||
| http | HttpSetup needed for executing API calls. |
|
||||
| ruleTypeRegistry | Registry for alert types. |
|
||||
| actionTypeRegistry | Registry for action types. |
|
||||
| uiSettings | Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring. |
|
||||
| docLinks | Documentation Links, needed to link to the documentation from informational callouts. |
|
||||
| toastNotifications | Toast messages. |
|
||||
| charts | Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring. |
|
||||
| dataFieldsFormats | Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring. |
|
||||
|
||||
## Embed the Create Connector flyout within any Kibana plugin
|
||||
|
||||
|
@ -1517,10 +1479,11 @@ Then this dependency will be used to embed Create Connector flyout or register n
|
|||
2. Add Create Connector flyout to React component:
|
||||
```
|
||||
// import section
|
||||
import { ActionsConnectorsContextProvider, ConnectorAddFlyout } from '../../../../../../../triggers_actions_ui/public';
|
||||
import { ActionsConnectorsContextProvider, CreateConnectorFlyout } from '../../../../../../../triggers_actions_ui/public';
|
||||
|
||||
// in the component state definition section
|
||||
const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false);
|
||||
const onClose = useCallback(() => setAddFlyoutVisibility(false), []);
|
||||
|
||||
// load required dependancied
|
||||
const { http, triggersActionsUi, notifications, application, docLinks } = useKibana().services;
|
||||
|
@ -1549,35 +1512,38 @@ const connector = {
|
|||
</EuiButton>
|
||||
|
||||
// in render section of component
|
||||
<ConnectorAddFlyout
|
||||
addFlyoutVisible={addFlyoutVisible}
|
||||
<CreateConnectorFlyout
|
||||
actionTypeRegistry={triggersActionsUi.actionTypeRegistry}
|
||||
onClose={onClose}
|
||||
setAddFlyoutVisibility={setAddFlyoutVisibility}
|
||||
actionTypes={[
|
||||
supportedActionTypes={[
|
||||
{
|
||||
id: '.index',
|
||||
enabled: true,
|
||||
name: 'Index',
|
||||
},
|
||||
]}
|
||||
reloadConnectors={reloadConnectors}
|
||||
consumer={'alerts'}
|
||||
/>
|
||||
```
|
||||
|
||||
ConnectorAddFlyout Props definition:
|
||||
CreateConnectorFlyout Props definition:
|
||||
```
|
||||
export interface ConnectorAddFlyoutProps {
|
||||
addFlyoutVisible: boolean;
|
||||
setAddFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
actionTypes?: ActionType[];
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
onClose: () => void;
|
||||
supportedActionTypes?: ActionType[];
|
||||
onConnectorCreated?: (connector: ActionConnector) => void;
|
||||
onTestConnector?: (connector: ActionConnector) => void;
|
||||
}
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|addFlyoutVisible|Visibility state of the Create Connector flyout.|
|
||||
|setAddFlyoutVisibility|Function for changing visibility state of the Create Connector flyout.|
|
||||
|actionTypes|Optional property, that allows to define only specific action types list which is available for a current plugin.|
|
||||
| Property | Description |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| actionTypeRegistry | The action type registry. |
|
||||
| onClose | Called when closing the flyout |
|
||||
| supportedActionTypes | Optional property, that allows to define only specific action types list which is available for a current plugin. |
|
||||
| onConnectorCreated | Optional property. Function to be called after the creation of the connector. |
|
||||
| onTestConnector | Optional property. Function to be called when the user press the Save & Test button. |
|
||||
|
||||
## Embed the Edit Connector flyout within any Kibana plugin
|
||||
|
||||
|
@ -1600,7 +1566,7 @@ Then this dependency will be used to embed Edit Connector flyout.
|
|||
2. Add Create Connector flyout to React component:
|
||||
```
|
||||
// import section
|
||||
import { ActionsConnectorsContextProvider, ConnectorEditFlyout } from '../../../../../../../triggers_actions_ui/public';
|
||||
import { ActionsConnectorsContextProvider, EditConnectorFlyout } from '../../../../../../../triggers_actions_ui/public';
|
||||
|
||||
// in the component state definition section
|
||||
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
|
||||
|
@ -1622,31 +1588,32 @@ const { http, triggersActionsUi, notifications, application } = useKibana().serv
|
|||
</EuiButton>
|
||||
|
||||
// in render section of component
|
||||
<ConnectorEditFlyout
|
||||
initialConnector={editedConnectorItem}
|
||||
<EditConnectorFlyout
|
||||
actionTypeRegistry={triggersActionsUi.actionTypeRegistry}
|
||||
connector={editedConnectorItem}
|
||||
onClose={onCloseEditFlyout}
|
||||
reloadConnectors={reloadConnectors}
|
||||
consumer={'alerts'}
|
||||
onConnectorUpdated={reloadConnectors}
|
||||
/>
|
||||
|
||||
```
|
||||
|
||||
ConnectorEditFlyout Props definition:
|
||||
EditConnectorFlyout Props definition:
|
||||
```
|
||||
export interface ConnectorEditProps {
|
||||
initialConnector: ActionConnector;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
connector: ActionConnector;
|
||||
onClose: () => void;
|
||||
tab?: EditConectorTabs;
|
||||
reloadConnectors?: () => Promise<ActionConnector[] | void>;
|
||||
consumer?: string;
|
||||
tab?: EditConnectorTabs;
|
||||
onConnectorUpdated?: (connector: ActionConnector) => void;
|
||||
}
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|initialConnector|Property, that allows to define the initial state of edited connector.|
|
||||
|editFlyoutVisible|Visibility state of the Edit Connector flyout.|
|
||||
|setEditFlyoutVisibility|Function for changing visibility state of the Edit Connector flyout.|
|
||||
| Property | Description |
|
||||
| ------------------ | --------------------------------------------------------------------------- |
|
||||
| actionTypeRegistry | The action type registry. |
|
||||
| connector | Property, that allows to define the initial state of edited connector. |
|
||||
| onClose | Called when closing the flyout |
|
||||
| onConnectorUpdated | Optional property. Function to be called after the update of the connector. |
|
||||
|
||||
ActionsConnectorsContextValue options:
|
||||
```
|
||||
|
@ -1662,10 +1629,10 @@ export interface ActionsConnectorsContextValue {
|
|||
}
|
||||
```
|
||||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|http|HttpSetup needed for executing API calls.|
|
||||
|actionTypeRegistry|Registry for action types.|
|
||||
|capabilities|Property, which is defining action current user usage capabilities like canSave or canDelete.|
|
||||
|toastNotifications|Toast messages.|
|
||||
|reloadConnectors|Optional function, which will be executed if connector was saved sucsessfuly, like reload list of connecotrs.|
|
||||
| Property | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
|
||||
| http | HttpSetup needed for executing API calls. |
|
||||
| actionTypeRegistry | Registry for action types. |
|
||||
| capabilities | Property, which is defining action current user usage capabilities like canSave or canDelete. |
|
||||
| toastNotifications | Toast messages. |
|
||||
| reloadConnectors | Optional function, which will be executed if connector was saved sucsessfuly, like reload list of connecotrs. |
|
||||
|
|
|
@ -31,7 +31,6 @@ const createMockActionTypeModel = (actionType: Partial<ActionTypeModel> = {}): A
|
|||
id,
|
||||
iconClass: `iconClass-${id}`,
|
||||
selectMessage: `selectMessage-${id}`,
|
||||
validateConnector: jest.fn(),
|
||||
validateParams: jest.fn(),
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
|
|
|
@ -23,6 +23,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
|||
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public';
|
||||
import { suspendedComponentWithProps } from './lib/suspended_component_with_props';
|
||||
import {
|
||||
ActionTypeRegistryContract,
|
||||
|
@ -32,7 +33,8 @@ import {
|
|||
import { Section, routeToRuleDetails, legacyRouteToRuleDetails } from './constants';
|
||||
|
||||
import { setDataViewsService } from '../common/lib/data_apis';
|
||||
import { KibanaContextProvider } from '../common/lib/kibana';
|
||||
import { KibanaContextProvider, useKibana } from '../common/lib/kibana';
|
||||
import { ConnectorProvider } from './context/connector_context';
|
||||
|
||||
const TriggersActionsUIHome = lazy(() => import('./home'));
|
||||
const RuleDetailsRoute = lazy(
|
||||
|
@ -40,6 +42,7 @@ const RuleDetailsRoute = lazy(
|
|||
);
|
||||
|
||||
export interface TriggersAndActionsUiServices extends CoreStart {
|
||||
actions: ActionsPublicPluginSetup;
|
||||
data: DataPublicPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
charts: ChartsPluginStart;
|
||||
|
@ -89,23 +92,29 @@ export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => {
|
|||
};
|
||||
|
||||
export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) => {
|
||||
const {
|
||||
actions: { validateEmailAddresses },
|
||||
} = useKibana().services;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
path={`/:section(${sectionsRegex})`}
|
||||
component={suspendedComponentWithProps(TriggersActionsUIHome, 'xl')}
|
||||
/>
|
||||
<Route
|
||||
path={routeToRuleDetails}
|
||||
component={suspendedComponentWithProps(RuleDetailsRoute, 'xl')}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={legacyRouteToRuleDetails}
|
||||
render={({ match }) => <Redirect to={`/rule/${match.params.alertId}`} />}
|
||||
/>
|
||||
<Redirect from={'/'} to="rules" />
|
||||
<Redirect from={'/alerts'} to="rules" />
|
||||
</Switch>
|
||||
<ConnectorProvider value={{ services: { validateEmailAddresses } }}>
|
||||
<Switch>
|
||||
<Route
|
||||
path={`/:section(${sectionsRegex})`}
|
||||
component={suspendedComponentWithProps(TriggersActionsUIHome, 'xl')}
|
||||
/>
|
||||
<Route
|
||||
path={routeToRuleDetails}
|
||||
component={suspendedComponentWithProps(RuleDetailsRoute, 'xl')}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={legacyRouteToRuleDetails}
|
||||
render={({ match }) => <Redirect to={`/rule/${match.params.alertId}`} />}
|
||||
/>
|
||||
<Redirect from={'/'} to="rules" />
|
||||
<Redirect from={'/alerts'} to="rules" />
|
||||
</Switch>
|
||||
</ConnectorProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { EmailActionConnector } from '../types';
|
||||
import { getEmailServices } from './email';
|
||||
import {
|
||||
ValidatedEmail,
|
||||
|
@ -73,301 +72,6 @@ describe('getEmailServices', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
port: 2323,
|
||||
host: 'localhost',
|
||||
test: 'test',
|
||||
hasAuth: true,
|
||||
service: 'other',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
port: [],
|
||||
host: [],
|
||||
service: [],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation succeeds when connector config is valid with empty user/password', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: null,
|
||||
password: null,
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
port: 2323,
|
||||
host: 'localhost',
|
||||
test: 'test',
|
||||
hasAuth: false,
|
||||
service: 'other',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
port: [],
|
||||
host: [],
|
||||
service: [],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
test('connector validation fails when connector config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@notallowed.com',
|
||||
hasAuth: true,
|
||||
service: 'other',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: ['Email address test@notallowed.com is not allowed.'],
|
||||
port: ['Port is required.'],
|
||||
host: ['Host is required.'],
|
||||
service: [],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// also check that mustache is not valid
|
||||
actionConnector.config.from = '{{mustached}}';
|
||||
const validation = await actionTypeModel.validateConnector(actionConnector);
|
||||
expect(validation?.config?.errors?.from).toEqual(['Email address {{mustached}} is not valid.']);
|
||||
});
|
||||
|
||||
test('connector validation fails when user specified but not password', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: null,
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
port: 2323,
|
||||
host: 'localhost',
|
||||
test: 'test',
|
||||
hasAuth: true,
|
||||
service: 'other',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
port: [],
|
||||
host: [],
|
||||
service: [],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: ['Password is required when username is used.'],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
test('connector validation fails when password specified but not user', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: null,
|
||||
password: 'password',
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
port: 2323,
|
||||
host: 'localhost',
|
||||
test: 'test',
|
||||
hasAuth: true,
|
||||
service: 'other',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
port: [],
|
||||
host: [],
|
||||
service: [],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: ['Username is required when password is used.'],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
test('connector validation fails when server type is not selected', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'password',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
port: 2323,
|
||||
host: 'localhost',
|
||||
test: 'test',
|
||||
hasAuth: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
await actionTypeModel.validateConnector(actionConnector as unknown as EmailActionConnector)
|
||||
).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
port: [],
|
||||
host: [],
|
||||
service: ['Service is required.'],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
test('connector validation fails when for exchange service selected, but clientId, tenantId and clientSecrets were not defined', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
hasAuth: true,
|
||||
service: 'exchange_server',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
port: [],
|
||||
host: [],
|
||||
service: [],
|
||||
clientId: ['Client ID is required.'],
|
||||
tenantId: ['Tenant ID is required.'],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
clientSecret: ['Client Secret is required.'],
|
||||
password: [],
|
||||
user: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
|
|
|
@ -9,13 +9,9 @@ import { uniq } from 'lodash';
|
|||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSelectOption } from '@elastic/eui';
|
||||
import { AdditionalEmailServices, InvalidEmailReason } from '@kbn/actions-plugin/common';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
ConnectorValidationResult,
|
||||
GenericValidationResult,
|
||||
} from '../../../../types';
|
||||
import { EmailActionParams, EmailConfig, EmailSecrets, EmailActionConnector } from '../types';
|
||||
import { InvalidEmailReason } from '@kbn/actions-plugin/common';
|
||||
import { ActionTypeModel, GenericValidationResult } from '../../../../types';
|
||||
import { EmailActionParams, EmailConfig, EmailSecrets } from '../types';
|
||||
import { RegistrationServices } from '..';
|
||||
|
||||
const emailServices: EuiSelectOption[] = [
|
||||
|
@ -99,84 +95,6 @@ export function getActionType(
|
|||
defaultMessage: 'Send to email',
|
||||
}
|
||||
),
|
||||
validateConnector: async (
|
||||
action: EmailActionConnector
|
||||
): Promise<
|
||||
ConnectorValidationResult<Omit<EmailConfig, 'secure' | 'hasAuth'>, EmailSecrets>
|
||||
> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
from: new Array<string>(),
|
||||
port: new Array<string>(),
|
||||
host: new Array<string>(),
|
||||
service: new Array<string>(),
|
||||
clientId: new Array<string>(),
|
||||
tenantId: new Array<string>(),
|
||||
};
|
||||
const secretsErrors = {
|
||||
user: new Array<string>(),
|
||||
password: new Array<string>(),
|
||||
clientSecret: new Array<string>(),
|
||||
};
|
||||
|
||||
const validationResult = {
|
||||
config: { errors: configErrors },
|
||||
secrets: { errors: secretsErrors },
|
||||
};
|
||||
if (!action.config.from) {
|
||||
configErrors.from.push(translations.SENDER_REQUIRED);
|
||||
} else {
|
||||
const validatedEmail = services.validateEmailAddresses([action.config.from])[0];
|
||||
if (!validatedEmail.valid) {
|
||||
const message =
|
||||
validatedEmail.reason === InvalidEmailReason.notAllowed
|
||||
? translations.getNotAllowedEmailAddress(action.config.from)
|
||||
: translations.getInvalidEmailAddress(action.config.from);
|
||||
configErrors.from.push(message);
|
||||
}
|
||||
}
|
||||
if (action.config.service !== AdditionalEmailServices.EXCHANGE) {
|
||||
if (!action.config.port) {
|
||||
configErrors.port.push(translations.PORT_REQUIRED);
|
||||
}
|
||||
if (!action.config.host) {
|
||||
configErrors.host.push(translations.HOST_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.user.push(translations.USERNAME_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED);
|
||||
}
|
||||
} else {
|
||||
if (!action.config.clientId) {
|
||||
configErrors.clientId.push(translations.CLIENT_ID_REQUIRED);
|
||||
}
|
||||
if (!action.config.tenantId) {
|
||||
configErrors.tenantId.push(translations.TENANT_ID_REQUIRED);
|
||||
}
|
||||
if (!action.secrets.clientSecret) {
|
||||
secretsErrors.clientSecret.push(translations.CLIENT_SECRET_REQUIRED);
|
||||
}
|
||||
}
|
||||
if (!action.config.service) {
|
||||
configErrors.service.push(translations.SERVICE_REQUIRED);
|
||||
}
|
||||
if (action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED_FOR_USER_USED);
|
||||
}
|
||||
if (!action.secrets.user && action.secrets.password) {
|
||||
secretsErrors.user.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText',
|
||||
{
|
||||
defaultMessage: 'Username is required when password is used.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: async (
|
||||
actionParams: EmailActionParams
|
||||
): Promise<GenericValidationResult<EmailActionParams>> => {
|
||||
|
|
|
@ -5,16 +5,25 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { EmailActionConnector } from '../types';
|
||||
import { act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import EmailActionConnectorFields from './email_connector';
|
||||
import * as hooks from './use_email_config';
|
||||
import {
|
||||
AppMockRenderer,
|
||||
ConnectorFormTestProvider,
|
||||
createAppMockRenderer,
|
||||
waitForComponentToUpdate,
|
||||
} from '../test_utils';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
describe('EmailActionConnectorFields renders', () => {
|
||||
test('all connector fields is rendered', () => {
|
||||
describe('EmailActionConnectorFields', () => {
|
||||
test('all connector fields are rendered', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
|
@ -27,18 +36,21 @@ describe('EmailActionConnectorFields renders', () => {
|
|||
from: 'test@test.com',
|
||||
hasAuth: true,
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<EmailActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [], service: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe(
|
||||
'test@test.com'
|
||||
|
@ -50,7 +62,7 @@ describe('EmailActionConnectorFields renders', () => {
|
|||
expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('secret connector fields is not rendered when hasAuth false', () => {
|
||||
test('secret connector fields are not rendered when hasAuth false', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
|
@ -60,18 +72,21 @@ describe('EmailActionConnectorFields renders', () => {
|
|||
from: 'test@test.com',
|
||||
hasAuth: false,
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<EmailActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [], service: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe(
|
||||
'test@test.com'
|
||||
|
@ -82,7 +97,7 @@ describe('EmailActionConnectorFields renders', () => {
|
|||
expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeFalsy();
|
||||
});
|
||||
|
||||
test('service field defaults to empty when not defined', () => {
|
||||
test('service field defaults to empty when not defined', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
|
@ -95,18 +110,21 @@ describe('EmailActionConnectorFields renders', () => {
|
|||
from: 'test@test.com',
|
||||
hasAuth: true,
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<EmailActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [], service: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe(
|
||||
'test@test.com'
|
||||
);
|
||||
|
@ -116,7 +134,7 @@ describe('EmailActionConnectorFields renders', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('service field is correctly selected when defined', () => {
|
||||
test('service field are correctly selected when defined', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
|
@ -130,28 +148,35 @@ describe('EmailActionConnectorFields renders', () => {
|
|||
hasAuth: true,
|
||||
service: 'gmail',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<EmailActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [], service: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="emailServiceSelectInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('select[data-test-subj="emailServiceSelectInput"]').prop('value')).toEqual(
|
||||
'gmail'
|
||||
);
|
||||
});
|
||||
|
||||
test('host, port and secure fields should be disabled when service field is set to well known service', () => {
|
||||
test('host, port and secure fields should be disabled when service field is set to well known service', async () => {
|
||||
const getEmailServiceConfig = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ host: 'https://example.com', port: 80, secure: false });
|
||||
jest
|
||||
.spyOn(hooks, 'useEmailConfig')
|
||||
.mockImplementation(() => ({ emailServiceConfigurable: false, setEmailService: jest.fn() }));
|
||||
.mockImplementation(() => ({ isLoading: false, getEmailServiceConfig }));
|
||||
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
|
@ -165,18 +190,22 @@ describe('EmailActionConnectorFields renders', () => {
|
|||
hasAuth: true,
|
||||
service: 'gmail',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<EmailActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [], service: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="emailHostInput"]').first().prop('disabled')).toBe(true);
|
||||
expect(wrapper.find('[data-test-subj="emailPortInput"]').first().prop('disabled')).toBe(true);
|
||||
expect(wrapper.find('[data-test-subj="emailSecureSwitch"]').first().prop('disabled')).toBe(
|
||||
|
@ -184,10 +213,14 @@ describe('EmailActionConnectorFields renders', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('host, port and secure fields should not be disabled when service field is set to other', () => {
|
||||
test('host, port and secure fields should not be disabled when service field is set to other', async () => {
|
||||
const getEmailServiceConfig = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ host: 'https://example.com', port: 80, secure: false });
|
||||
jest
|
||||
.spyOn(hooks, 'useEmailConfig')
|
||||
.mockImplementation(() => ({ emailServiceConfigurable: true, setEmailService: jest.fn() }));
|
||||
.mockImplementation(() => ({ isLoading: false, getEmailServiceConfig }));
|
||||
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
|
@ -201,18 +234,21 @@ describe('EmailActionConnectorFields renders', () => {
|
|||
hasAuth: true,
|
||||
service: 'other',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<EmailActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [], service: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="emailHostInput"]').first().prop('disabled')).toBe(false);
|
||||
expect(wrapper.find('[data-test-subj="emailPortInput"]').first().prop('disabled')).toBe(false);
|
||||
expect(wrapper.find('[data-test-subj="emailSecureSwitch"]').first().prop('disabled')).toBe(
|
||||
|
@ -220,75 +256,436 @@ describe('EmailActionConnectorFields renders', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('should display a message to remember username and password when creating a connector with authentication', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.email',
|
||||
config: {
|
||||
hasAuth: true,
|
||||
},
|
||||
secrets: {},
|
||||
} as EmailActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<EmailActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
describe('Validation', () => {
|
||||
let appMockRenderer: AppMockRenderer;
|
||||
const onSubmit = jest.fn();
|
||||
const validateEmailAddresses = jest.fn();
|
||||
|
||||
test('should display a message for missing secrets after import', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.email',
|
||||
config: {
|
||||
hasAuth: true,
|
||||
},
|
||||
isMissingSecrets: true,
|
||||
secrets: {},
|
||||
} as EmailActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<EmailActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0);
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
appMockRenderer = createAppMockRenderer();
|
||||
validateEmailAddresses.mockReturnValue([{ valid: true }]);
|
||||
});
|
||||
|
||||
test('should display a message when editing an authenticated email connector explaining why username and password must be re-entered', () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
hasAuth: true,
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<EmailActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
it('submits the connector', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
port: 2323,
|
||||
host: 'localhost',
|
||||
test: 'test',
|
||||
hasAuth: true,
|
||||
service: 'other',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider
|
||||
connector={actionConnector}
|
||||
onSubmit={onSubmit}
|
||||
connectorServices={{ validateEmailAddresses }}
|
||||
>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
hasAuth: true,
|
||||
host: 'localhost',
|
||||
port: 2323,
|
||||
secure: false,
|
||||
service: 'other',
|
||||
},
|
||||
id: 'test',
|
||||
isDeprecated: false,
|
||||
name: 'email',
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('submits the connector with auth false', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: null,
|
||||
password: null,
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
port: 2323,
|
||||
host: 'localhost',
|
||||
test: 'test',
|
||||
hasAuth: false,
|
||||
service: 'other',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider
|
||||
connector={actionConnector}
|
||||
onSubmit={onSubmit}
|
||||
connectorServices={{ validateEmailAddresses }}
|
||||
>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
port: 2323,
|
||||
host: 'localhost',
|
||||
hasAuth: false,
|
||||
service: 'other',
|
||||
secure: false,
|
||||
},
|
||||
id: 'test',
|
||||
isDeprecated: false,
|
||||
name: 'email',
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('connector validation fails when connector config is not valid', async () => {
|
||||
useKibanaMock().services.actions.validateEmailAddresses = jest
|
||||
.fn()
|
||||
.mockReturnValue([{ valid: false }]);
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@notallowed.com',
|
||||
hasAuth: true,
|
||||
service: 'other',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider
|
||||
connector={actionConnector}
|
||||
onSubmit={onSubmit}
|
||||
connectorServices={{ validateEmailAddresses }}
|
||||
>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {},
|
||||
isValid: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('connector validation fails when user specified but not password', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: '',
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
port: 2323,
|
||||
host: 'localhost',
|
||||
test: 'test',
|
||||
hasAuth: true,
|
||||
service: 'other',
|
||||
},
|
||||
};
|
||||
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider
|
||||
connector={actionConnector}
|
||||
onSubmit={onSubmit}
|
||||
connectorServices={{ validateEmailAddresses }}
|
||||
>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {},
|
||||
isValid: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('connector validation fails when server type is not selected', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'password',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
port: 2323,
|
||||
host: 'localhost',
|
||||
test: 'test',
|
||||
hasAuth: true,
|
||||
},
|
||||
};
|
||||
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider
|
||||
connector={actionConnector}
|
||||
onSubmit={onSubmit}
|
||||
connectorServices={{ validateEmailAddresses }}
|
||||
>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {},
|
||||
isValid: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('connector validation fails when exchange service is selected, but clientId, tenantId and clientSecrets were not defined', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
hasAuth: true,
|
||||
service: 'exchange_server',
|
||||
},
|
||||
};
|
||||
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider
|
||||
connector={actionConnector}
|
||||
onSubmit={onSubmit}
|
||||
connectorServices={{ validateEmailAddresses }}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</Suspense>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {},
|
||||
isValid: false,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([[123.5], ['123.5']])(
|
||||
'connector validation fails when port is not an integer: %p',
|
||||
async (port) => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@notallowed.com',
|
||||
hasAuth: true,
|
||||
service: 'other',
|
||||
host: 'my-host',
|
||||
port,
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider
|
||||
connector={actionConnector}
|
||||
onSubmit={onSubmit}
|
||||
connectorServices={{ validateEmailAddresses }}
|
||||
>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {},
|
||||
isValid: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
|
||||
it.each([[123], ['123']])('connector validation pass when port is valid: %p', async (port) => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@notallowed.com',
|
||||
hasAuth: true,
|
||||
service: 'other',
|
||||
host: 'my-host',
|
||||
port,
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider
|
||||
connector={actionConnector}
|
||||
onSubmit={onSubmit}
|
||||
connectorServices={{ validateEmailAddresses }}
|
||||
>
|
||||
<EmailActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.email',
|
||||
config: {
|
||||
from: 'test@notallowed.com',
|
||||
hasAuth: true,
|
||||
host: 'my-host',
|
||||
port,
|
||||
secure: false,
|
||||
service: 'other',
|
||||
},
|
||||
id: 'test',
|
||||
isDeprecated: false,
|
||||
name: 'email',
|
||||
secrets: {
|
||||
password: 'pass',
|
||||
user: 'user',
|
||||
},
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,314 +5,244 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { lazy, useEffect } from 'react';
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiFieldNumber,
|
||||
EuiFieldPassword,
|
||||
EuiSelect,
|
||||
EuiSwitch,
|
||||
EuiFormRow,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { lazy, useEffect, useMemo } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { EuiFlexItem, EuiFlexGroup, EuiTitle, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { AdditionalEmailServices } from '@kbn/actions-plugin/common';
|
||||
import { AdditionalEmailServices, InvalidEmailReason } from '@kbn/actions-plugin/common';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public';
|
||||
import {
|
||||
UseField,
|
||||
useFormContext,
|
||||
useFormData,
|
||||
FieldConfig,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import {
|
||||
NumericField,
|
||||
SelectField,
|
||||
TextField,
|
||||
ToggleField,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { EmailActionConnector } from '../types';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label';
|
||||
import { getEmailServices } from './email';
|
||||
import { useEmailConfig } from './use_email_config';
|
||||
import { PasswordField } from '../../password_field';
|
||||
import * as i18n from './translations';
|
||||
import { useConnectorContext } from '../../../context/use_connector_context';
|
||||
|
||||
const { emptyField } = fieldValidators;
|
||||
|
||||
const ExchangeFormFields = lazy(() => import('./exchange_form'));
|
||||
export const EmailActionConnectorFields: React.FunctionComponent<
|
||||
ActionConnectorFieldsProps<EmailActionConnector>
|
||||
> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => {
|
||||
const { docLinks, http, isCloud } = useKibana().services;
|
||||
const { from, host, port, secure, hasAuth, service } = action.config;
|
||||
const { user, password } = action.secrets;
|
||||
|
||||
const { emailServiceConfigurable, setEmailService } = useEmailConfig(
|
||||
const shouldDisableEmailConfiguration = (service: string | null | undefined) =>
|
||||
isEmpty(service) ||
|
||||
(service !== AdditionalEmailServices.EXCHANGE && service !== AdditionalEmailServices.OTHER);
|
||||
|
||||
const getEmailConfig = (
|
||||
href: string,
|
||||
validateFunc: ActionsPublicPluginSetup['validateEmailAddresses']
|
||||
): FieldConfig<string> => ({
|
||||
label: i18n.FROM_LABEL,
|
||||
helpText: (
|
||||
<EuiLink href={href} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.emailAction.configureAccountsHelpLabel"
|
||||
defaultMessage="Configure email accounts"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
validations: [
|
||||
{ validator: emptyField(i18n.SENDER_REQUIRED) },
|
||||
{
|
||||
validator: ({ value }) => {
|
||||
const validatedEmail = validateFunc([value])[0];
|
||||
if (!validatedEmail.valid) {
|
||||
const message =
|
||||
validatedEmail.reason === InvalidEmailReason.notAllowed
|
||||
? i18n.getNotAllowedEmailAddress(value)
|
||||
: i18n.getInvalidEmailAddress(value);
|
||||
|
||||
return {
|
||||
message,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const portConfig: FieldConfig<string> = {
|
||||
label: i18n.PORT_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.PORT_REQUIRED),
|
||||
},
|
||||
{
|
||||
validator: ({ value }) => {
|
||||
const port = Number.parseFloat(value);
|
||||
|
||||
if (!Number.isInteger(port)) {
|
||||
return { message: i18n.PORT_INVALID };
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const EmailActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
readOnly,
|
||||
}) => {
|
||||
const {
|
||||
docLinks,
|
||||
http,
|
||||
service,
|
||||
editActionConfig
|
||||
isCloud,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const {
|
||||
services: { validateEmailAddresses },
|
||||
} = useConnectorContext();
|
||||
|
||||
const form = useFormContext();
|
||||
const { updateFieldValues } = form;
|
||||
const [{ config }] = useFormData({
|
||||
watch: ['config.service', 'config.hasAuth'],
|
||||
});
|
||||
|
||||
const emailFieldConfig = useMemo(
|
||||
() => getEmailConfig(docLinks.links.alerting.emailActionConfig, validateEmailAddresses),
|
||||
[docLinks.links.alerting.emailActionConfig, validateEmailAddresses]
|
||||
);
|
||||
|
||||
const { service = null, hasAuth = false } = config ?? {};
|
||||
const disableServiceConfig = shouldDisableEmailConfiguration(service);
|
||||
const { isLoading, getEmailServiceConfig } = useEmailConfig({ http, toasts });
|
||||
|
||||
useEffect(() => {
|
||||
if (!action.id) {
|
||||
editActionConfig('hasAuth', true);
|
||||
async function fetchConfig() {
|
||||
if (
|
||||
service === null ||
|
||||
service === AdditionalEmailServices.OTHER ||
|
||||
service === AdditionalEmailServices.EXCHANGE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emailConfig = await getEmailServiceConfig(service);
|
||||
updateFieldValues({
|
||||
config: {
|
||||
host: emailConfig?.host,
|
||||
port: emailConfig?.port,
|
||||
secure: emailConfig?.secure,
|
||||
},
|
||||
});
|
||||
}
|
||||
// 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 isServiceInvalid: boolean =
|
||||
service !== undefined && errors.service !== undefined && errors.service.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;
|
||||
|
||||
const authForm = (
|
||||
<>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
2,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Username and password are encrypted. Please reenter values for these fields.',
|
||||
}
|
||||
)
|
||||
)}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="emailUser"
|
||||
fullWidth
|
||||
error={errors.user}
|
||||
isInvalid={isUserInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Username',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isUserInvalid}
|
||||
name="user"
|
||||
readOnly={readOnly}
|
||||
value={user || ''}
|
||||
data-test-subj="emailUserInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('user', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!user) {
|
||||
editActionSecrets('user', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="emailPassword"
|
||||
fullWidth
|
||||
error={errors.password}
|
||||
isInvalid={isPasswordInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Password',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={isPasswordInvalid}
|
||||
name="password"
|
||||
value={password || ''}
|
||||
data-test-subj="emailPasswordInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('password', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!password) {
|
||||
editActionSecrets('password', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
fetchConfig();
|
||||
}, [updateFieldValues, getEmailServiceConfig, service]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="from"
|
||||
fullWidth
|
||||
error={errors.from}
|
||||
isInvalid={isFromInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Sender',
|
||||
}
|
||||
)}
|
||||
helpText={
|
||||
<EuiLink href={docLinks.links.alerting.emailActionConfig} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.emailAction.configureAccountsHelpLabel"
|
||||
defaultMessage="Configure email accounts"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={isFromInvalid}
|
||||
name="from"
|
||||
value={from || ''}
|
||||
data-test-subj="emailFromInput"
|
||||
onChange={(e) => {
|
||||
editActionConfig('from', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!from) {
|
||||
editActionConfig('from', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path="config.from"
|
||||
component={TextField}
|
||||
config={emailFieldConfig}
|
||||
componentProps={{
|
||||
euiFieldProps: { 'data-test-subj': 'emailFromInput', readOnly },
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.serviceTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Service',
|
||||
}
|
||||
)}
|
||||
error={errors.serverType}
|
||||
isInvalid={isServiceInvalid}
|
||||
>
|
||||
<EuiSelect
|
||||
name="service"
|
||||
hasNoInitialSelection={true}
|
||||
value={service}
|
||||
disabled={readOnly}
|
||||
isInvalid={isServiceInvalid}
|
||||
data-test-subj="emailServiceSelectInput"
|
||||
options={getEmailServices(isCloud)}
|
||||
onChange={(e) => {
|
||||
setEmailService(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path="config.service"
|
||||
component={SelectField}
|
||||
config={{
|
||||
label: i18n.SERVICE_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.SERVICE_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'emailServiceSelectInput',
|
||||
options: getEmailServices(isCloud),
|
||||
fullWidth: true,
|
||||
hasNoInitialSelection: true,
|
||||
disabled: readOnly || isLoading,
|
||||
isLoading,
|
||||
readOnly,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{service === AdditionalEmailServices.EXCHANGE ? (
|
||||
<ExchangeFormFields
|
||||
action={action}
|
||||
editActionConfig={editActionConfig}
|
||||
editActionSecrets={editActionSecrets}
|
||||
errors={errors}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
<ExchangeFormFields readOnly={readOnly} />
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="emailHost"
|
||||
fullWidth
|
||||
error={errors.host}
|
||||
isInvalid={isHostInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Host',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
disabled={!emailServiceConfigurable}
|
||||
readOnly={readOnly}
|
||||
isInvalid={isHostInvalid}
|
||||
name="host"
|
||||
value={host || ''}
|
||||
data-test-subj="emailHostInput"
|
||||
onChange={(e) => {
|
||||
editActionConfig('host', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!host) {
|
||||
editActionConfig('host', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path="config.host"
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.HOST_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.HOST_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'emailHostInput',
|
||||
readOnly,
|
||||
isLoading,
|
||||
disabled: disableServiceConfig,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="emailPort"
|
||||
fullWidth
|
||||
placeholder="587"
|
||||
error={errors.port}
|
||||
isInvalid={isPortInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Port',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
prepend=":"
|
||||
isInvalid={isPortInvalid}
|
||||
fullWidth
|
||||
disabled={!emailServiceConfigurable}
|
||||
readOnly={readOnly}
|
||||
name="port"
|
||||
value={port || ''}
|
||||
data-test-subj="emailPortInput"
|
||||
onChange={(e) => {
|
||||
editActionConfig('port', parseInt(e.target.value, 10));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!port) {
|
||||
editActionConfig('port', 0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path="config.port"
|
||||
component={NumericField}
|
||||
config={portConfig}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'emailPortInput',
|
||||
readOnly,
|
||||
isLoading,
|
||||
disabled: disableServiceConfig,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow hasEmptyLabelSpace>
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.secureSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Secure',
|
||||
}
|
||||
)}
|
||||
data-test-subj="emailSecureSwitch"
|
||||
disabled={readOnly || !emailServiceConfigurable}
|
||||
checked={secure || false}
|
||||
onChange={(e) => {
|
||||
editActionConfig('secure', e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<UseField
|
||||
path="config.secure"
|
||||
component={ToggleField}
|
||||
config={{ defaultValue: false }}
|
||||
componentProps={{
|
||||
hasEmptyLabelSpace: true,
|
||||
euiFieldProps: {
|
||||
label: i18n.SECURE_LABEL,
|
||||
disabled: readOnly || disableServiceConfig,
|
||||
'data-test-subj': 'emailSecureSwitch',
|
||||
readOnly,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
@ -329,26 +259,51 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hasAuthSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Require authentication for this server',
|
||||
}
|
||||
)}
|
||||
disabled={readOnly}
|
||||
checked={hasAuth || false}
|
||||
onChange={(e) => {
|
||||
editActionConfig('hasAuth', e.target.checked);
|
||||
if (!e.target.checked) {
|
||||
editActionSecrets('user', null);
|
||||
editActionSecrets('password', null);
|
||||
}
|
||||
<UseField
|
||||
path="config.hasAuth"
|
||||
component={ToggleField}
|
||||
config={{ defaultValue: true }}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
label: i18n.HAS_AUTH_LABEL,
|
||||
disabled: readOnly,
|
||||
readOnly,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{hasAuth ? authForm : null}
|
||||
{hasAuth ? (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path="secrets.user"
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.USERNAME_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.USERNAME_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: { 'data-test-subj': 'emailUserInput', readOnly },
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PasswordField
|
||||
path="secrets.password"
|
||||
label={i18n.PASSWORD_LABEL}
|
||||
readOnly={readOnly}
|
||||
data-test-subj="emailPasswordInput"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -7,36 +7,35 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { EmailActionConnector } from '../types';
|
||||
import ExchangeFormFields from './exchange_form';
|
||||
import { ConnectorFormTestProvider } from '../test_utils';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
describe('ExchangeFormFields renders', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
clientSecret: 'secret',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
service: 'exchange_server',
|
||||
tenantId: 'tenant-id',
|
||||
clientId: 'clientId-id',
|
||||
},
|
||||
};
|
||||
|
||||
test('should display exchange form fields', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
clientSecret: 'user',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'exchange email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
hasAuth: true,
|
||||
service: 'exchange_server',
|
||||
clientId: '123',
|
||||
tenantId: '1234',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<ExchangeFormFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [], service: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<ExchangeFormFields readOnly={false} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="emailClientId"]').length > 0).toBeTruthy();
|
||||
|
@ -44,25 +43,18 @@ describe('ExchangeFormFields renders', () => {
|
|||
});
|
||||
|
||||
test('exchange field defaults to empty when not defined', () => {
|
||||
const actionConnector = {
|
||||
const connector = {
|
||||
...actionConnector,
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
hasAuth: true,
|
||||
service: 'exchange_server',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ExchangeFormFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [], service: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={connector}>
|
||||
<ExchangeFormFields readOnly={false} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('input[data-test-subj="emailClientSecret"]').prop('value')).toEqual('');
|
||||
|
@ -73,4 +65,67 @@ describe('ExchangeFormFields renders', () => {
|
|||
expect(wrapper.find('[data-test-subj="emailTenantId"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('input[data-test-subj="emailTenantId"]').prop('value')).toEqual('');
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const tests: Array<[string, string]> = [
|
||||
['emailTenantId', ''],
|
||||
['emailClientId', ''],
|
||||
['emailClientSecret', ''],
|
||||
];
|
||||
|
||||
it('connector validation succeeds when connector config is valid', async () => {
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<ExchangeFormFields readOnly={false} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
secrets: {
|
||||
clientSecret: 'secret',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
tenantId: 'tenant-id',
|
||||
clientId: 'clientId-id',
|
||||
},
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(tests)('validates correctly %p', async (field, value) => {
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<ExchangeFormFields readOnly={false} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,158 +6,88 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiFormRow,
|
||||
EuiFieldPassword,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexItem, EuiFlexGroup, EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { IErrorObject } from '../../../../types';
|
||||
import { EmailActionConnector } from '../types';
|
||||
import { nullableString } from './email_connector';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import * as i18n from './translations';
|
||||
import { PasswordField } from '../../password_field';
|
||||
|
||||
const { emptyField } = fieldValidators;
|
||||
|
||||
interface ExchangeFormFieldsProps {
|
||||
action: EmailActionConnector;
|
||||
editActionConfig: (property: string, value: unknown) => void;
|
||||
editActionSecrets: (property: string, value: unknown) => void;
|
||||
errors: IErrorObject;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
const ExchangeFormFields: React.FunctionComponent<ExchangeFormFieldsProps> = ({
|
||||
action,
|
||||
editActionConfig,
|
||||
editActionSecrets,
|
||||
errors,
|
||||
readOnly,
|
||||
}) => {
|
||||
const ExchangeFormFields: React.FC<ExchangeFormFieldsProps> = ({ readOnly }) => {
|
||||
const { docLinks } = useKibana().services;
|
||||
const { tenantId, clientId } = action.config;
|
||||
const { clientSecret } = action.secrets;
|
||||
|
||||
const isClientIdInvalid: boolean =
|
||||
clientId !== undefined && errors.clientId !== undefined && errors.clientId.length > 0;
|
||||
const isTenantIdInvalid: boolean =
|
||||
tenantId !== undefined && errors.tenantId !== undefined && errors.tenantId.length > 0;
|
||||
const isClientSecretInvalid: boolean =
|
||||
clientSecret !== undefined &&
|
||||
errors.clientSecret !== undefined &&
|
||||
errors.clientSecret.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="tenantId"
|
||||
error={errors.tenantId}
|
||||
isInvalid={isTenantIdInvalid}
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.tenantIdFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Tenant ID',
|
||||
}
|
||||
)}
|
||||
helpText={
|
||||
<EuiLink href={docLinks.links.alerting.emailExchangeClientIdConfig} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.email.exchangeForm.tenantIdHelpLabel"
|
||||
defaultMessage="Configure Tenant ID"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isTenantIdInvalid}
|
||||
name="tenantId"
|
||||
data-test-subj="emailTenantId"
|
||||
readOnly={readOnly}
|
||||
value={tenantId || ''}
|
||||
placeholder={'00000000-0000-0000-0000-000000000000'}
|
||||
onChange={(e) => {
|
||||
editActionConfig('tenantId', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!tenantId) {
|
||||
editActionConfig('tenantId', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path="config.tenantId"
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.TENANT_ID_LABEL,
|
||||
helpText: (
|
||||
<EuiLink href={docLinks.links.alerting.emailExchangeClientIdConfig} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.email.exchangeForm.tenantIdHelpLabel"
|
||||
defaultMessage="Configure Tenant ID"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.TENANT_ID_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: { 'data-test-subj': 'emailTenantId' },
|
||||
readOnly,
|
||||
placeholder: '00000000-0000-0000-0000-000000000000',
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="clientId"
|
||||
error={errors.clientId}
|
||||
isInvalid={isClientIdInvalid}
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.clientIdFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Client ID',
|
||||
}
|
||||
)}
|
||||
helpText={
|
||||
<EuiLink href={docLinks.links.alerting.emailExchangeClientIdConfig} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.email.exchangeForm.clientIdHelpLabel"
|
||||
defaultMessage="Configure Client ID"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isClientIdInvalid}
|
||||
name="clientId"
|
||||
data-test-subj="emailClientId"
|
||||
readOnly={readOnly}
|
||||
placeholder={'00000000-0000-0000-0000-000000000000'}
|
||||
value={clientId || ''}
|
||||
onChange={(e) => {
|
||||
editActionConfig('clientId', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!clientId) {
|
||||
editActionConfig('clientId', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path="config.clientId"
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.CLIENT_ID_LABEL,
|
||||
helpText: (
|
||||
<EuiLink href={docLinks.links.alerting.emailExchangeClientIdConfig} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.email.exchangeForm.clientIdHelpLabel"
|
||||
defaultMessage="Configure Client ID"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.CLIENT_ID_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: { 'data-test-subj': 'emailClientId' },
|
||||
readOnly,
|
||||
placeholder: '00000000-0000-0000-0000-000000000000',
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
1,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel',
|
||||
{
|
||||
defaultMessage: 'Client Secret is encrypted. Please reenter value for this field.',
|
||||
}
|
||||
)
|
||||
)}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="clientSecret"
|
||||
fullWidth
|
||||
error={errors.clientSecret}
|
||||
isInvalid={isClientSecretInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.clientSecretTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Client Secret',
|
||||
}
|
||||
)}
|
||||
<PasswordField
|
||||
path="secrets.clientSecret"
|
||||
label={i18n.CLIENT_SECRET_LABEL}
|
||||
readOnly={readOnly}
|
||||
helpText={
|
||||
<EuiLink
|
||||
href={docLinks.links.alerting.emailExchangeClientSecretConfig}
|
||||
|
@ -169,24 +99,8 @@ const ExchangeFormFields: React.FunctionComponent<ExchangeFormFieldsProps> = ({
|
|||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
isInvalid={isClientSecretInvalid}
|
||||
name="clientSecret"
|
||||
readOnly={readOnly}
|
||||
value={clientSecret || ''}
|
||||
data-test-subj="emailClientSecret"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('clientSecret', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!clientSecret) {
|
||||
editActionSecrets('clientSecret', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
data-test-subj="emailClientSecret"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
|
|
|
@ -7,6 +7,83 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const USERNAME_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Username',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Password',
|
||||
}
|
||||
);
|
||||
|
||||
export const FROM_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Sender',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICE_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.serviceTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Service',
|
||||
}
|
||||
);
|
||||
|
||||
export const TENANT_ID_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.tenantIdFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Tenant ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLIENT_ID_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.clientIdFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Client ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLIENT_SECRET_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.clientSecretTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Client Secret',
|
||||
}
|
||||
);
|
||||
|
||||
export const HOST_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Host',
|
||||
}
|
||||
);
|
||||
|
||||
export const PORT_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Port',
|
||||
}
|
||||
);
|
||||
|
||||
export const SECURE_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.secureSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Secure',
|
||||
}
|
||||
);
|
||||
|
||||
export const HAS_AUTH_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hasAuthSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Require authentication for this server',
|
||||
}
|
||||
);
|
||||
|
||||
export const SENDER_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText',
|
||||
{
|
||||
|
@ -14,13 +91,6 @@ export const SENDER_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SENDER_NOT_VALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText',
|
||||
{
|
||||
defaultMessage: 'Sender is not a valid email address.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLIENT_ID_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientIdText',
|
||||
{
|
||||
|
@ -35,13 +105,6 @@ export const TENANT_ID_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CLIENT_SECRET_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText',
|
||||
{
|
||||
defaultMessage: 'Client Secret is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PORT_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText',
|
||||
{
|
||||
|
@ -49,6 +112,13 @@ export const PORT_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PORT_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.invalidPortText',
|
||||
{
|
||||
defaultMessage: 'Port is invalid.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICE_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServiceText',
|
||||
{
|
||||
|
@ -70,20 +140,6 @@ export const USERNAME_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
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',
|
||||
{
|
||||
|
|
|
@ -5,112 +5,93 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { httpServiceMock, notificationServiceMock } from '@kbn/core/public/mocks';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { useEmailConfig } from './use_email_config';
|
||||
|
||||
const http = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
const editActionConfig = jest.fn();
|
||||
const http = httpServiceMock.createStartContract();
|
||||
const toasts = notificationServiceMock.createStartContract().toasts;
|
||||
|
||||
const renderUseEmailConfigHook = (currentService?: string) =>
|
||||
renderHook(() => useEmailConfig(http as unknown as HttpSetup, currentService, editActionConfig));
|
||||
renderHook(() => useEmailConfig({ http, toasts }));
|
||||
|
||||
describe('useEmailConfig', () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('should call get email config API when service changes and handle result', async () => {
|
||||
it('should return the correct result when requesting the config of a service', async () => {
|
||||
http.get.mockResolvedValueOnce({
|
||||
host: 'smtp.gmail.com',
|
||||
port: 465,
|
||||
secure: true,
|
||||
});
|
||||
const { result, waitForNextUpdate } = renderUseEmailConfigHook();
|
||||
|
||||
const { result } = renderUseEmailConfigHook();
|
||||
await act(async () => {
|
||||
result.current.setEmailService('gmail');
|
||||
await waitForNextUpdate();
|
||||
const res = await result.current.getEmailServiceConfig('gmail');
|
||||
expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail');
|
||||
expect(res).toEqual({
|
||||
host: 'smtp.gmail.com',
|
||||
port: 465,
|
||||
secure: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail');
|
||||
expect(editActionConfig).toHaveBeenCalledWith('service', 'gmail');
|
||||
|
||||
expect(editActionConfig).toHaveBeenCalledWith('host', 'smtp.gmail.com');
|
||||
expect(editActionConfig).toHaveBeenCalledWith('port', 465);
|
||||
expect(editActionConfig).toHaveBeenCalledWith('secure', true);
|
||||
|
||||
expect(result.current.emailServiceConfigurable).toEqual(false);
|
||||
});
|
||||
|
||||
it('should call get email config API when service changes and handle partial result', async () => {
|
||||
it('should return the correct result when requesting the config of a service on partial result', async () => {
|
||||
http.get.mockResolvedValueOnce({
|
||||
host: 'smtp.gmail.com',
|
||||
port: 465,
|
||||
});
|
||||
const { result, waitForNextUpdate } = renderUseEmailConfigHook();
|
||||
|
||||
const { result } = renderUseEmailConfigHook();
|
||||
await act(async () => {
|
||||
result.current.setEmailService('gmail');
|
||||
await waitForNextUpdate();
|
||||
const res = await result.current.getEmailServiceConfig('gmail');
|
||||
expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail');
|
||||
expect(res).toEqual({
|
||||
host: 'smtp.gmail.com',
|
||||
port: 465,
|
||||
secure: false,
|
||||
});
|
||||
});
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail');
|
||||
expect(editActionConfig).toHaveBeenCalledWith('service', 'gmail');
|
||||
|
||||
expect(editActionConfig).toHaveBeenCalledWith('host', 'smtp.gmail.com');
|
||||
expect(editActionConfig).toHaveBeenCalledWith('port', 465);
|
||||
expect(editActionConfig).toHaveBeenCalledWith('secure', false);
|
||||
|
||||
expect(result.current.emailServiceConfigurable).toEqual(false);
|
||||
});
|
||||
|
||||
it('should call get email config API when service changes and handle empty result', async () => {
|
||||
it('should return the correct result when requesting the config of a service on empty result', async () => {
|
||||
http.get.mockResolvedValueOnce({});
|
||||
const { result, waitForNextUpdate } = renderUseEmailConfigHook();
|
||||
const { result } = renderUseEmailConfigHook();
|
||||
|
||||
await act(async () => {
|
||||
result.current.setEmailService('foo');
|
||||
await waitForNextUpdate();
|
||||
const res = await result.current.getEmailServiceConfig('foo');
|
||||
expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/foo');
|
||||
expect(res).toEqual({
|
||||
host: '',
|
||||
port: 0,
|
||||
secure: false,
|
||||
});
|
||||
});
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/foo');
|
||||
expect(editActionConfig).toHaveBeenCalledWith('service', 'foo');
|
||||
|
||||
expect(editActionConfig).toHaveBeenCalledWith('host', '');
|
||||
expect(editActionConfig).toHaveBeenCalledWith('port', 0);
|
||||
expect(editActionConfig).toHaveBeenCalledWith('secure', false);
|
||||
|
||||
expect(result.current.emailServiceConfigurable).toEqual(true);
|
||||
});
|
||||
|
||||
it('should call get email config API when service changes and handle errors', async () => {
|
||||
it('should show a danger toaster on error', async () => {
|
||||
http.get.mockImplementationOnce(() => {
|
||||
throw new Error('no!');
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderUseEmailConfigHook();
|
||||
|
||||
await act(async () => {
|
||||
result.current.setEmailService('foo');
|
||||
result.current.getEmailServiceConfig('foo');
|
||||
await waitForNextUpdate();
|
||||
expect(toasts.addDanger).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/foo');
|
||||
expect(editActionConfig).toHaveBeenCalledWith('service', 'foo');
|
||||
|
||||
expect(editActionConfig).toHaveBeenCalledWith('host', '');
|
||||
expect(editActionConfig).toHaveBeenCalledWith('port', 0);
|
||||
expect(editActionConfig).toHaveBeenCalledWith('secure', false);
|
||||
|
||||
expect(result.current.emailServiceConfigurable).toEqual(true);
|
||||
});
|
||||
|
||||
it('should call get email config API when initial service value is passed and determine if config is editable without overwriting config', async () => {
|
||||
it('should not make an API call if the service is empty', async () => {
|
||||
http.get.mockResolvedValueOnce({
|
||||
host: 'smtp.gmail.com',
|
||||
port: 465,
|
||||
secure: true,
|
||||
});
|
||||
const { result } = renderUseEmailConfigHook('gmail');
|
||||
expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail');
|
||||
expect(editActionConfig).not.toHaveBeenCalled();
|
||||
expect(result.current.emailServiceConfigurable).toEqual(false);
|
||||
|
||||
renderUseEmailConfigHook('');
|
||||
expect(http.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,66 +5,95 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { HttpSetup, IToasts } from '@kbn/core/public';
|
||||
import { AdditionalEmailServices } from '@kbn/actions-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EmailConfig } from '../types';
|
||||
import { getServiceConfig } from './api';
|
||||
|
||||
export function useEmailConfig(
|
||||
http: HttpSetup,
|
||||
currentService: string | undefined,
|
||||
editActionConfig: (property: string, value: unknown) => void
|
||||
) {
|
||||
const [emailServiceConfigurable, setEmailServiceConfigurable] = useState<boolean>(false);
|
||||
const [emailService, setEmailService] = useState<string | undefined>(undefined);
|
||||
interface Props {
|
||||
http: HttpSetup;
|
||||
toasts: IToasts;
|
||||
}
|
||||
|
||||
interface UseEmailConfigReturnValue {
|
||||
isLoading: boolean;
|
||||
getEmailServiceConfig: (
|
||||
service: string
|
||||
) => Promise<Partial<Pick<EmailConfig, 'host' | 'port' | 'secure'>> | undefined>;
|
||||
}
|
||||
|
||||
const getConfig = (
|
||||
service: string,
|
||||
config: Partial<Pick<EmailConfig, 'host' | 'port' | 'secure'>>
|
||||
): Pick<EmailConfig, 'host' | 'port' | 'secure'> | undefined => {
|
||||
if (service) {
|
||||
if (service === AdditionalEmailServices.EXCHANGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
host: config?.host ? config.host : '',
|
||||
port: config?.port ? config.port : 0,
|
||||
secure: null != config?.secure ? config.secure : false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function useEmailConfig({ http, toasts }: Props): UseEmailConfigReturnValue {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const abortCtrlRef = useRef(new AbortController());
|
||||
const isMounted = useRef(false);
|
||||
|
||||
const getEmailServiceConfig = useCallback(
|
||||
async (service: string) => {
|
||||
let serviceConfig: Partial<Pick<EmailConfig, 'host' | 'port' | 'secure'>>;
|
||||
try {
|
||||
serviceConfig = await getServiceConfig({ http, service });
|
||||
setEmailServiceConfigurable(isEmpty(serviceConfig));
|
||||
} catch (err) {
|
||||
serviceConfig = {};
|
||||
setEmailServiceConfigurable(true);
|
||||
async (service: string | null) => {
|
||||
if (service == null || isEmpty(service)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return serviceConfig;
|
||||
setIsLoading(true);
|
||||
isMounted.current = true;
|
||||
abortCtrlRef.current.abort();
|
||||
abortCtrlRef.current = new AbortController();
|
||||
|
||||
try {
|
||||
const serviceConfig = await getServiceConfig({ http, service });
|
||||
if (isMounted.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return getConfig(service, serviceConfig);
|
||||
} catch (error) {
|
||||
if (isMounted.current) {
|
||||
setIsLoading(false);
|
||||
|
||||
if (error.name !== 'AbortError') {
|
||||
toasts.addDanger(
|
||||
error.body?.message ??
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.updateErrorNotificationText',
|
||||
{ defaultMessage: 'Cannot get service configuration' }
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[editActionConfig]
|
||||
[http, toasts]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (emailService) {
|
||||
editActionConfig('service', emailService);
|
||||
if (emailService === AdditionalEmailServices.EXCHANGE) {
|
||||
return;
|
||||
}
|
||||
const serviceConfig = await getEmailServiceConfig(emailService);
|
||||
|
||||
editActionConfig('host', serviceConfig?.host ? serviceConfig.host : '');
|
||||
editActionConfig('port', serviceConfig?.port ? serviceConfig.port : 0);
|
||||
editActionConfig('secure', null != serviceConfig?.secure ? serviceConfig.secure : false);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [emailService]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (currentService) {
|
||||
await getEmailServiceConfig(currentService);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentService]);
|
||||
isMounted.current = true;
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
abortCtrlRef.current.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
emailServiceConfigurable,
|
||||
setEmailService,
|
||||
isLoading,
|
||||
getEmailServiceConfig,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { EsIndexActionConnector } from '../types';
|
||||
import { registrationServicesMock } from '../../../../mocks';
|
||||
|
||||
const ACTION_TYPE_ID = '.index';
|
||||
|
@ -30,58 +29,6 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('index connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.index',
|
||||
name: 'es_index',
|
||||
config: {
|
||||
index: 'test_es_index',
|
||||
refresh: false,
|
||||
executionTimeField: '1',
|
||||
},
|
||||
} as EsIndexActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
index: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('index connector validation with minimal config', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.index',
|
||||
name: 'es_index',
|
||||
config: {
|
||||
index: 'test_es_index',
|
||||
},
|
||||
} as EsIndexActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
index: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('action params validation', () => {
|
||||
test('action params validation succeeds when action params are valid', async () => {
|
||||
expect(
|
||||
|
|
|
@ -7,13 +7,8 @@
|
|||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
GenericValidationResult,
|
||||
ConnectorValidationResult,
|
||||
ALERT_HISTORY_PREFIX,
|
||||
} from '../../../../types';
|
||||
import { EsIndexActionConnector, EsIndexConfig, IndexActionParams } from '../types';
|
||||
import { ActionTypeModel, GenericValidationResult, ALERT_HISTORY_PREFIX } from '../../../../types';
|
||||
import { EsIndexConfig, IndexActionParams } from '../types';
|
||||
|
||||
export function getActionType(): ActionTypeModel<EsIndexConfig, unknown, IndexActionParams> {
|
||||
return {
|
||||
|
@ -31,19 +26,6 @@ export function getActionType(): ActionTypeModel<EsIndexConfig, unknown, IndexAc
|
|||
defaultMessage: 'Index data',
|
||||
}
|
||||
),
|
||||
validateConnector: async (
|
||||
action: EsIndexActionConnector
|
||||
): 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(translations.INDEX_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
actionConnectorFields: lazy(() => import('./es_index_connector')),
|
||||
actionParamsFields: lazy(() => import('./es_index_params')),
|
||||
validateParams: async (
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EsIndexActionConnector } from '../types';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { screen, fireEvent, waitFor, render } from '@testing-library/react';
|
||||
import IndexActionConnectorFields from './es_index_connector';
|
||||
import { EuiComboBox, EuiSwitch, EuiSwitchEvent, EuiSelect } from '@elastic/eui';
|
||||
import { screen, render, fireEvent } from '@testing-library/react';
|
||||
import { AppMockRenderer, ConnectorFormTestProvider, createAppMockRenderer } from '../test_utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
jest.mock('lodash', () => {
|
||||
|
@ -23,7 +25,10 @@ jest.mock('lodash', () => {
|
|||
});
|
||||
|
||||
jest.mock('../../../../common/index_controls', () => ({
|
||||
firstFieldOption: jest.fn(),
|
||||
firstFieldOption: {
|
||||
text: 'Select a field',
|
||||
value: '',
|
||||
},
|
||||
getFields: jest.fn(),
|
||||
getIndexOptions: jest.fn(),
|
||||
}));
|
||||
|
@ -42,12 +47,22 @@ getIndexOptions.mockResolvedValueOnce([
|
|||
|
||||
const { getFields } = jest.requireMock('../../../../common/index_controls');
|
||||
|
||||
async function setup(props: any) {
|
||||
const wrapper = mountWithIntl(<IndexActionConnectorFields {...props} />);
|
||||
async function setup(actionConnector: any) {
|
||||
const wrapper = mountWithIntl(
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<IndexActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
|
@ -64,22 +79,16 @@ function setupGetFieldsResponse(getFieldsWithDateMapping: boolean) {
|
|||
]);
|
||||
}
|
||||
|
||||
describe('IndexActionConnectorFields renders', () => {
|
||||
describe('IndexActionConnectorFields', () => {
|
||||
test('renders correctly when creating connector', async () => {
|
||||
const props = {
|
||||
action: {
|
||||
actionTypeId: '.index',
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as EsIndexActionConnector,
|
||||
editActionConfig: () => {},
|
||||
editActionSecrets: () => {},
|
||||
errors: { index: [] },
|
||||
readOnly: false,
|
||||
setCallbacks: () => {},
|
||||
isEdit: false,
|
||||
const connector = {
|
||||
actionTypeId: '.index',
|
||||
config: {},
|
||||
secrets: {},
|
||||
};
|
||||
const wrapper = mountWithIntl(<IndexActionConnectorFields {...props} />);
|
||||
|
||||
setupGetFieldsResponse(false);
|
||||
const wrapper = await setup(connector);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy();
|
||||
|
@ -95,34 +104,41 @@ describe('IndexActionConnectorFields renders', () => {
|
|||
// time field switch should show up if index has date type field mapping
|
||||
setupGetFieldsResponse(true);
|
||||
await act(async () => {
|
||||
indexComboBox.prop('onChange')!([{ label: 'selection' }]);
|
||||
indexComboBox.prop('onChange')!([{ label: 'selection', value: 'selection' }]);
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy();
|
||||
|
||||
// time field switch should show up if index has date type field mapping
|
||||
// time field switch should go away if index does not has date type field mapping
|
||||
setupGetFieldsResponse(false);
|
||||
await act(async () => {
|
||||
indexComboBox.prop('onChange')!([{ label: 'selection' }]);
|
||||
indexComboBox.prop('onChange')!([{ label: 'selection', value: 'selection' }]);
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy();
|
||||
|
||||
// time field dropdown should show up if index has date type field mapping and time switch is clicked
|
||||
setupGetFieldsResponse(true);
|
||||
await act(async () => {
|
||||
indexComboBox.prop('onChange')!([{ label: 'selection' }]);
|
||||
indexComboBox.prop('onChange')!([{ label: 'selection', value: 'selection' }]);
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy();
|
||||
|
||||
const timeFieldSwitch = wrapper
|
||||
.find(EuiSwitch)
|
||||
.filter('[data-test-subj="hasTimeFieldCheckbox"]');
|
||||
|
||||
await act(async () => {
|
||||
timeFieldSwitch.prop('onChange')!({
|
||||
target: { checked: true },
|
||||
|
@ -130,26 +146,25 @@ describe('IndexActionConnectorFields renders', () => {
|
|||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy();
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('renders correctly when editing connector - no date type field mapping', async () => {
|
||||
const indexName = 'index-no-date-fields';
|
||||
const props = {
|
||||
action: {
|
||||
name: 'Index Connector for Index With No Date Type',
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: indexName,
|
||||
refresh: false,
|
||||
},
|
||||
secrets: {},
|
||||
} as EsIndexActionConnector,
|
||||
editActionConfig: () => {},
|
||||
editActionSecrets: () => {},
|
||||
errors: { index: [] },
|
||||
readOnly: false,
|
||||
name: 'Index Connector for Index With No Date Type',
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: indexName,
|
||||
refresh: false,
|
||||
},
|
||||
secrets: {},
|
||||
};
|
||||
|
||||
setupGetFieldsResponse(false);
|
||||
const wrapper = await setup(props);
|
||||
|
||||
|
@ -172,20 +187,15 @@ describe('IndexActionConnectorFields renders', () => {
|
|||
test('renders correctly when editing connector - refresh set to true', async () => {
|
||||
const indexName = 'index-no-date-fields';
|
||||
const props = {
|
||||
action: {
|
||||
name: 'Index Connector for Index With No Date Type',
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: indexName,
|
||||
refresh: true,
|
||||
},
|
||||
secrets: {},
|
||||
} as EsIndexActionConnector,
|
||||
editActionConfig: () => {},
|
||||
editActionSecrets: () => {},
|
||||
errors: { index: [] },
|
||||
readOnly: false,
|
||||
name: 'Index Connector for Index With No Date Type',
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: indexName,
|
||||
refresh: true,
|
||||
},
|
||||
secrets: {},
|
||||
};
|
||||
|
||||
setupGetFieldsResponse(false);
|
||||
const wrapper = await setup(props);
|
||||
|
||||
|
@ -206,27 +216,24 @@ describe('IndexActionConnectorFields renders', () => {
|
|||
test('renders correctly when editing connector - with date type field mapping but no time field selected', async () => {
|
||||
const indexName = 'index-no-date-fields';
|
||||
const props = {
|
||||
action: {
|
||||
name: 'Index Connector for Index With No Date Type',
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: indexName,
|
||||
refresh: false,
|
||||
},
|
||||
secrets: {},
|
||||
} as EsIndexActionConnector,
|
||||
editActionConfig: () => {},
|
||||
editActionSecrets: () => {},
|
||||
errors: { index: [] },
|
||||
readOnly: false,
|
||||
name: 'Index Connector for Index With No Date Type',
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: indexName,
|
||||
refresh: false,
|
||||
},
|
||||
secrets: {},
|
||||
};
|
||||
|
||||
setupGetFieldsResponse(true);
|
||||
const wrapper = await setup(props);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy();
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
const indexComboBox = wrapper
|
||||
.find(EuiComboBox)
|
||||
|
@ -245,28 +252,25 @@ describe('IndexActionConnectorFields renders', () => {
|
|||
test('renders correctly when editing connector - with date type field mapping and selected time field', async () => {
|
||||
const indexName = 'index-no-date-fields';
|
||||
const props = {
|
||||
action: {
|
||||
name: 'Index Connector for Index With No Date Type',
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: indexName,
|
||||
refresh: false,
|
||||
executionTimeField: 'test1',
|
||||
},
|
||||
secrets: {},
|
||||
} as EsIndexActionConnector,
|
||||
editActionConfig: () => {},
|
||||
editActionSecrets: () => {},
|
||||
errors: { index: [] },
|
||||
readOnly: false,
|
||||
name: 'Index Connector for Index With No Date Type',
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: indexName,
|
||||
refresh: false,
|
||||
executionTimeField: 'test1',
|
||||
},
|
||||
};
|
||||
|
||||
setupGetFieldsResponse(true);
|
||||
const wrapper = await setup(props);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
const indexComboBox = wrapper
|
||||
.find(EuiComboBox)
|
||||
|
@ -289,20 +293,23 @@ describe('IndexActionConnectorFields renders', () => {
|
|||
|
||||
test('fetches index names on index combobox input change', async () => {
|
||||
const mockIndexName = 'test-index';
|
||||
const props = {
|
||||
action: {
|
||||
actionTypeId: '.index',
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as EsIndexActionConnector,
|
||||
editActionConfig: () => {},
|
||||
editActionSecrets: () => {},
|
||||
errors: { index: [] },
|
||||
readOnly: false,
|
||||
setCallbacks: () => {},
|
||||
isEdit: false,
|
||||
const connector = {
|
||||
actionTypeId: '.index',
|
||||
name: 'index',
|
||||
isDeprecated: false,
|
||||
config: {},
|
||||
secrets: {},
|
||||
};
|
||||
render(<IndexActionConnectorFields {...props} />);
|
||||
|
||||
render(
|
||||
<ConnectorFormTestProvider connector={connector}>
|
||||
<IndexActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
const indexComboBox = await screen.findByTestId('connectorIndexesComboBox');
|
||||
|
||||
|
@ -322,4 +329,134 @@ describe('IndexActionConnectorFields renders', () => {
|
|||
expect(screen.getByText('indexPattern1')).toBeInTheDocument();
|
||||
expect(screen.getByText('indexPattern2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
let appMockRenderer: AppMockRenderer;
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
appMockRenderer = createAppMockRenderer();
|
||||
});
|
||||
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.index',
|
||||
name: 'es_index',
|
||||
config: {
|
||||
index: 'test_es_index',
|
||||
refresh: false,
|
||||
executionTimeField: '1',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<IndexActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: 'test_es_index',
|
||||
refresh: false,
|
||||
executionTimeField: '1',
|
||||
},
|
||||
id: 'test',
|
||||
isDeprecated: false,
|
||||
__internal__: {
|
||||
hasTimeFieldCheckbox: true,
|
||||
},
|
||||
name: 'es_index',
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation succeeds when connector config is valid with minimal config', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.index',
|
||||
name: 'es_index',
|
||||
config: {
|
||||
index: 'test_es_index',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<IndexActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.index',
|
||||
config: {
|
||||
index: 'test_es_index',
|
||||
refresh: false,
|
||||
},
|
||||
id: 'test',
|
||||
isDeprecated: false,
|
||||
name: 'es_index',
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when index is empty', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.index',
|
||||
name: 'es_index',
|
||||
config: {
|
||||
index: '',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<IndexActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {},
|
||||
isValid: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,54 +6,95 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiSwitch,
|
||||
EuiSpacer,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiSelect,
|
||||
EuiTitle,
|
||||
EuiIconTip,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
FieldConfig,
|
||||
getFieldValidityAndErrorMessage,
|
||||
UseField,
|
||||
useFormContext,
|
||||
useFormData,
|
||||
VALIDATION_TYPES,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { ToggleField, SelectField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { DocLinksStart } from '@kbn/core/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { debounce } from 'lodash';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { EsIndexActionConnector } from '../types';
|
||||
import { getTimeFieldOptions } from '../../../../common/lib/get_time_options';
|
||||
import { firstFieldOption, getFields, getIndexOptions } from '../../../../common/index_controls';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import * as translations from './translations';
|
||||
|
||||
interface TimeFieldOptions {
|
||||
value: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const IndexActionConnectorFields: React.FunctionComponent<
|
||||
ActionConnectorFieldsProps<EsIndexActionConnector>
|
||||
> = ({ action, editActionConfig, errors, readOnly }) => {
|
||||
const { indexPatternField, emptyField } = fieldValidators;
|
||||
|
||||
const getIndexConfig = (docLinks: DocLinksStart): FieldConfig => ({
|
||||
label: translations.INDEX_LABEL,
|
||||
helpText: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.howToBroadenSearchQueryDescription"
|
||||
defaultMessage="Use * to broaden your query."
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLink href={docLinks.links.alerting.indexAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.configureIndexHelpLabel"
|
||||
defaultMessage="Configuring index connector."
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(translations.INDEX_IS_NOT_VALID),
|
||||
},
|
||||
{
|
||||
validator: indexPatternField(i18n),
|
||||
type: VALIDATION_TYPES.ARRAY_ITEM,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const IndexActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
readOnly,
|
||||
}) => {
|
||||
const { http, docLinks } = useKibana().services;
|
||||
const { index, refresh, executionTimeField } = action.config;
|
||||
const [showTimeFieldCheckbox, setShowTimeFieldCheckboxState] = useState<boolean>(
|
||||
executionTimeField != null
|
||||
);
|
||||
const [hasTimeFieldCheckbox, setHasTimeFieldCheckboxState] = useState<boolean>(
|
||||
executionTimeField != null
|
||||
);
|
||||
const { getFieldDefaultValue } = useFormContext();
|
||||
const [{ config, __internal__ }] = useFormData({
|
||||
watch: ['config.executionTimeField', 'config.index', '__internal__.hasTimeFieldCheckbox'],
|
||||
});
|
||||
|
||||
const { index = null } = config ?? {};
|
||||
|
||||
const [indexOptions, setIndexOptions] = useState<EuiComboBoxOptionOption[]>([]);
|
||||
const [timeFieldOptions, setTimeFieldOptions] = useState<TimeFieldOptions[]>([]);
|
||||
const [areIndiciesLoading, setAreIndicesLoading] = useState<boolean>(false);
|
||||
|
||||
const hasTimeFieldCheckboxDefaultValue = !!getFieldDefaultValue<string | undefined>(
|
||||
'config.executionTimeField'
|
||||
);
|
||||
const showTimeFieldCheckbox = index != null && timeFieldOptions.length > 0;
|
||||
const showTimeFieldSelect = __internal__ != null ? __internal__.hasTimeFieldCheckbox : false;
|
||||
|
||||
const setTimeFields = (fields: TimeFieldOptions[]) => {
|
||||
if (fields.length > 0) {
|
||||
setShowTimeFieldCheckboxState(true);
|
||||
setTimeFieldOptions([firstFieldOption, ...fields]);
|
||||
} else {
|
||||
setHasTimeFieldCheckboxState(false);
|
||||
setShowTimeFieldCheckboxState(false);
|
||||
setTimeFieldOptions([]);
|
||||
}
|
||||
};
|
||||
|
@ -68,14 +109,13 @@ const IndexActionConnectorFields: React.FunctionComponent<
|
|||
const indexPatternsFunction = async () => {
|
||||
if (index) {
|
||||
const currentEsFields = await getFields(http!, [index]);
|
||||
setTimeFields(getTimeFieldOptions(currentEsFields as any));
|
||||
if (Array.isArray(currentEsFields)) {
|
||||
setTimeFields(getTimeFieldOptions(currentEsFields as any));
|
||||
}
|
||||
}
|
||||
};
|
||||
indexPatternsFunction();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const isIndexInvalid: boolean =
|
||||
errors.index !== undefined && errors.index.length > 0 && index !== undefined;
|
||||
}, [http, index]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -88,57 +128,13 @@ const IndexActionConnectorFields: React.FunctionComponent<
|
|||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
id="indexConnectorSelectSearchBox"
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indicesToQueryLabel"
|
||||
defaultMessage="Index"
|
||||
/>
|
||||
}
|
||||
isInvalid={isIndexInvalid}
|
||||
error={errors.index}
|
||||
helpText={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.howToBroadenSearchQueryDescription"
|
||||
defaultMessage="Use * to broaden your query."
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLink href={docLinks.links.alerting.indexAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.configureIndexHelpLabel"
|
||||
defaultMessage="Configuring index connector."
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
singleSelection={{ asPlainText: true }}
|
||||
async
|
||||
isLoading={areIndiciesLoading}
|
||||
isInvalid={isIndexInvalid}
|
||||
noSuggestions={!indexOptions.length}
|
||||
options={indexOptions}
|
||||
data-test-subj="connectorIndexesComboBox"
|
||||
data-testid="connectorIndexesComboBox"
|
||||
selectedOptions={
|
||||
index
|
||||
? [
|
||||
{
|
||||
value: index,
|
||||
label: index,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
isDisabled={readOnly}
|
||||
onChange={async (selected: EuiComboBoxOptionOption[]) => {
|
||||
editActionConfig('index', selected.length > 0 ? selected[0].value : '');
|
||||
const indices = selected.map((s) => s.value as string);
|
||||
<UseField path="config.index" config={getIndexConfig(docLinks)}>
|
||||
{(field) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
const onComboChange = async (options: EuiComboBoxOptionOption[]) => {
|
||||
field.setValue(options.length > 0 ? options[0].value : '');
|
||||
const indices = options.map((s) => s.value as string);
|
||||
|
||||
// reset time field and expression fields if indices are deleted
|
||||
if (indices.length === 0) {
|
||||
|
@ -147,117 +143,149 @@ const IndexActionConnectorFields: React.FunctionComponent<
|
|||
}
|
||||
const currentEsFields = await getFields(http!, indices);
|
||||
setTimeFields(getTimeFieldOptions(currentEsFields as any));
|
||||
}}
|
||||
onSearchChange={loadIndexOptions}
|
||||
onBlur={() => {
|
||||
if (!index) {
|
||||
editActionConfig('index', '');
|
||||
};
|
||||
|
||||
const onSearchComboChange = (value: string) => {
|
||||
if (value !== undefined) {
|
||||
field.clearErrors(VALIDATION_TYPES.ARRAY_ITEM);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSwitch
|
||||
data-test-subj="indexRefreshCheckbox"
|
||||
checked={refresh || false}
|
||||
disabled={readOnly}
|
||||
onChange={(e) => {
|
||||
editActionConfig('refresh', e.target.checked);
|
||||
}}
|
||||
label={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshLabel"
|
||||
defaultMessage="Refresh index"
|
||||
/>{' '}
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
type="questionInCircle"
|
||||
content={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Refresh the affected shards to make this operation visible to search.',
|
||||
|
||||
loadIndexOptions(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
id="indexConnectorSelectSearchBox"
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indicesToQueryLabel"
|
||||
defaultMessage="Index"
|
||||
/>
|
||||
}
|
||||
isInvalid={isInvalid}
|
||||
error={errorMessage}
|
||||
helpText={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.howToBroadenSearchQueryDescription"
|
||||
defaultMessage="Use * to broaden your query."
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLink href={docLinks.links.alerting.indexAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.configureIndexHelpLabel"
|
||||
defaultMessage="Configuring index connector."
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
singleSelection={{ asPlainText: true }}
|
||||
async
|
||||
isLoading={areIndiciesLoading}
|
||||
isInvalid={isInvalid}
|
||||
noSuggestions={!indexOptions.length}
|
||||
options={indexOptions}
|
||||
data-test-subj="connectorIndexesComboBox"
|
||||
data-testid="connectorIndexesComboBox"
|
||||
selectedOptions={
|
||||
index
|
||||
? [
|
||||
{
|
||||
value: index,
|
||||
label: index,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
isDisabled={readOnly}
|
||||
onChange={onComboChange}
|
||||
onSearchChange={onSearchComboChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
<EuiSpacer size="m" />
|
||||
<UseField
|
||||
path="config.refresh"
|
||||
component={ToggleField}
|
||||
config={{
|
||||
defaultValue: false,
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
label: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshLabel"
|
||||
defaultMessage="Refresh index"
|
||||
/>{' '}
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
type="questionInCircle"
|
||||
content={translations.REFRESH_FIELD_TOGGLE_TOOLTIP}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
disabled: readOnly,
|
||||
'data-test-subj': 'indexRefreshCheckbox',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{showTimeFieldCheckbox && (
|
||||
<EuiSwitch
|
||||
data-test-subj="hasTimeFieldCheckbox"
|
||||
checked={hasTimeFieldCheckbox || false}
|
||||
disabled={readOnly}
|
||||
onChange={() => {
|
||||
setHasTimeFieldCheckboxState(!hasTimeFieldCheckbox);
|
||||
// if changing from checked to not checked (hasTimeField === true),
|
||||
// set time field to null
|
||||
if (hasTimeFieldCheckbox) {
|
||||
editActionConfig('executionTimeField', null);
|
||||
}
|
||||
{showTimeFieldCheckbox ? (
|
||||
<UseField
|
||||
path="__internal__.hasTimeFieldCheckbox"
|
||||
component={ToggleField}
|
||||
config={{ defaultValue: hasTimeFieldCheckboxDefaultValue }}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
label: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.defineTimeFieldLabel"
|
||||
defaultMessage="Define time field for each document"
|
||||
/>
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
type="questionInCircle"
|
||||
content={translations.SHOW_TIME_FIELD_TOGGLE_TOOLTIP}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
disabled: readOnly,
|
||||
'data-test-subj': 'hasTimeFieldCheckbox',
|
||||
},
|
||||
}}
|
||||
label={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.defineTimeFieldLabel"
|
||||
defaultMessage="Define time field for each document"
|
||||
/>
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
type="questionInCircle"
|
||||
content={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.definedateFieldTooltip',
|
||||
{
|
||||
defaultMessage: `Set this time field to the time the document was indexed.`,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{hasTimeFieldCheckbox && (
|
||||
) : null}
|
||||
{showTimeFieldSelect ? (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
id="executionTimeField"
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.executionTimeFieldLabel"
|
||||
defaultMessage="Time field"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiSelect
|
||||
options={timeFieldOptions}
|
||||
fullWidth
|
||||
name="executionTimeField"
|
||||
data-test-subj="executionTimeFieldSelect"
|
||||
value={executionTimeField ?? ''}
|
||||
onChange={(e) => {
|
||||
editActionConfig('executionTimeField', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (executionTimeField === undefined) {
|
||||
editActionConfig('executionTimeField', null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path="config.executionTimeField"
|
||||
component={SelectField}
|
||||
config={{
|
||||
label: translations.EXECUTION_TIME_LABEL,
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'executionTimeFieldSelect',
|
||||
options: timeFieldOptions,
|
||||
fullWidth: true,
|
||||
readOnly,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// if the string == null or is empty, return null, else return string
|
||||
function nullableString(str: string | null | undefined) {
|
||||
if (str == null || str.trim() === '') return null;
|
||||
return str;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { IndexActionConnectorFields as default };
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const INDEX_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText',
|
||||
export const INDEX_IS_NOT_VALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.notValidIndexText',
|
||||
{
|
||||
defaultMessage: 'Index is required.',
|
||||
defaultMessage: 'Index is not valid.',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -27,3 +27,31 @@ export const HISTORY_NOT_VALID = i18n.translate(
|
|||
defaultMessage: 'Alert history index must contain valid suffix.',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXECUTION_TIME_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.executionTimeFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Time field',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOW_TIME_FIELD_TOGGLE_TOOLTIP = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.definedateFieldTooltip',
|
||||
{
|
||||
defaultMessage: `Set this time field to the time the document was indexed.`,
|
||||
}
|
||||
);
|
||||
|
||||
export const REFRESH_FIELD_TOGGLE_TOOLTIP = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshTooltip',
|
||||
{
|
||||
defaultMessage: 'Refresh the affected shards to make this operation visible to search.',
|
||||
}
|
||||
);
|
||||
|
||||
export const INDEX_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indicesToQueryLabel',
|
||||
{
|
||||
defaultMessage: 'Index',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { JiraActionConnector } from './types';
|
||||
import { registrationServicesMock } from '../../../../mocks';
|
||||
|
||||
const ACTION_TYPE_ID = '.jira';
|
||||
|
@ -29,68 +28,6 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('jira connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
email: 'email',
|
||||
apiToken: 'apiToken',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.jira',
|
||||
name: 'jira',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
apiUrl: 'https://siem-kibana.atlassian.net',
|
||||
projectKey: 'CK',
|
||||
},
|
||||
} as JiraActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
projectKey: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
apiToken: [],
|
||||
email: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
email: 'user',
|
||||
},
|
||||
id: '.jira',
|
||||
actionTypeId: '.jira',
|
||||
name: 'jira',
|
||||
config: {},
|
||||
} as unknown as JiraActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: ['URL is required.'],
|
||||
projectKey: ['Project key is required'],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
apiToken: ['API token is required'],
|
||||
email: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('jira action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
|
|
|
@ -7,58 +7,8 @@
|
|||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
GenericValidationResult,
|
||||
ActionTypeModel,
|
||||
ConnectorValidationResult,
|
||||
} from '../../../../types';
|
||||
import { JiraActionConnector, JiraConfig, JiraSecrets, JiraActionParams } from './types';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
|
||||
const validateConnector = async (
|
||||
action: JiraActionConnector
|
||||
): Promise<ConnectorValidationResult<JiraConfig, JiraSecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
apiUrl: new Array<string>(),
|
||||
projectKey: new Array<string>(),
|
||||
};
|
||||
const secretsErrors = {
|
||||
email: new Array<string>(),
|
||||
apiToken: new Array<string>(),
|
||||
};
|
||||
|
||||
const validationResult = {
|
||||
config: { errors: configErrors },
|
||||
secrets: { errors: secretsErrors },
|
||||
};
|
||||
|
||||
if (!action.config.apiUrl) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED];
|
||||
}
|
||||
|
||||
if (action.config.apiUrl) {
|
||||
if (!isValidUrl(action.config.apiUrl)) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID];
|
||||
} else if (!isValidUrl(action.config.apiUrl, 'https:')) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS];
|
||||
}
|
||||
}
|
||||
|
||||
if (!action.config.projectKey) {
|
||||
configErrors.projectKey = [...configErrors.projectKey, translations.JIRA_PROJECT_KEY_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.email) {
|
||||
secretsErrors.email = [...secretsErrors.email, translations.JIRA_EMAIL_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.apiToken) {
|
||||
secretsErrors.apiToken = [...secretsErrors.apiToken, translations.JIRA_API_TOKEN_REQUIRED];
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
};
|
||||
import { GenericValidationResult, ActionTypeModel } from '../../../../types';
|
||||
import { JiraConfig, JiraSecrets, JiraActionParams } from './types';
|
||||
|
||||
export const JIRA_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText',
|
||||
|
@ -80,7 +30,6 @@ export function getActionType(): ActionTypeModel<JiraConfig, JiraSecrets, JiraAc
|
|||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: JIRA_DESC,
|
||||
actionTypeTitle: JIRA_TITLE,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./jira_connectors')),
|
||||
validateParams: async (
|
||||
actionParams: JiraActionParams
|
||||
|
|
|
@ -8,168 +8,141 @@
|
|||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import JiraConnectorFields from './jira_connectors';
|
||||
import { JiraActionConnector } from './types';
|
||||
import { ConnectorFormTestProvider } from '../test_utils';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
describe('JiraActionConnectorFields renders', () => {
|
||||
test('alerting Jira connector fields are rendered', () => {
|
||||
test('Jira connector fields are rendered', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.jira',
|
||||
name: 'jira',
|
||||
config: {
|
||||
apiUrl: 'https://test.com',
|
||||
projectKey: 'CK',
|
||||
},
|
||||
secrets: {
|
||||
email: 'email',
|
||||
apiToken: 'token',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.jira',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'jira',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
projectKey: 'CK',
|
||||
},
|
||||
} as JiraActionConnector;
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<JiraConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], email: [], apiToken: [], projectKey: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<JiraConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-jira-project-key-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-jira-email-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="config.apiUrl-input"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="config.projectKey-input"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="secrets.email-input"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="secrets.apiToken-input"]').length > 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('case specific Jira connector fields is rendered', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
email: 'email',
|
||||
apiToken: 'token',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.jira',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'jira',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
projectKey: 'CK',
|
||||
},
|
||||
} as JiraActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<JiraConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], email: [], apiToken: [], projectKey: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
consumer={'case'}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-jira-project-key-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-jira-email-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
});
|
||||
const tests: Array<[string, string]> = [
|
||||
['config.apiUrl-input', 'not-valid'],
|
||||
['config.projectKey-input', ''],
|
||||
['secrets.email-input', ''],
|
||||
['secrets.apiToken-input', ''],
|
||||
];
|
||||
|
||||
test('should display a message on create to remember credentials', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.jira',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
secrets: {},
|
||||
config: {},
|
||||
} as JiraActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<JiraConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], email: [], apiToken: [], projectKey: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
it('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.jira',
|
||||
name: 'jira',
|
||||
config: {
|
||||
apiUrl: 'https://test.com',
|
||||
projectKey: 'CK',
|
||||
},
|
||||
secrets: {
|
||||
email: 'email',
|
||||
apiToken: 'token',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
test('should display a message when secrets is missing', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.jira',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: true,
|
||||
secrets: {},
|
||||
config: {},
|
||||
} as JiraActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<JiraConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], email: [], apiToken: [], projectKey: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0);
|
||||
});
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<JiraConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
test('should display a message on edit to re-enter credentials', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
email: 'email',
|
||||
apiToken: 'token',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.jira',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'jira',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
projectKey: 'CK',
|
||||
},
|
||||
} as JiraActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<JiraConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], email: [], apiToken: [], projectKey: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.jira',
|
||||
name: 'jira',
|
||||
config: {
|
||||
apiUrl: 'https://test.com',
|
||||
projectKey: 'CK',
|
||||
},
|
||||
secrets: {
|
||||
email: 'email',
|
||||
apiToken: 'token',
|
||||
},
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(tests)('validates correctly %p', async (field, value) => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.jira',
|
||||
name: 'jira',
|
||||
config: {
|
||||
apiUrl: 'https://test.com',
|
||||
projectKey: 'CK',
|
||||
},
|
||||
secrets: {
|
||||
email: 'email',
|
||||
apiToken: 'token',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<JiraConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,182 +5,34 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiFieldPassword,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { JiraActionConnector } from './types';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label';
|
||||
import {
|
||||
ConfigFieldSchema,
|
||||
SimpleConnectorForm,
|
||||
SecretsFieldSchema,
|
||||
} from '../../simple_connector_form';
|
||||
|
||||
const JiraConnectorFields: React.FC<ActionConnectorFieldsProps<JiraActionConnector>> = ({
|
||||
action,
|
||||
editActionSecrets,
|
||||
editActionConfig,
|
||||
errors,
|
||||
readOnly,
|
||||
}) => {
|
||||
const { apiUrl, projectKey } = action.config;
|
||||
|
||||
const isApiUrlInvalid: boolean =
|
||||
apiUrl !== undefined && errors.apiUrl !== undefined && errors.apiUrl.length > 0;
|
||||
|
||||
const { email, apiToken } = action.secrets;
|
||||
|
||||
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),
|
||||
[editActionConfig]
|
||||
);
|
||||
|
||||
const handleOnChangeSecretConfig = useCallback(
|
||||
(key: string, value: string) => editActionSecrets(key, value),
|
||||
[editActionSecrets]
|
||||
);
|
||||
|
||||
const handleResetField = useCallback(
|
||||
(checkValue, fieldName: string, actionField: 'config' | 'secrets') => {
|
||||
if (!checkValue) {
|
||||
if (actionField === 'config') {
|
||||
handleOnChangeActionConfig(fieldName, '');
|
||||
} else {
|
||||
handleOnChangeSecretConfig(fieldName, '');
|
||||
}
|
||||
}
|
||||
},
|
||||
[handleOnChangeActionConfig, handleOnChangeSecretConfig]
|
||||
);
|
||||
const configFormSchema: ConfigFieldSchema[] = [
|
||||
{ id: 'apiUrl', label: i18n.API_URL_LABEL, isUrlField: true },
|
||||
{ id: 'projectKey', label: i18n.JIRA_PROJECT_KEY_LABEL },
|
||||
];
|
||||
const secretsFormSchema: SecretsFieldSchema[] = [
|
||||
{ id: 'email', label: i18n.JIRA_EMAIL_LABEL },
|
||||
{ id: 'apiToken', label: i18n.JIRA_API_TOKEN_LABEL, isPasswordField: true },
|
||||
];
|
||||
|
||||
const JiraConnectorFields: React.FC<ActionConnectorFieldsProps> = ({ readOnly, isEdit }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="apiUrl"
|
||||
fullWidth
|
||||
error={errors.apiUrl}
|
||||
isInvalid={isApiUrlInvalid}
|
||||
label={i18n.API_URL_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isApiUrlInvalid}
|
||||
name="apiUrl"
|
||||
readOnly={readOnly}
|
||||
value={apiUrl || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="apiUrlFromInput"
|
||||
onChange={(evt) => handleOnChangeActionConfig('apiUrl', evt.target.value)}
|
||||
onBlur={() => handleResetField(apiUrl, 'apiUrl', 'config')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-jira-project-key"
|
||||
fullWidth
|
||||
error={errors.projectKey}
|
||||
isInvalid={isProjectKeyInvalid}
|
||||
label={i18n.JIRA_PROJECT_KEY_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isProjectKeyInvalid}
|
||||
name="connector-jira-project-key"
|
||||
value={projectKey || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-jira-project-key-form-input"
|
||||
onChange={(evt) => handleOnChangeActionConfig('projectKey', evt.target.value)}
|
||||
onBlur={() => handleResetField(projectKey, 'projectKey', 'config')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xxs">
|
||||
<h4>{i18n.JIRA_AUTHENTICATION_LABEL}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
2,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.JIRA_REENTER_VALUES_LABEL
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-jira-email"
|
||||
fullWidth
|
||||
error={errors.email}
|
||||
isInvalid={isEmailInvalid}
|
||||
label={i18n.JIRA_EMAIL_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isEmailInvalid}
|
||||
readOnly={readOnly}
|
||||
name="connector-jira-email"
|
||||
value={email || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-jira-email-form-input"
|
||||
onChange={(evt) => handleOnChangeSecretConfig('email', evt.target.value)}
|
||||
onBlur={() => handleResetField(email, 'email', 'secrets')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-jira-apiToken"
|
||||
fullWidth
|
||||
error={errors.apiToken}
|
||||
isInvalid={isApiTokenInvalid}
|
||||
label={i18n.JIRA_API_TOKEN_LABEL}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={isApiTokenInvalid}
|
||||
name="connector-jira-apiToken"
|
||||
value={apiToken || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-jira-apiToken-form-input"
|
||||
onChange={(evt) => handleOnChangeSecretConfig('apiToken', evt.target.value)}
|
||||
onBlur={() => handleResetField(apiToken, 'apiToken', 'secrets')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
<SimpleConnectorForm
|
||||
isEdit={isEdit}
|
||||
readOnly={readOnly}
|
||||
configFormSchema={configFormSchema}
|
||||
secretsFormSchema={secretsFormSchema}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -14,27 +14,6 @@ export const API_URL_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const API_URL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is invalid.',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_REQUIRE_HTTPS = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.requireHttpsApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL must start with https://.',
|
||||
}
|
||||
);
|
||||
|
||||
export const JIRA_PROJECT_KEY_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey',
|
||||
{
|
||||
|
@ -42,28 +21,6 @@ export const JIRA_PROJECT_KEY_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField',
|
||||
{
|
||||
defaultMessage: 'Project key is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const JIRA_AUTHENTICATION_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.authenticationLabel',
|
||||
{
|
||||
defaultMessage: 'Authentication',
|
||||
}
|
||||
);
|
||||
|
||||
export const JIRA_REENTER_VALUES_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.reenterValuesLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Authentication credentials are encrypted. Please reenter values for these fields.',
|
||||
}
|
||||
);
|
||||
|
||||
export const JIRA_EMAIL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel',
|
||||
{
|
||||
|
@ -71,13 +28,6 @@ export const JIRA_EMAIL_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const JIRA_EMAIL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField',
|
||||
{
|
||||
defaultMessage: 'Email address is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const JIRA_API_TOKEN_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel',
|
||||
{
|
||||
|
@ -85,27 +35,6 @@ export const JIRA_API_TOKEN_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const JIRA_API_TOKEN_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField',
|
||||
{
|
||||
defaultMessage: 'API token is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_SUMMARY = i18n.translate(
|
||||
'xpack.triggersActionsUI.cases.configureCases.mappingFieldSummary',
|
||||
{
|
||||
defaultMessage: 'Summary',
|
||||
}
|
||||
);
|
||||
|
||||
export const DESCRIPTION_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField',
|
||||
{
|
||||
defaultMessage: 'Description is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SUMMARY_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredSummaryTextField',
|
||||
{
|
||||
|
@ -113,20 +42,6 @@ export const SUMMARY_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription',
|
||||
{
|
||||
defaultMessage: 'Description',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_COMMENTS = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments',
|
||||
{
|
||||
defaultMessage: 'Comments',
|
||||
}
|
||||
);
|
||||
|
||||
export const ISSUE_TYPES_API_ERROR = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage',
|
||||
{
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { PagerDutyActionConnector } from '../types';
|
||||
import { registrationServicesMock } from '../../../../mocks';
|
||||
|
||||
const ACTION_TYPE_ID = '.pagerduty';
|
||||
|
@ -30,60 +29,6 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('pagerduty connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
routingKey: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.pagerduty',
|
||||
name: 'pagerduty',
|
||||
config: {
|
||||
apiUrl: 'http:\\test',
|
||||
},
|
||||
} as PagerDutyActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
secrets: {
|
||||
errors: {
|
||||
routingKey: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
delete actionConnector.config.apiUrl;
|
||||
actionConnector.secrets.routingKey = 'test1';
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
secrets: {
|
||||
errors: {
|
||||
routingKey: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.pagerduty',
|
||||
name: 'pagerduty',
|
||||
config: {
|
||||
apiUrl: 'http:\\test',
|
||||
},
|
||||
} as PagerDutyActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
secrets: {
|
||||
errors: {
|
||||
routingKey: ['An integration key / routing key is required.'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagerduty action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
|
|
|
@ -8,13 +8,8 @@
|
|||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import { ActionTypeModel, GenericValidationResult } from '../../../../types';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
GenericValidationResult,
|
||||
ConnectorValidationResult,
|
||||
} from '../../../../types';
|
||||
import {
|
||||
PagerDutyActionConnector,
|
||||
PagerDutyConfig,
|
||||
PagerDutySecrets,
|
||||
PagerDutyActionParams,
|
||||
|
@ -42,22 +37,6 @@ export function getActionType(): ActionTypeModel<
|
|||
defaultMessage: 'Send to PagerDuty',
|
||||
}
|
||||
),
|
||||
validateConnector: async (
|
||||
action: PagerDutyActionConnector
|
||||
): Promise<ConnectorValidationResult<PagerDutyConfig, PagerDutySecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const secretsErrors = {
|
||||
routingKey: new Array<string>(),
|
||||
};
|
||||
const validationResult = {
|
||||
secrets: { errors: secretsErrors },
|
||||
};
|
||||
|
||||
if (!action.secrets.routingKey) {
|
||||
secretsErrors.routingKey.push(translations.INTEGRATION_KEY_REQUIRED);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: async (
|
||||
actionParams: PagerDutyActionParams
|
||||
): Promise<
|
||||
|
|
|
@ -8,8 +8,11 @@
|
|||
import React from 'react';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { PagerDutyActionConnector } from '../types';
|
||||
import PagerDutyActionConnectorFields from './pagerduty_connectors';
|
||||
import { ConnectorFormTestProvider } from '../test_utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
describe('PagerDutyActionConnectorFields renders', () => {
|
||||
|
@ -22,20 +25,19 @@ describe('PagerDutyActionConnectorFields renders', () => {
|
|||
actionTypeId: '.pagerduty',
|
||||
name: 'pagerduty',
|
||||
config: {
|
||||
apiUrl: 'http:\\test',
|
||||
apiUrl: 'http://test.com',
|
||||
},
|
||||
} as PagerDutyActionConnector;
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<PagerDutyActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], routingKey: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<PagerDutyActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -45,83 +47,171 @@ describe('PagerDutyActionConnectorFields renders', () => {
|
|||
|
||||
expect(wrapper.find('[data-test-subj="pagerdutyApiUrlInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="pagerdutyApiUrlInput"]').first().prop('value')).toBe(
|
||||
'http:\\test'
|
||||
'http://test.com'
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="pagerdutyRoutingKeyInput"]').length > 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display a message on create to remember credentials', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.pagerduty',
|
||||
secrets: {},
|
||||
config: {},
|
||||
} as PagerDutyActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<PagerDutyActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], routingKey: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
test('should display a message on edit to re-enter credentials', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
routingKey: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.pagerduty',
|
||||
name: 'pagerduty',
|
||||
config: {
|
||||
apiUrl: 'http:\\test',
|
||||
},
|
||||
} as PagerDutyActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<PagerDutyActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], routingKey: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should display a message for missing secrets after import', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
routingKey: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.pagerduty',
|
||||
isMissingSecrets: true,
|
||||
name: 'pagerduty',
|
||||
config: {
|
||||
apiUrl: 'http:\\test',
|
||||
},
|
||||
} as PagerDutyActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<PagerDutyActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], routingKey: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0);
|
||||
it('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
routingKey: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.pagerduty',
|
||||
name: 'pagerduty',
|
||||
config: {
|
||||
apiUrl: 'http://test.com',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<PagerDutyActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
secrets: {
|
||||
routingKey: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.pagerduty',
|
||||
name: 'pagerduty',
|
||||
config: {
|
||||
apiUrl: 'http://test.com',
|
||||
},
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('validates correctly if the apiUrl is empty', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
routingKey: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.pagerduty',
|
||||
name: 'pagerduty',
|
||||
config: {
|
||||
apiUrl: '',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<PagerDutyActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
secrets: {
|
||||
routingKey: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.pagerduty',
|
||||
name: 'pagerduty',
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('validates correctly if the apiUrl is not empty and not a valid url', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
routingKey: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.pagerduty',
|
||||
name: 'pagerduty',
|
||||
config: {
|
||||
apiUrl: 'not-valid',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<PagerDutyActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {},
|
||||
isValid: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('validates correctly the routingKey', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
routingKey: '',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.pagerduty',
|
||||
name: 'pagerduty',
|
||||
config: {
|
||||
apiUrl: 'not-valid',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<PagerDutyActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {},
|
||||
isValid: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,99 +6,88 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { DocLinksStart } from '@kbn/core/public';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { PagerDutyActionConnector } from '../types';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const PagerDutyActionConnectorFields: React.FunctionComponent<
|
||||
ActionConnectorFieldsProps<PagerDutyActionConnector>
|
||||
> = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => {
|
||||
const { emptyField, urlField } = fieldValidators;
|
||||
|
||||
const getApiURLConfig = (): FieldConfig => ({
|
||||
label: i18n.API_URL_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: (args) => {
|
||||
const { value } = args;
|
||||
/**
|
||||
* The field is optional so if it is empty
|
||||
* we do not validate
|
||||
*/
|
||||
if (isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return urlField(i18n.API_URL_INVALID)(args);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const getRoutingKeyConfig = (docLinks: DocLinksStart): FieldConfig => ({
|
||||
label: i18n.INTEGRATION_KEY_LABEL,
|
||||
helpText: (
|
||||
<EuiLink href={docLinks.links.alerting.pagerDutyAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyNameHelpLabel"
|
||||
defaultMessage="Configure a PagerDuty account"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.INTEGRATION_KEY_REQUIRED),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const PagerDutyActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
readOnly,
|
||||
isEdit,
|
||||
}) => {
|
||||
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
|
||||
id="apiUrl"
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'API URL (optional)',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
name="apiUrl"
|
||||
value={apiUrl || ''}
|
||||
readOnly={readOnly}
|
||||
data-test-subj="pagerdutyApiUrlInput"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
editActionConfig('apiUrl', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!apiUrl) {
|
||||
editActionConfig('apiUrl', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="routingKey"
|
||||
fullWidth
|
||||
helpText={
|
||||
<EuiLink href={docLinks.links.alerting.pagerDutyAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyNameHelpLabel"
|
||||
defaultMessage="Configure a PagerDuty account"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
error={errors.routingKey}
|
||||
isInvalid={isRoutingKeyInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Integration key',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
1,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.reenterValueLabel',
|
||||
{ defaultMessage: 'This key is encrypted. Please reenter a value for this field.' }
|
||||
)
|
||||
)}
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isRoutingKeyInvalid}
|
||||
name="routingKey"
|
||||
readOnly={readOnly}
|
||||
value={routingKey || ''}
|
||||
data-test-subj="pagerdutyRoutingKeyInput"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
editActionSecrets('routingKey', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!routingKey) {
|
||||
editActionSecrets('routingKey', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path="config.apiUrl"
|
||||
component={Field}
|
||||
config={getApiURLConfig()}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
readOnly,
|
||||
'data-test-subj': 'pagerdutyApiUrlInput',
|
||||
fullWidth: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path="secrets.routingKey"
|
||||
config={getRoutingKeyConfig(docLinks)}
|
||||
component={Field}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
readOnly,
|
||||
'data-test-subj': 'pagerdutyRoutingKeyInput',
|
||||
fullWidth: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -27,3 +27,24 @@ export const INTEGRATION_KEY_REQUIRED = i18n.translate(
|
|||
defaultMessage: 'An integration key / routing key is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'API URL (optional)',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlInvalid',
|
||||
{
|
||||
defaultMessage: 'Invalid API URL',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATION_KEY_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Integration key',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { ResilientActionConnector } from './types';
|
||||
import { registrationServicesMock } from '../../../../mocks';
|
||||
|
||||
const ACTION_TYPE_ID = '.resilient';
|
||||
|
@ -29,68 +28,6 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('resilient connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiKeyId: 'email',
|
||||
apiKeySecret: 'token',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.resilient',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'resilient',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
orgId: '201',
|
||||
},
|
||||
} as ResilientActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
orgId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
apiKeySecret: [],
|
||||
apiKeyId: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiKeyId: 'user',
|
||||
},
|
||||
id: '.jira',
|
||||
actionTypeId: '.jira',
|
||||
name: 'jira',
|
||||
config: {},
|
||||
} as unknown as ResilientActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: ['URL is required.'],
|
||||
orgId: ['Organization ID is required'],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
apiKeySecret: ['Secret is required'],
|
||||
apiKeyId: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resilient action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
|
|
|
@ -7,66 +7,8 @@
|
|||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
GenericValidationResult,
|
||||
ActionTypeModel,
|
||||
ConnectorValidationResult,
|
||||
} from '../../../../types';
|
||||
import {
|
||||
ResilientActionConnector,
|
||||
ResilientConfig,
|
||||
ResilientSecrets,
|
||||
ResilientActionParams,
|
||||
} from './types';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
|
||||
const validateConnector = async (
|
||||
action: ResilientActionConnector
|
||||
): Promise<ConnectorValidationResult<ResilientConfig, ResilientSecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
apiUrl: new Array<string>(),
|
||||
orgId: new Array<string>(),
|
||||
};
|
||||
const secretsErrors = {
|
||||
apiKeyId: new Array<string>(),
|
||||
apiKeySecret: new Array<string>(),
|
||||
};
|
||||
|
||||
const validationResult = {
|
||||
config: { errors: configErrors },
|
||||
secrets: { errors: secretsErrors },
|
||||
};
|
||||
|
||||
if (!action.config.apiUrl) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED];
|
||||
}
|
||||
|
||||
if (action.config.apiUrl) {
|
||||
if (!isValidUrl(action.config.apiUrl)) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID];
|
||||
} else if (!isValidUrl(action.config.apiUrl, 'https:')) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS];
|
||||
}
|
||||
}
|
||||
|
||||
if (!action.config.orgId) {
|
||||
configErrors.orgId = [...configErrors.orgId, translations.ORG_ID_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.apiKeyId) {
|
||||
secretsErrors.apiKeyId = [...secretsErrors.apiKeyId, translations.API_KEY_ID_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.apiKeySecret) {
|
||||
secretsErrors.apiKeySecret = [
|
||||
...secretsErrors.apiKeySecret,
|
||||
translations.API_KEY_SECRET_REQUIRED,
|
||||
];
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
};
|
||||
import { GenericValidationResult, ActionTypeModel } from '../../../../types';
|
||||
import { ResilientConfig, ResilientSecrets, ResilientActionParams } from './types';
|
||||
|
||||
export const DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText',
|
||||
|
@ -92,7 +34,6 @@ export function getActionType(): ActionTypeModel<
|
|||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: DESC,
|
||||
actionTypeTitle: TITLE,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./resilient_connectors')),
|
||||
validateParams: async (
|
||||
actionParams: ResilientActionParams
|
||||
|
|
|
@ -8,169 +8,141 @@
|
|||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import ResilientConnectorFields from './resilient_connectors';
|
||||
import { ResilientActionConnector } from './types';
|
||||
import { ConnectorFormTestProvider } from '../test_utils';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
describe('ResilientActionConnectorFields renders', () => {
|
||||
test('alerting Resilient connector fields are rendered', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.resilient',
|
||||
name: 'resilient',
|
||||
config: {
|
||||
apiUrl: 'https://test.com',
|
||||
orgId: '201',
|
||||
},
|
||||
secrets: {
|
||||
apiKeyId: 'key',
|
||||
apiKeySecret: 'secret',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.resilient',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'resilient',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
orgId: '201',
|
||||
},
|
||||
} as ResilientActionConnector;
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], apiKeyId: [], apiKeySecret: [], orgId: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<ResilientConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-orgId-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="config.apiUrl-input"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="config.orgId-input"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="secrets.apiKeyId-input"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="secrets.apiKeySecret-input"]').length > 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('case specific Resilient connector fields is rendered', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiKeyId: 'email',
|
||||
apiKeySecret: 'token',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.resilient',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'resilient',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
orgId: '201',
|
||||
},
|
||||
} as ResilientActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], apiKeyId: [], apiKeySecret: [], orgId: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
consumer={'case'}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-orgId-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
const tests: Array<[string, string]> = [
|
||||
['config.apiUrl-input', 'not-valid'],
|
||||
['config.orgId-input', ''],
|
||||
['secrets.apiKeyId-input', ''],
|
||||
['secrets.apiKeySecret-input', ''],
|
||||
];
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.resilient',
|
||||
name: 'resilient',
|
||||
config: {
|
||||
apiUrl: 'https://test.com',
|
||||
orgId: '201',
|
||||
},
|
||||
secrets: {
|
||||
apiKeyId: 'key',
|
||||
apiKeySecret: 'secret',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
test('should display a message on create to remember credentials', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.resilient',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as ResilientActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], apiKeyId: [], apiKeySecret: [], orgId: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<ResilientConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
test('should display a message for missing secrets after import', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.resilient',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {},
|
||||
secrets: {},
|
||||
isMissingSecrets: true,
|
||||
} as ResilientActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], apiKeyId: [], apiKeySecret: [], orgId: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0);
|
||||
});
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
test('should display a message on edit to re-enter credentials', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiKeyId: 'key',
|
||||
apiKeySecret: 'secret',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.resilient',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'resilient',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
orgId: '201',
|
||||
},
|
||||
} as ResilientActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<ResilientConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], apiKeyId: [], apiKeySecret: [], orgId: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.resilient',
|
||||
name: 'resilient',
|
||||
config: {
|
||||
apiUrl: 'https://test.com',
|
||||
orgId: '201',
|
||||
},
|
||||
secrets: {
|
||||
apiKeyId: 'key',
|
||||
apiKeySecret: 'secret',
|
||||
},
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(tests)('validates correctly %p', async (field, value) => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.resilient',
|
||||
name: 'resilient',
|
||||
config: {
|
||||
apiUrl: 'https://test.com',
|
||||
orgId: '201',
|
||||
},
|
||||
secrets: {
|
||||
apiKeyId: 'key',
|
||||
apiKeySecret: 'secret',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<ResilientConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,185 +5,33 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiFieldPassword,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import * as i18n from './translations';
|
||||
import { ResilientActionConnector } from './types';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label';
|
||||
import {
|
||||
ConfigFieldSchema,
|
||||
SecretsFieldSchema,
|
||||
SimpleConnectorForm,
|
||||
} from '../../simple_connector_form';
|
||||
|
||||
const ResilientConnectorFields: React.FC<ActionConnectorFieldsProps<ResilientActionConnector>> = ({
|
||||
action,
|
||||
editActionSecrets,
|
||||
editActionConfig,
|
||||
errors,
|
||||
readOnly,
|
||||
}) => {
|
||||
const { apiUrl, orgId } = action.config;
|
||||
const isApiUrlInvalid: boolean =
|
||||
apiUrl !== undefined && errors.apiUrl !== undefined && errors.apiUrl.length > 0;
|
||||
|
||||
const { apiKeyId, apiKeySecret } = action.secrets;
|
||||
|
||||
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 =
|
||||
apiKeySecret !== undefined &&
|
||||
errors.apiKeySecret !== undefined &&
|
||||
errors.apiKeySecret.length > 0;
|
||||
|
||||
const handleOnChangeActionConfig = useCallback(
|
||||
(key: string, value: string) => editActionConfig(key, value),
|
||||
[editActionConfig]
|
||||
);
|
||||
|
||||
const handleOnChangeSecretConfig = useCallback(
|
||||
(key: string, value: string) => editActionSecrets(key, value),
|
||||
[editActionSecrets]
|
||||
);
|
||||
const configFormSchema: ConfigFieldSchema[] = [
|
||||
{ id: 'apiUrl', label: i18n.API_URL_LABEL, isUrlField: true },
|
||||
{ id: 'orgId', label: i18n.ORG_ID_LABEL },
|
||||
];
|
||||
const secretsFormSchema: SecretsFieldSchema[] = [
|
||||
{ id: 'apiKeyId', label: i18n.API_KEY_ID_LABEL },
|
||||
{ id: 'apiKeySecret', label: i18n.API_KEY_SECRET_LABEL, isPasswordField: true },
|
||||
];
|
||||
|
||||
const ResilientConnectorFields: React.FC<ActionConnectorFieldsProps> = ({ readOnly, isEdit }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="apiUrl"
|
||||
fullWidth
|
||||
error={errors.apiUrl}
|
||||
isInvalid={isApiUrlInvalid}
|
||||
label={i18n.API_URL_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isApiUrlInvalid}
|
||||
name="apiUrl"
|
||||
readOnly={readOnly}
|
||||
value={apiUrl || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="apiUrlFromInput"
|
||||
onChange={(evt) => handleOnChangeActionConfig('apiUrl', evt.target.value)}
|
||||
onBlur={() => {
|
||||
if (!apiUrl) {
|
||||
editActionConfig('apiUrl', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-resilient-orgId-key"
|
||||
fullWidth
|
||||
error={errors.orgId}
|
||||
isInvalid={isOrgIdInvalid}
|
||||
label={i18n.ORG_ID_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isOrgIdInvalid}
|
||||
name="connector-resilient-orgId"
|
||||
value={orgId || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-resilient-orgId-form-input"
|
||||
onChange={(evt) => handleOnChangeActionConfig('orgId', evt.target.value)}
|
||||
onBlur={() => {
|
||||
if (!orgId) {
|
||||
editActionConfig('orgId', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xxs">
|
||||
<h4>{i18n.API_KEY_LABEL}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
2,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.REENTER_VALUES_LABEL
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-resilient-apiKeyId"
|
||||
fullWidth
|
||||
error={errors.apiKeyId}
|
||||
isInvalid={isApiKeyInvalid}
|
||||
label={i18n.API_KEY_ID_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isApiKeyInvalid}
|
||||
readOnly={readOnly}
|
||||
name="connector-resilient-apiKeyId"
|
||||
value={apiKeyId || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-resilient-apiKeyId-form-input"
|
||||
onChange={(evt) => handleOnChangeSecretConfig('apiKeyId', evt.target.value)}
|
||||
onBlur={() => {
|
||||
if (!apiKeyId) {
|
||||
editActionSecrets('apiKeyId', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="connector-resilient-apiKeySecret"
|
||||
fullWidth
|
||||
error={errors.apiKeySecret}
|
||||
isInvalid={isApiKeySecretInvalid}
|
||||
label={i18n.API_KEY_SECRET_LABEL}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={isApiKeySecretInvalid}
|
||||
name="connector-resilient-apiKeySecret"
|
||||
value={apiKeySecret || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-resilient-apiKeySecret-form-input"
|
||||
onChange={(evt) => handleOnChangeSecretConfig('apiKeySecret', evt.target.value)}
|
||||
onBlur={() => {
|
||||
if (!apiKeySecret) {
|
||||
editActionSecrets('apiKeySecret', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
<SimpleConnectorForm
|
||||
isEdit={isEdit}
|
||||
readOnly={readOnly}
|
||||
configFormSchema={configFormSchema}
|
||||
secretsFormSchema={secretsFormSchema}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -14,27 +14,6 @@ export const API_URL_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const API_URL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is invalid.',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_REQUIRE_HTTPS = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requireHttpsApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL must start with https://.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ORG_ID_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId',
|
||||
{
|
||||
|
@ -42,88 +21,17 @@ export const ORG_ID_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ORG_ID_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField',
|
||||
{
|
||||
defaultMessage: 'Organization ID is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_KEY_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKey',
|
||||
{
|
||||
defaultMessage: 'API key',
|
||||
}
|
||||
);
|
||||
|
||||
export const REMEMBER_VALUES_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.rememberValuesLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Remember these values. You must reenter them each time you edit the connector.',
|
||||
}
|
||||
);
|
||||
|
||||
export const REENTER_VALUES_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.reenterValuesLabel',
|
||||
{
|
||||
defaultMessage: 'ID and secret are encrypted. Please reenter values for these fields.',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_KEY_ID_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId',
|
||||
{
|
||||
defaultMessage: 'ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_KEY_ID_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField',
|
||||
{
|
||||
defaultMessage: 'ID is required',
|
||||
defaultMessage: 'API Key ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_KEY_SECRET_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret',
|
||||
{
|
||||
defaultMessage: 'Secret',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_KEY_SECRET_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField',
|
||||
{
|
||||
defaultMessage: 'Secret is required',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_NAME = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription',
|
||||
{
|
||||
defaultMessage: 'Description',
|
||||
}
|
||||
);
|
||||
|
||||
export const MAPPING_FIELD_COMMENTS = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments',
|
||||
{
|
||||
defaultMessage: 'Comments',
|
||||
}
|
||||
);
|
||||
|
||||
export const DESCRIPTION_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredDescriptionTextField',
|
||||
{
|
||||
defaultMessage: 'Description is required.',
|
||||
defaultMessage: 'API Key Secret',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel, UserConfiguredActionConnector } from '../../../../types';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { registrationServicesMock } from '../../../../mocks';
|
||||
|
||||
const ACTION_TYPE_ID = '.server-log';
|
||||
|
@ -29,29 +29,6 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('server-log connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector: UserConfiguredActionConnector<{}, {}> = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.server-log',
|
||||
name: 'server-log',
|
||||
config: {},
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
secrets: {
|
||||
errors: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
|
|
|
@ -7,11 +7,7 @@
|
|||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
GenericValidationResult,
|
||||
ConnectorValidationResult,
|
||||
} from '../../../../types';
|
||||
import { ActionTypeModel, GenericValidationResult } from '../../../../types';
|
||||
import { ServerLogActionParams } from '../types';
|
||||
|
||||
export function getActionType(): ActionTypeModel<unknown, unknown, ServerLogActionParams> {
|
||||
|
@ -30,9 +26,6 @@ export function getActionType(): ActionTypeModel<unknown, unknown, ServerLogActi
|
|||
defaultMessage: 'Send to Server log',
|
||||
}
|
||||
),
|
||||
validateConnector: (): Promise<ConnectorValidationResult<unknown, unknown>> => {
|
||||
return Promise.resolve({ config: { errors: {} }, secrets: { errors: {} } });
|
||||
},
|
||||
validateParams: (
|
||||
actionParams: ServerLogActionParams
|
||||
): Promise<GenericValidationResult<Pick<ServerLogActionParams, 'message'>>> => {
|
||||
|
|
|
@ -25,7 +25,7 @@ const ERROR_MESSAGE = i18n.translate(
|
|||
);
|
||||
|
||||
interface Props {
|
||||
appId: string;
|
||||
appId?: string;
|
||||
message?: string | null;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,104 +5,52 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { EuiFormRow, EuiFieldText, EuiFieldPassword } from '@elastic/eui';
|
||||
import type { ActionConnectorFieldsProps } from '../../../../../types';
|
||||
import React, { memo } from 'react';
|
||||
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { PasswordField } from '../../../password_field';
|
||||
import * as i18n from '../translations';
|
||||
import type { ServiceNowActionConnector } from '../types';
|
||||
import { isFieldInvalid } from '../helpers';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../../get_encrypted_field_notify_label';
|
||||
|
||||
interface Props {
|
||||
action: ActionConnectorFieldsProps<ServiceNowActionConnector>['action'];
|
||||
errors: ActionConnectorFieldsProps<ServiceNowActionConnector>['errors'];
|
||||
readOnly: boolean;
|
||||
isLoading: boolean;
|
||||
editActionSecrets: ActionConnectorFieldsProps<ServiceNowActionConnector>['editActionSecrets'];
|
||||
pathPrefix?: string;
|
||||
}
|
||||
|
||||
const NUMBER_OF_FIELDS = 2;
|
||||
|
||||
const CredentialsAuthComponent: React.FC<Props> = ({
|
||||
action,
|
||||
errors,
|
||||
isLoading,
|
||||
readOnly,
|
||||
editActionSecrets,
|
||||
}) => {
|
||||
const { username, password } = action.secrets;
|
||||
|
||||
const isUsernameInvalid = isFieldInvalid(username, errors.username);
|
||||
const isPasswordInvalid = isFieldInvalid(password, errors.password);
|
||||
|
||||
const onChangeUsernameEvent = useCallback(
|
||||
(event?: React.ChangeEvent<HTMLInputElement>) =>
|
||||
editActionSecrets('username', event?.target.value ?? ''),
|
||||
[editActionSecrets]
|
||||
);
|
||||
|
||||
const onChangePasswordEvent = useCallback(
|
||||
(event?: React.ChangeEvent<HTMLInputElement>) =>
|
||||
editActionSecrets('password', event?.target.value ?? ''),
|
||||
[editActionSecrets]
|
||||
);
|
||||
const { emptyField } = fieldValidators;
|
||||
|
||||
const CredentialsAuthComponent: React.FC<Props> = ({ isLoading, readOnly, pathPrefix = '' }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow fullWidth>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
NUMBER_OF_FIELDS,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.REENTER_VALUES_LABEL
|
||||
)}
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="connector-servicenow-username"
|
||||
fullWidth
|
||||
error={errors.username}
|
||||
isInvalid={isUsernameInvalid}
|
||||
label={i18n.USERNAME_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isUsernameInvalid}
|
||||
readOnly={readOnly}
|
||||
name="connector-servicenow-username"
|
||||
value={username || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-servicenow-username-form-input"
|
||||
onChange={onChangeUsernameEvent}
|
||||
onBlur={() => {
|
||||
if (!username) {
|
||||
onChangeUsernameEvent();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="connector-servicenow-password"
|
||||
fullWidth
|
||||
error={errors.password}
|
||||
isInvalid={isPasswordInvalid}
|
||||
<UseField
|
||||
path={`${pathPrefix}secrets.username`}
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.USERNAME_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.USERNAME_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'connector-servicenow-username-form-input',
|
||||
isLoading,
|
||||
readOnly,
|
||||
disabled: readOnly || isLoading,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<PasswordField
|
||||
path={`${pathPrefix}secrets.password`}
|
||||
label={i18n.PASSWORD_LABEL}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={isPasswordInvalid}
|
||||
name="connector-servicenow-password"
|
||||
value={password || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="connector-servicenow-password-form-input"
|
||||
onChange={onChangePasswordEvent}
|
||||
onBlur={() => {
|
||||
if (!password) {
|
||||
onChangePasswordEvent();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
readOnly={readOnly}
|
||||
data-test-subj="connector-servicenow-password-form-input"
|
||||
isLoading={isLoading}
|
||||
disabled={readOnly || isLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,224 +5,121 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { EuiFormRow, EuiFieldText, EuiFieldPassword, EuiTextArea } from '@elastic/eui';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../../get_encrypted_field_notify_label';
|
||||
import type { ActionConnectorFieldsProps } from '../../../../../types';
|
||||
import type { ServiceNowActionConnector } from '../types';
|
||||
import React, { memo } from 'react';
|
||||
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { TextAreaField, TextField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import * as i18n from '../translations';
|
||||
import { isFieldInvalid } from '../helpers';
|
||||
import { PRIVATE_KEY_PASSWORD_HELPER_TEXT } from '../translations';
|
||||
import { PasswordField } from '../../../password_field';
|
||||
|
||||
interface Props {
|
||||
action: ActionConnectorFieldsProps<ServiceNowActionConnector>['action'];
|
||||
errors: ActionConnectorFieldsProps<ServiceNowActionConnector>['errors'];
|
||||
readOnly: boolean;
|
||||
isLoading: boolean;
|
||||
editActionSecrets: ActionConnectorFieldsProps<ServiceNowActionConnector>['editActionSecrets'];
|
||||
editActionConfig: ActionConnectorFieldsProps<ServiceNowActionConnector>['editActionConfig'];
|
||||
pathPrefix?: string;
|
||||
}
|
||||
|
||||
const NUMBER_OF_FIELDS = 3;
|
||||
|
||||
const OAuthComponent: React.FC<Props> = ({
|
||||
action,
|
||||
errors,
|
||||
isLoading,
|
||||
readOnly,
|
||||
editActionSecrets,
|
||||
editActionConfig,
|
||||
}) => {
|
||||
const { clientId, userIdentifierValue, jwtKeyId } = action.config;
|
||||
const { clientSecret, privateKey, privateKeyPassword } = action.secrets;
|
||||
|
||||
const isClientIdInvalid = isFieldInvalid(clientId, errors.clientId);
|
||||
const isUserIdentifierInvalid = isFieldInvalid(userIdentifierValue, errors.userIdentifierValue);
|
||||
const isKeyIdInvalid = isFieldInvalid(jwtKeyId, errors.jwtKeyId);
|
||||
const isClientSecretInvalid = isFieldInvalid(clientSecret, errors.clientSecret);
|
||||
const isPrivateKeyInvalid = isFieldInvalid(privateKey, errors.privateKey);
|
||||
|
||||
const onChangeClientIdEvent = useCallback(
|
||||
(event?: React.ChangeEvent<HTMLInputElement>) =>
|
||||
editActionConfig('clientId', event?.target.value ?? ''),
|
||||
[editActionConfig]
|
||||
);
|
||||
const onChangeUserIdentifierInvalidEvent = useCallback(
|
||||
(event?: React.ChangeEvent<HTMLInputElement>) =>
|
||||
editActionConfig('userIdentifierValue', event?.target.value ?? ''),
|
||||
[editActionConfig]
|
||||
);
|
||||
const onChangeJWTKeyIdEvent = useCallback(
|
||||
(event?: React.ChangeEvent<HTMLInputElement>) =>
|
||||
editActionConfig('jwtKeyId', event?.target.value ?? ''),
|
||||
[editActionConfig]
|
||||
);
|
||||
|
||||
const onChangeClientSecretEvent = useCallback(
|
||||
(event?: React.ChangeEvent<HTMLInputElement>) =>
|
||||
editActionSecrets('clientSecret', event?.target.value ?? ''),
|
||||
[editActionSecrets]
|
||||
);
|
||||
|
||||
const onChangePrivateKeyEvent = useCallback(
|
||||
(event?: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
editActionSecrets('privateKey', event?.target.value ?? ''),
|
||||
[editActionSecrets]
|
||||
);
|
||||
|
||||
const onChangePrivateKeyPasswordEvent = useCallback(
|
||||
(event?: React.ChangeEvent<HTMLInputElement>) =>
|
||||
editActionSecrets('privateKeyPassword', event?.target.value ?? ''),
|
||||
[editActionSecrets]
|
||||
);
|
||||
const { emptyField } = fieldValidators;
|
||||
|
||||
const OAuthComponent: React.FC<Props> = ({ isLoading, readOnly, pathPrefix = '' }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow fullWidth>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
NUMBER_OF_FIELDS,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.REENTER_VALUES_LABEL
|
||||
)}
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="connector-servicenow-client-id"
|
||||
fullWidth
|
||||
error={errors.clientId}
|
||||
isInvalid={isClientIdInvalid}
|
||||
label={i18n.CLIENTID_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isClientIdInvalid}
|
||||
readOnly={readOnly}
|
||||
name="connector-servicenow-client-id"
|
||||
value={clientId || ''}
|
||||
data-test-subj="connector-servicenow-client-id-form-input"
|
||||
onChange={onChangeClientIdEvent}
|
||||
onBlur={() => {
|
||||
if (!clientId) {
|
||||
onChangeClientIdEvent();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="connector-servicenow-useremail"
|
||||
fullWidth
|
||||
error={errors.userEmail}
|
||||
isInvalid={isUserIdentifierInvalid}
|
||||
label={i18n.USER_EMAIL_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isUserIdentifierInvalid}
|
||||
readOnly={readOnly}
|
||||
name="connector-servicenow-user-identifier"
|
||||
value={userIdentifierValue || ''}
|
||||
data-test-subj="connector-servicenow-user-identifier-form-input"
|
||||
onChange={onChangeUserIdentifierInvalidEvent}
|
||||
onBlur={() => {
|
||||
if (!userIdentifierValue) {
|
||||
onChangeUserIdentifierInvalidEvent();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="connector-servicenow-keyid"
|
||||
fullWidth
|
||||
error={errors.keyId}
|
||||
isInvalid={isKeyIdInvalid}
|
||||
label={i18n.KEY_ID_LABEL}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={isKeyIdInvalid}
|
||||
name="connector-servicenow-jwt-key-id"
|
||||
value={jwtKeyId || ''}
|
||||
data-test-subj="connector-servicenow-jwt-key-id-form-input"
|
||||
onChange={onChangeJWTKeyIdEvent}
|
||||
onBlur={() => {
|
||||
if (!jwtKeyId) {
|
||||
onChangeJWTKeyIdEvent();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="connector-servicenow-client-secret"
|
||||
fullWidth
|
||||
error={errors.clientSecret}
|
||||
isInvalid={isClientSecretInvalid}
|
||||
<UseField
|
||||
path={`${pathPrefix}config.clientId`}
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.CLIENTID_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.CLIENTID_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'connector-servicenow-client-id-form-input',
|
||||
readOnly,
|
||||
isLoading,
|
||||
disabled: readOnly || isLoading,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path={`${pathPrefix}config.userIdentifierValue`}
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.USER_IDENTIFIER_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.USER_IDENTIFIER_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'connector-servicenow-user-identifier-form-input',
|
||||
readOnly,
|
||||
disabled: readOnly || isLoading,
|
||||
isLoading,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path={`${pathPrefix}config.jwtKeyId`}
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.KEY_ID_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.KEYID_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'connector-servicenow-jwt-key-id-form-input',
|
||||
readOnly,
|
||||
disabled: readOnly || isLoading,
|
||||
isLoading,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<PasswordField
|
||||
path={`${pathPrefix}secrets.clientSecret`}
|
||||
label={i18n.CLIENTSECRET_LABEL}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
isInvalid={isClientSecretInvalid}
|
||||
readOnly={readOnly}
|
||||
name="connector-servicenow-client-secret"
|
||||
value={clientSecret || ''}
|
||||
data-test-subj="connector-servicenow-client-secret-form-input"
|
||||
onChange={onChangeClientSecretEvent}
|
||||
onBlur={() => {
|
||||
if (!clientSecret) {
|
||||
onChangeClientSecretEvent();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="connector-servicenow-private-key"
|
||||
fullWidth
|
||||
error={errors.privateKey}
|
||||
isInvalid={isPrivateKeyInvalid}
|
||||
label={i18n.PRIVATE_KEY_LABEL}
|
||||
>
|
||||
<EuiTextArea
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={isPrivateKeyInvalid}
|
||||
name="connector-servicenow-private-key"
|
||||
value={privateKey || ''}
|
||||
data-test-subj="connector-servicenow-private-key-form-input"
|
||||
onChange={onChangePrivateKeyEvent}
|
||||
onBlur={() => {
|
||||
if (!privateKey) {
|
||||
onChangePrivateKeyEvent();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="connector-servicenow-private-key-password"
|
||||
fullWidth
|
||||
error={errors.privateKeyPassword}
|
||||
readOnly={readOnly}
|
||||
data-test-subj="connector-servicenow-client-secret-form-input"
|
||||
isLoading={isLoading}
|
||||
disabled={readOnly || isLoading}
|
||||
/>
|
||||
<UseField
|
||||
path="secrets.privateKey"
|
||||
component={TextAreaField}
|
||||
config={{
|
||||
label: i18n.PRIVATE_KEY_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.PRIVATE_KEY_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
readOnly,
|
||||
'data-test-subj': 'connector-servicenow-private-key-form-input',
|
||||
disabled: readOnly || isLoading,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<PasswordField
|
||||
path={`${pathPrefix}secrets.privateKeyPassword`}
|
||||
label={i18n.PRIVATE_KEY_PASSWORD_LABEL}
|
||||
helpText={PRIVATE_KEY_PASSWORD_HELPER_TEXT}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
name="connector-servicenow-private-key-password"
|
||||
value={privateKeyPassword || ''}
|
||||
data-test-subj="connector-servicenow-private-key-password-form-input"
|
||||
onChange={onChangePrivateKeyPasswordEvent}
|
||||
onBlur={() => {
|
||||
if (!privateKeyPassword) {
|
||||
onChangePrivateKeyPasswordEvent();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
helpText={i18n.PRIVATE_KEY_PASSWORD_HELPER_TEXT}
|
||||
validate={false}
|
||||
data-test-subj="connector-servicenow-private-key-password-form-input"
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
disabled={readOnly || isLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,31 +8,29 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { Credentials } from './credentials';
|
||||
import { ServiceNowActionConnector } from './types';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { ConnectorFormTestProvider } from '../test_utils';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
const editActionConfigMock = jest.fn();
|
||||
const editActionSecretsMock = jest.fn();
|
||||
|
||||
const basicAuthConnector: ServiceNowActionConnector = {
|
||||
secrets: { username: 'test', password: 'test' },
|
||||
config: { isOAuth: false, apiUrl: 'https://example.com', usesTableApi: false },
|
||||
} as ServiceNowActionConnector;
|
||||
|
||||
describe('Credentials', () => {
|
||||
const connector = {
|
||||
id: 'test',
|
||||
actionTypeId: '.servicenow',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: true,
|
||||
name: 'SN',
|
||||
config: {},
|
||||
secrets: {},
|
||||
};
|
||||
|
||||
it('renders basic auth form', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<Credentials
|
||||
action={basicAuthConnector}
|
||||
errors={{}}
|
||||
readOnly={false}
|
||||
isLoading={false}
|
||||
editActionSecrets={() => {}}
|
||||
editActionConfig={() => {}}
|
||||
/>
|
||||
</IntlProvider>
|
||||
<ConnectorFormTestProvider connector={connector}>
|
||||
<IntlProvider locale="en">
|
||||
<Credentials isOAuth={false} readOnly={false} isLoading={false} />
|
||||
</IntlProvider>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
expect(screen.getByRole('switch')).toBeInTheDocument();
|
||||
expect((await screen.findByRole('switch')).getAttribute('aria-checked')).toEqual('false');
|
||||
|
@ -50,24 +48,15 @@ describe('Credentials', () => {
|
|||
|
||||
it('switches to oauth form', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<Credentials
|
||||
action={basicAuthConnector}
|
||||
errors={{}}
|
||||
readOnly={false}
|
||||
isLoading={false}
|
||||
editActionSecrets={editActionSecretsMock}
|
||||
editActionConfig={editActionConfigMock}
|
||||
/>
|
||||
</IntlProvider>
|
||||
<ConnectorFormTestProvider connector={connector}>
|
||||
<IntlProvider locale="en">
|
||||
<Credentials isOAuth={true} readOnly={false} isLoading={false} />
|
||||
</IntlProvider>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('switch'));
|
||||
|
||||
expect(editActionConfigMock).toHaveBeenCalledWith('isOAuth', true);
|
||||
expect(editActionSecretsMock).toHaveBeenCalledWith('username', null);
|
||||
expect(editActionSecretsMock).toHaveBeenCalledWith('password', null);
|
||||
|
||||
expect((await screen.findByRole('switch')).getAttribute('aria-checked')).toEqual('true');
|
||||
|
||||
expect(screen.getByLabelText('ServiceNow instance URL')).toBeInTheDocument();
|
||||
|
|
|
@ -5,56 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
} from '@elastic/eui';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import React, { memo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { ToggleField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import * as i18n from './translations';
|
||||
import { ServiceNowActionConnector } from './types';
|
||||
import { CredentialsApiUrl } from './credentials_api_url';
|
||||
import { CredentialsAuth, OAuth } from './auth_types';
|
||||
|
||||
interface Props {
|
||||
action: ActionConnectorFieldsProps<ServiceNowActionConnector>['action'];
|
||||
errors: ActionConnectorFieldsProps<ServiceNowActionConnector>['errors'];
|
||||
isOAuth: boolean;
|
||||
readOnly: boolean;
|
||||
isLoading: boolean;
|
||||
editActionSecrets: ActionConnectorFieldsProps<ServiceNowActionConnector>['editActionSecrets'];
|
||||
editActionConfig: ActionConnectorFieldsProps<ServiceNowActionConnector>['editActionConfig'];
|
||||
}
|
||||
|
||||
const CredentialsComponent: React.FC<Props> = ({
|
||||
action,
|
||||
errors,
|
||||
readOnly,
|
||||
isLoading,
|
||||
editActionSecrets,
|
||||
editActionConfig,
|
||||
}) => {
|
||||
const [isOAuth, setIsOAuth] = useState(action.config.isOAuth);
|
||||
|
||||
const switchIsOAuth = (e: EuiSwitchEvent) => {
|
||||
setIsOAuth(e.target.checked);
|
||||
editActionConfig('isOAuth', e.target.checked);
|
||||
if (!e.target.checked) {
|
||||
editActionConfig('clientId', null);
|
||||
editActionConfig('userIdentifierValue', null);
|
||||
editActionConfig('jwtKeyId', null);
|
||||
editActionSecrets('clientSecret', null);
|
||||
editActionSecrets('privateKey', null);
|
||||
editActionSecrets('privateKeyPassword', null);
|
||||
} else {
|
||||
editActionSecrets('username', null);
|
||||
editActionSecrets('password', null);
|
||||
}
|
||||
};
|
||||
|
||||
const CredentialsComponent: React.FC<Props> = ({ readOnly, isLoading, isOAuth }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column">
|
||||
|
@ -62,13 +27,7 @@ const CredentialsComponent: React.FC<Props> = ({
|
|||
<EuiTitle size="xxs">
|
||||
<h4>{i18n.SN_INSTANCE_LABEL}</h4>
|
||||
</EuiTitle>
|
||||
<CredentialsApiUrl
|
||||
action={action}
|
||||
errors={errors}
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
editActionConfig={editActionConfig}
|
||||
/>
|
||||
<CredentialsApiUrl readOnly={readOnly} isLoading={isLoading} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
|
@ -80,31 +39,24 @@ const CredentialsComponent: React.FC<Props> = ({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
label={i18n.IS_OAUTH}
|
||||
disabled={readOnly}
|
||||
checked={isOAuth || false}
|
||||
onChange={switchIsOAuth}
|
||||
<UseField
|
||||
path="config.isOAuth"
|
||||
component={ToggleField}
|
||||
config={{ defaultValue: false }}
|
||||
componentProps={{
|
||||
hasEmptyLabelSpace: true,
|
||||
euiFieldProps: {
|
||||
label: i18n.IS_OAUTH,
|
||||
disabled: readOnly,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexItem>
|
||||
{isOAuth ? (
|
||||
<OAuth
|
||||
action={action}
|
||||
errors={errors}
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
editActionSecrets={editActionSecrets}
|
||||
editActionConfig={editActionConfig}
|
||||
/>
|
||||
<OAuth readOnly={readOnly} isLoading={isLoading} />
|
||||
) : (
|
||||
<CredentialsAuth
|
||||
action={action}
|
||||
errors={errors}
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
editActionSecrets={editActionSecrets}
|
||||
/>
|
||||
<CredentialsAuth readOnly={readOnly} isLoading={isLoading} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
|
|
|
@ -5,40 +5,26 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiFormRow, EuiLink, EuiFieldText, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFormRow, EuiLink, EuiSpacer } from '@elastic/eui';
|
||||
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import type { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import * as i18n from './translations';
|
||||
import type { ServiceNowActionConnector } from './types';
|
||||
import { isFieldInvalid } from './helpers';
|
||||
|
||||
interface Props {
|
||||
action: ActionConnectorFieldsProps<ServiceNowActionConnector>['action'];
|
||||
errors: ActionConnectorFieldsProps<ServiceNowActionConnector>['errors'];
|
||||
readOnly: boolean;
|
||||
isLoading: boolean;
|
||||
editActionConfig: ActionConnectorFieldsProps<ServiceNowActionConnector>['editActionConfig'];
|
||||
pathPrefix?: string;
|
||||
}
|
||||
|
||||
const CredentialsApiUrlComponent: React.FC<Props> = ({
|
||||
action,
|
||||
errors,
|
||||
isLoading,
|
||||
readOnly,
|
||||
editActionConfig,
|
||||
}) => {
|
||||
const { urlField } = fieldValidators;
|
||||
|
||||
const CredentialsApiUrlComponent: React.FC<Props> = ({ isLoading, readOnly, pathPrefix = '' }) => {
|
||||
const { docLinks } = useKibana().services;
|
||||
const { apiUrl } = action.config;
|
||||
|
||||
const isApiUrlInvalid = isFieldInvalid(apiUrl, errors.apiUrl);
|
||||
|
||||
const onChangeApiUrlEvent = useCallback(
|
||||
(event?: React.ChangeEvent<HTMLInputElement>) =>
|
||||
editActionConfig('apiUrl', event?.target.value ?? ''),
|
||||
[editActionConfig]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -58,30 +44,26 @@ const CredentialsApiUrlComponent: React.FC<Props> = ({
|
|||
</p>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFormRow
|
||||
id="apiUrl"
|
||||
fullWidth
|
||||
error={errors.apiUrl}
|
||||
isInvalid={isApiUrlInvalid}
|
||||
label={i18n.API_URL_LABEL}
|
||||
helpText={i18n.API_URL_HELPTEXT}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isApiUrlInvalid}
|
||||
name="apiUrl"
|
||||
readOnly={readOnly}
|
||||
value={apiUrl || ''} // Needed to prevent uncontrolled input error when value is undefined
|
||||
data-test-subj="credentialsApiUrlFromInput"
|
||||
onChange={onChangeApiUrlEvent}
|
||||
onBlur={() => {
|
||||
if (!apiUrl) {
|
||||
onChangeApiUrlEvent();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path={`${pathPrefix}config.apiUrl`}
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.API_URL_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: urlField(i18n.API_URL_INVALID),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'credentialsApiUrlFromInput',
|
||||
isLoading,
|
||||
readOnly,
|
||||
disabled: readOnly || isLoading,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,8 +16,9 @@ export const DEFAULT_CORRELATION_ID = '{{rule.id}}:{{alert.id}}';
|
|||
export const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] =>
|
||||
choices.map((choice) => ({ value: choice.value, text: choice.label }));
|
||||
|
||||
export const isRESTApiError = (res: AppInfo | RESTApiError): res is RESTApiError =>
|
||||
(res as RESTApiError).error != null || (res as RESTApiError).status === 'failure';
|
||||
export const isRESTApiError = (res: AppInfo | RESTApiError | undefined): res is RESTApiError =>
|
||||
res != null &&
|
||||
((res as RESTApiError).error != null || (res as RESTApiError).status === 'failure');
|
||||
|
||||
export const isFieldInvalid = (
|
||||
field: string | undefined | null,
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { ServiceNowActionConnector } from './types';
|
||||
import { registrationServicesMock } from '../../../../mocks';
|
||||
|
||||
const SERVICENOW_ITSM_ACTION_TYPE_ID = '.servicenow';
|
||||
|
@ -34,196 +33,6 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('servicenow connector validation', () => {
|
||||
[
|
||||
SERVICENOW_ITSM_ACTION_TYPE_ID,
|
||||
SERVICENOW_SIR_ACTION_TYPE_ID,
|
||||
SERVICENOW_ITOM_ACTION_TYPE_ID,
|
||||
].forEach((id) => {
|
||||
test(`${id}: connector validation succeeds when connector config is valid`, async () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: id,
|
||||
name: 'ServiceNow',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
isOAuth: false,
|
||||
apiUrl: 'https://dev94428.service-now.com/',
|
||||
usesTableApi: false,
|
||||
},
|
||||
} as ServiceNowActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
clientId: [],
|
||||
jwtKeyId: [],
|
||||
userIdentifierValue: [],
|
||||
usesTableApi: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
username: [],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
privateKey: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test(`${id}: connector validation fails when connector config is not valid`, async () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
username: 'user',
|
||||
},
|
||||
id,
|
||||
actionTypeId: id,
|
||||
name: 'servicenow',
|
||||
config: {},
|
||||
} as unknown as ServiceNowActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: ['URL is required.'],
|
||||
usesTableApi: [],
|
||||
clientId: [],
|
||||
jwtKeyId: [],
|
||||
userIdentifierValue: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
username: [],
|
||||
password: ['Password is required.'],
|
||||
clientSecret: [],
|
||||
privateKey: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('servicenow connector validation for OAuth', () => {
|
||||
[
|
||||
SERVICENOW_ITSM_ACTION_TYPE_ID,
|
||||
SERVICENOW_SIR_ACTION_TYPE_ID,
|
||||
SERVICENOW_ITOM_ACTION_TYPE_ID,
|
||||
].forEach((id) => {
|
||||
const mockConnector = ({
|
||||
actionTypeId = '',
|
||||
clientSecret = 'clientSecret',
|
||||
privateKey = 'privateKey',
|
||||
privateKeyPassword = 'privateKeyPassword',
|
||||
isOAuth = true,
|
||||
apiUrl = 'https://dev94428.service-now.com/',
|
||||
usesTableApi = false,
|
||||
clientId = 'clientId',
|
||||
jwtKeyId = 'jwtKeyId',
|
||||
userIdentifierValue = 'userIdentifierValue',
|
||||
}: {
|
||||
actionTypeId?: string | null;
|
||||
clientSecret?: string | null;
|
||||
privateKey?: string | null;
|
||||
privateKeyPassword?: string | null;
|
||||
isOAuth?: boolean;
|
||||
apiUrl?: string | null;
|
||||
usesTableApi?: boolean | null;
|
||||
clientId?: string | null;
|
||||
jwtKeyId?: string | null;
|
||||
userIdentifierValue?: string | null;
|
||||
}) =>
|
||||
({
|
||||
secrets: {
|
||||
clientSecret,
|
||||
privateKey,
|
||||
privateKeyPassword,
|
||||
},
|
||||
id,
|
||||
actionTypeId,
|
||||
name: 'servicenow',
|
||||
config: {
|
||||
isOAuth,
|
||||
apiUrl,
|
||||
usesTableApi,
|
||||
clientId,
|
||||
jwtKeyId,
|
||||
userIdentifierValue,
|
||||
},
|
||||
} as unknown as ServiceNowActionConnector);
|
||||
|
||||
test(`${id}: valid OAuth Connector`, async () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionConnector = mockConnector({ actionTypeId: id });
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
usesTableApi: [],
|
||||
clientId: [],
|
||||
jwtKeyId: [],
|
||||
userIdentifierValue: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
username: [],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
privateKey: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test(`${id}: has invalid fields`, async () => {
|
||||
const actionTypeModel = actionTypeRegistry.get(id);
|
||||
const actionConnector = mockConnector({
|
||||
actionTypeId: id,
|
||||
apiUrl: null,
|
||||
clientId: null,
|
||||
jwtKeyId: null,
|
||||
userIdentifierValue: null,
|
||||
clientSecret: null,
|
||||
privateKey: null,
|
||||
privateKeyPassword: null,
|
||||
});
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: ['URL is required.'],
|
||||
usesTableApi: [],
|
||||
clientId: ['Client ID is required.'],
|
||||
jwtKeyId: ['JWT Verifier Key ID is required.'],
|
||||
userIdentifierValue: ['User Identifier is required.'],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
username: [],
|
||||
password: [],
|
||||
clientSecret: ['Client Secret is required.'],
|
||||
privateKey: ['Private Key is required.'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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`, async () => {
|
||||
|
|
|
@ -7,20 +7,14 @@
|
|||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionTypeModel, GenericValidationResult } from '../../../../types';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
ConnectorValidationResult,
|
||||
GenericValidationResult,
|
||||
} from '../../../../types';
|
||||
import {
|
||||
ServiceNowActionConnector,
|
||||
ServiceNowConfig,
|
||||
ServiceNowITOMActionParams,
|
||||
ServiceNowITSMActionParams,
|
||||
ServiceNowSecrets,
|
||||
ServiceNowSIRActionParams,
|
||||
} from './types';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
import { getConnectorDescriptiveTitle, getSelectedConnectorIcon } from './helpers';
|
||||
|
||||
export const SERVICENOW_ITOM_TITLE = i18n.translate(
|
||||
|
@ -65,84 +59,6 @@ export const SERVICENOW_SIR_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const validateConnector = async (
|
||||
action: ServiceNowActionConnector
|
||||
): Promise<
|
||||
ConnectorValidationResult<
|
||||
Omit<ServiceNowConfig, 'isOAuth'>,
|
||||
Omit<ServiceNowSecrets, 'privateKeyPassword'>
|
||||
>
|
||||
> => {
|
||||
const translations = await import('./translations');
|
||||
|
||||
const configErrors = {
|
||||
apiUrl: new Array<string>(),
|
||||
usesTableApi: new Array<string>(),
|
||||
clientId: new Array<string>(),
|
||||
userIdentifierValue: new Array<string>(),
|
||||
jwtKeyId: new Array<string>(),
|
||||
};
|
||||
|
||||
const secretsErrors = {
|
||||
username: new Array<string>(),
|
||||
password: new Array<string>(),
|
||||
clientSecret: new Array<string>(),
|
||||
privateKey: new Array<string>(),
|
||||
};
|
||||
|
||||
if (!action.config.apiUrl) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED];
|
||||
}
|
||||
|
||||
if (action.config.apiUrl) {
|
||||
if (!isValidUrl(action.config.apiUrl)) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID];
|
||||
} else if (!isValidUrl(action.config.apiUrl, 'https:')) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS];
|
||||
}
|
||||
}
|
||||
|
||||
if (action.config.isOAuth) {
|
||||
if (!action.config.clientId) {
|
||||
configErrors.clientId = [...configErrors.clientId, translations.CLIENTID_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.config.userIdentifierValue) {
|
||||
configErrors.userIdentifierValue = [
|
||||
...configErrors.userIdentifierValue,
|
||||
translations.USER_EMAIL_REQUIRED,
|
||||
];
|
||||
}
|
||||
|
||||
if (!action.config.jwtKeyId) {
|
||||
configErrors.jwtKeyId = [...configErrors.jwtKeyId, translations.KEYID_REQUIRED];
|
||||
}
|
||||
|
||||
if (!action.secrets.clientSecret) {
|
||||
secretsErrors.clientSecret = [
|
||||
...secretsErrors.clientSecret,
|
||||
translations.CLIENTSECRET_REQUIRED,
|
||||
];
|
||||
}
|
||||
|
||||
if (!action.secrets.privateKey) {
|
||||
secretsErrors.privateKey = [...secretsErrors.privateKey, translations.PRIVATE_KEY_REQUIRED];
|
||||
}
|
||||
} else {
|
||||
if (!action.secrets.username) {
|
||||
secretsErrors.username = [...secretsErrors.username, translations.USERNAME_REQUIRED];
|
||||
}
|
||||
if (!action.secrets.password) {
|
||||
secretsErrors.password = [...secretsErrors.password, translations.PASSWORD_REQUIRED];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
config: { errors: configErrors },
|
||||
secrets: { errors: secretsErrors },
|
||||
};
|
||||
};
|
||||
|
||||
export function getServiceNowITSMActionType(): ActionTypeModel<
|
||||
ServiceNowConfig,
|
||||
ServiceNowSecrets,
|
||||
|
@ -153,7 +69,6 @@ export function getServiceNowITSMActionType(): ActionTypeModel<
|
|||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: SERVICENOW_ITSM_DESC,
|
||||
actionTypeTitle: SERVICENOW_ITSM_TITLE,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./servicenow_connectors')),
|
||||
validateParams: async (
|
||||
actionParams: ServiceNowITSMActionParams
|
||||
|
@ -192,7 +107,6 @@ export function getServiceNowSIRActionType(): ActionTypeModel<
|
|||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: SERVICENOW_SIR_DESC,
|
||||
actionTypeTitle: SERVICENOW_SIR_TITLE,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./servicenow_connectors')),
|
||||
validateParams: async (
|
||||
actionParams: ServiceNowSIRActionParams
|
||||
|
@ -231,7 +145,6 @@ export function getServiceNowITOMActionType(): ActionTypeModel<
|
|||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: SERVICENOW_ITOM_DESC,
|
||||
actionTypeTitle: SERVICENOW_ITOM_TITLE,
|
||||
validateConnector,
|
||||
actionConnectorFields: lazy(() => import('./servicenow_connectors_no_app')),
|
||||
validateParams: async (
|
||||
actionParams: ServiceNowITOMActionParams
|
||||
|
|
|
@ -6,15 +6,18 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from '@testing-library/react';
|
||||
import { act, within } from '@testing-library/react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { render, act as reactAct } from '@testing-library/react';
|
||||
|
||||
import { ConnectorValidationFunc } from '../../../../types';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ActionConnectorFieldsSetCallbacks } from '../../../../types';
|
||||
import { updateActionConnector } from '../../../lib/action_connector_api';
|
||||
import ServiceNowConnectorFields from './servicenow_connectors';
|
||||
import { ServiceNowActionConnector } from './types';
|
||||
import { getAppInfo } from './api';
|
||||
import { ConnectorFormTestProvider } from '../test_utils';
|
||||
import { mount } from 'enzyme';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../lib/action_connector_api');
|
||||
|
@ -26,41 +29,57 @@ const updateActionConnectorMock = updateActionConnector as jest.Mock;
|
|||
|
||||
describe('ServiceNowActionConnectorFields renders', () => {
|
||||
const usesTableApiConnector = {
|
||||
id: 'test',
|
||||
actionTypeId: '.servicenow',
|
||||
isDeprecated: true,
|
||||
name: 'SN',
|
||||
config: {
|
||||
apiUrl: 'https://test.com',
|
||||
usesTableApi: true,
|
||||
},
|
||||
secrets: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.servicenow',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: true,
|
||||
name: 'SN',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
usesTableApi: true,
|
||||
},
|
||||
} as ServiceNowActionConnector;
|
||||
};
|
||||
|
||||
const usesImportSetApiConnector = {
|
||||
...usesTableApiConnector,
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
...usesTableApiConnector.config,
|
||||
isOAuth: false,
|
||||
usesTableApi: false,
|
||||
},
|
||||
} as ServiceNowActionConnector;
|
||||
};
|
||||
|
||||
const usesImportSetApiConnectorOauth = {
|
||||
...usesTableApiConnector,
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
...usesTableApiConnector.config,
|
||||
isOAuth: true,
|
||||
usesTableApi: false,
|
||||
clientId: 'test-id',
|
||||
userIdentifierValue: 'email',
|
||||
jwtKeyId: 'test-id',
|
||||
},
|
||||
secrets: {
|
||||
clientSecret: 'secret',
|
||||
privateKey: 'secret-key',
|
||||
privateKeyPassword: 'secret-pass',
|
||||
},
|
||||
};
|
||||
|
||||
test('alerting servicenow connector fields are rendered', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesTableApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={usesTableApiConnector}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-servicenow-username-form-input"]').length > 0
|
||||
|
@ -74,16 +93,13 @@ describe('ServiceNowActionConnectorFields renders', () => {
|
|||
|
||||
test('case specific servicenow connector fields is rendered', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesImportSetApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
consumer={'case'}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={usesImportSetApiConnector}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="credentialsApiUrlFromInput"]').length > 0).toBeTruthy();
|
||||
|
@ -92,70 +108,6 @@ describe('ServiceNowActionConnectorFields renders', () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display a message on create to remember credentials', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.servicenow',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as ServiceNowActionConnector;
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('should display a message for missing secrets after import', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.servicenow',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: true,
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as ServiceNowActionConnector;
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should display a message on edit to re-enter credentials', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesTableApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
|
||||
describe('Elastic certified ServiceNow application', () => {
|
||||
const { services } = useKibanaMock();
|
||||
const applicationInfoData = {
|
||||
|
@ -164,14 +116,11 @@ describe('ServiceNowActionConnectorFields renders', () => {
|
|||
version: '1.0.0',
|
||||
};
|
||||
|
||||
let beforeActionConnectorSaveFn: () => Promise<void>;
|
||||
const setCallbacks = (({
|
||||
beforeActionConnectorSave,
|
||||
}: {
|
||||
beforeActionConnectorSave: () => Promise<void>;
|
||||
}) => {
|
||||
beforeActionConnectorSaveFn = beforeActionConnectorSave;
|
||||
}) as ActionConnectorFieldsSetCallbacks;
|
||||
let preSubmitValidator: ConnectorValidationFunc;
|
||||
|
||||
const registerPreSubmitValidator = (validator: ConnectorValidationFunc) => {
|
||||
preSubmitValidator = validator;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -179,16 +128,15 @@ describe('ServiceNowActionConnectorFields renders', () => {
|
|||
|
||||
test('should render the correct callouts when the connectors needs the application', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesImportSetApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={usesImportSetApiConnector}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={registerPreSubmitValidator}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="snInstallationCallout"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="snDeprecatedCallout"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeFalsy();
|
||||
|
@ -196,16 +144,15 @@ describe('ServiceNowActionConnectorFields renders', () => {
|
|||
|
||||
test('should render the correct callouts if the connector uses the table API', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesTableApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={usesTableApiConnector}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={registerPreSubmitValidator}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="snInstallationCallout"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="snDeprecatedCallout"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeFalsy();
|
||||
|
@ -215,19 +162,17 @@ describe('ServiceNowActionConnectorFields renders', () => {
|
|||
getAppInfoMock.mockResolvedValue(applicationInfoData);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesImportSetApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={setCallbacks}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={usesImportSetApiConnector}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={registerPreSubmitValidator}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await beforeActionConnectorSaveFn();
|
||||
await preSubmitValidator();
|
||||
});
|
||||
|
||||
expect(getAppInfoMock).toHaveBeenCalledTimes(1);
|
||||
|
@ -236,19 +181,17 @@ describe('ServiceNowActionConnectorFields renders', () => {
|
|||
|
||||
test('should NOT get application information when the connector uses the old API', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesTableApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={setCallbacks}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={usesTableApiConnector}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={registerPreSubmitValidator}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await beforeActionConnectorSaveFn();
|
||||
await preSubmitValidator();
|
||||
});
|
||||
|
||||
expect(getAppInfoMock).toHaveBeenCalledTimes(0);
|
||||
|
@ -256,110 +199,113 @@ describe('ServiceNowActionConnectorFields renders', () => {
|
|||
});
|
||||
|
||||
test('should render error when save failed', async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
const errorMessage = 'request failed';
|
||||
getAppInfoMock.mockRejectedValueOnce(new Error(errorMessage));
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesImportSetApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={setCallbacks}
|
||||
isEdit={false}
|
||||
/>
|
||||
mountWithIntl(
|
||||
<ConnectorFormTestProvider connector={usesImportSetApiConnector}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={registerPreSubmitValidator}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await expect(
|
||||
// The async is needed so the act will finished before asserting for the callout
|
||||
async () => await act(async () => await beforeActionConnectorSaveFn())
|
||||
).rejects.toThrow(errorMessage);
|
||||
expect(getAppInfoMock).toHaveBeenCalledTimes(1);
|
||||
await act(async () => {
|
||||
const res = await preSubmitValidator();
|
||||
const messageWrapper = mount(<>{res?.message}</>);
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeTruthy();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="snApplicationCallout"]')
|
||||
.first()
|
||||
.text()
|
||||
.includes(errorMessage)
|
||||
).toBeTruthy();
|
||||
expect(getAppInfoMock).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
messageWrapper.find('[data-test-subj="snApplicationCallout"]').exists()
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
messageWrapper
|
||||
.find('[data-test-subj="snApplicationCallout"]')
|
||||
.first()
|
||||
.text()
|
||||
.includes(errorMessage)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('should render error when the response is a REST api error', async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
const errorMessage = 'request failed';
|
||||
getAppInfoMock.mockResolvedValue({ error: { message: errorMessage }, status: 'failure' });
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesImportSetApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={setCallbacks}
|
||||
isEdit={false}
|
||||
/>
|
||||
mountWithIntl(
|
||||
<ConnectorFormTestProvider connector={usesImportSetApiConnector}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={registerPreSubmitValidator}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await expect(
|
||||
// The async is needed so the act will finished before asserting for the callout
|
||||
async () => await act(async () => await beforeActionConnectorSaveFn())
|
||||
).rejects.toThrow(errorMessage);
|
||||
expect(getAppInfoMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeTruthy();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="snApplicationCallout"]')
|
||||
.first()
|
||||
.text()
|
||||
.includes(errorMessage)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should migrate the deprecated connector when the application throws', async () => {
|
||||
getAppInfoMock.mockResolvedValue(applicationInfoData);
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesTableApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={setCallbacks}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="update-connector-btn"]').exists()).toBeTruthy();
|
||||
wrapper
|
||||
.find('[data-test-subj="update-connector-btn"]')
|
||||
.first()
|
||||
.find('button')
|
||||
.simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
// Update the connector
|
||||
wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().simulate('click');
|
||||
const res = await preSubmitValidator();
|
||||
const messageWrapper = mount(<>{res?.message}</>);
|
||||
|
||||
expect(getAppInfoMock).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
messageWrapper.find('[data-test-subj="snApplicationCallout"]').exists()
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
messageWrapper
|
||||
.find('[data-test-subj="snApplicationCallout"]')
|
||||
.first()
|
||||
.text()
|
||||
.includes(errorMessage)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('should migrate the deprecated connector correctly', async () => {
|
||||
getAppInfoMock.mockResolvedValue(applicationInfoData);
|
||||
updateActionConnectorMock.mockResolvedValue({ isDeprecated: false });
|
||||
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={usesTableApiConnector}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={registerPreSubmitValidator}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await reactAct(async () => {
|
||||
userEvent.click(getByTestId('update-connector-btn'));
|
||||
});
|
||||
|
||||
await reactAct(async () => {
|
||||
const updateConnectorForm = getByTestId('updateConnectorForm');
|
||||
const urlInput = within(updateConnectorForm).getByTestId('credentialsApiUrlFromInput');
|
||||
const usernameInput = within(updateConnectorForm).getByTestId(
|
||||
'connector-servicenow-username-form-input'
|
||||
);
|
||||
const passwordInput = within(updateConnectorForm).getByTestId(
|
||||
'connector-servicenow-password-form-input'
|
||||
);
|
||||
|
||||
await userEvent.type(urlInput, 'https://example.com', { delay: 100 });
|
||||
await userEvent.type(usernameInput, 'user', { delay: 100 });
|
||||
await userEvent.type(passwordInput, 'pass', { delay: 100 });
|
||||
userEvent.click(within(updateConnectorForm).getByTestId('snUpdateInstallationSubmit'));
|
||||
});
|
||||
|
||||
expect(getAppInfoMock).toHaveBeenCalledTimes(1);
|
||||
expect(updateActionConnectorMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: usesTableApiConnector.id,
|
||||
connector: {
|
||||
name: usesTableApiConnector.name,
|
||||
config: { ...usesTableApiConnector.config, usesTableApi: false },
|
||||
secrets: usesTableApiConnector.secrets,
|
||||
config: { apiUrl: 'https://example.com', usesTableApi: false },
|
||||
id: 'test',
|
||||
name: 'SN',
|
||||
secrets: { password: 'pass', username: 'user' },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -369,100 +315,181 @@ describe('ServiceNowActionConnectorFields renders', () => {
|
|||
title: 'SN connector updated',
|
||||
});
|
||||
|
||||
// The flyout is closed
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeFalsy();
|
||||
expect(queryByTestId('updateConnectorForm')).toBe(null);
|
||||
expect(queryByTestId('snDeprecatedCallout')).toBe(null);
|
||||
expect(getByTestId('snInstallationCallout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT migrate the deprecated connector when there is an error', async () => {
|
||||
const errorMessage = 'request failed';
|
||||
getAppInfoMock.mockRejectedValueOnce(new Error(errorMessage));
|
||||
updateActionConnectorMock.mockResolvedValue({ isDeprecated: false });
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesTableApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={setCallbacks}
|
||||
isEdit={false}
|
||||
/>
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={usesTableApiConnector}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={registerPreSubmitValidator}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="update-connector-btn"]').exists()).toBeTruthy();
|
||||
wrapper
|
||||
.find('[data-test-subj="update-connector-btn"]')
|
||||
.first()
|
||||
.find('button')
|
||||
.simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeTruthy();
|
||||
await reactAct(async () => {
|
||||
userEvent.click(getByTestId('update-connector-btn'));
|
||||
});
|
||||
|
||||
// The async is needed so the act will finished before asserting for the callout
|
||||
await act(async () => {
|
||||
wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().simulate('click');
|
||||
await reactAct(async () => {
|
||||
const updateConnectorForm = getByTestId('updateConnectorForm');
|
||||
const urlInput = within(updateConnectorForm).getByTestId('credentialsApiUrlFromInput');
|
||||
const usernameInput = within(updateConnectorForm).getByTestId(
|
||||
'connector-servicenow-username-form-input'
|
||||
);
|
||||
const passwordInput = within(updateConnectorForm).getByTestId(
|
||||
'connector-servicenow-password-form-input'
|
||||
);
|
||||
|
||||
await userEvent.type(urlInput, 'https://example.com', { delay: 100 });
|
||||
await userEvent.type(usernameInput, 'user', { delay: 100 });
|
||||
await userEvent.type(passwordInput, 'pass', { delay: 100 });
|
||||
userEvent.click(within(updateConnectorForm).getByTestId('snUpdateInstallationSubmit'));
|
||||
});
|
||||
|
||||
expect(getAppInfoMock).toHaveBeenCalledTimes(1);
|
||||
expect(updateActionConnectorMock).not.toHaveBeenCalled();
|
||||
|
||||
expect(services.notifications.toasts.addSuccess).not.toHaveBeenCalled();
|
||||
|
||||
// The flyout is still open
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeTruthy();
|
||||
|
||||
// The error message should be shown to the user
|
||||
expect(getByTestId('updateConnectorForm')).toBeInTheDocument();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="updateConnectorForm"] [data-test-subj="snApplicationCallout"]')
|
||||
.exists()
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="updateConnectorForm"] [data-test-subj="snApplicationCallout"]')
|
||||
.first()
|
||||
.text()
|
||||
.includes(errorMessage)
|
||||
).toBeTruthy();
|
||||
within(getByTestId('updateConnectorForm')).getByTestId('snApplicationCallout')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should set the usesTableApi to false when creating a connector', async () => {
|
||||
const newConnector = { ...usesTableApiConnector, config: {}, secrets: {} };
|
||||
const editActionConfig = jest.fn();
|
||||
const basicAuthTests: Array<[string, string]> = [
|
||||
['credentialsApiUrlFromInput', 'not-valid'],
|
||||
['connector-servicenow-username-form-input', ''],
|
||||
['connector-servicenow-password-form-input', ''],
|
||||
];
|
||||
|
||||
mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
// @ts-expect-error
|
||||
action={newConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={editActionConfig}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={setCallbacks}
|
||||
isEdit={false}
|
||||
/>
|
||||
const oauthTests: Array<[string, string]> = [
|
||||
['credentialsApiUrlFromInput', 'not-valid'],
|
||||
['connector-servicenow-client-id-form-input', ''],
|
||||
['connector-servicenow-user-identifier-form-input', ''],
|
||||
['connector-servicenow-jwt-key-id-form-input', ''],
|
||||
['connector-servicenow-client-secret-form-input', ''],
|
||||
['connector-servicenow-private-key-form-input', ''],
|
||||
];
|
||||
|
||||
it.each([[usesImportSetApiConnector], [usesImportSetApiConnectorOauth]])(
|
||||
'connector validation succeeds when connector config is valid',
|
||||
async (connector) => {
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: { ...connector }, isValid: true });
|
||||
}
|
||||
);
|
||||
|
||||
it('submits if the private key password is empty', async () => {
|
||||
const connector = {
|
||||
...usesImportSetApiConnectorOauth,
|
||||
secrets: {
|
||||
...usesImportSetApiConnectorOauth.secrets,
|
||||
clientSecret: 'secret',
|
||||
privateKey: 'secret-key',
|
||||
privateKeyPassword: '',
|
||||
},
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(editActionConfig).toHaveBeenCalledWith('usesTableApi', false);
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
const {
|
||||
secrets: { clientSecret, privateKey },
|
||||
...rest
|
||||
} = connector;
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
data: { ...rest, secrets: { clientSecret, privateKey } },
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('it should set the legacy attribute if it is not undefined', async () => {
|
||||
const editActionConfig = jest.fn();
|
||||
|
||||
mountWithIntl(
|
||||
<ServiceNowConnectorFields
|
||||
action={usesTableApiConnector}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={editActionConfig}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={setCallbacks}
|
||||
isEdit={false}
|
||||
/>
|
||||
it.each(basicAuthTests)('validates correctly %p', async (field, value) => {
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={usesImportSetApiConnector} onSubmit={onSubmit}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(editActionConfig).not.toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
|
||||
it.each(oauthTests)('validates correctly %p', async (field, value) => {
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={usesImportSetApiConnectorOauth} onSubmit={onSubmit}>
|
||||
<ServiceNowConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,157 +5,176 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { snExternalServiceConfig } from '@kbn/actions-plugin/common';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { useFormContext, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { ServiceNowActionConnector } from './types';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { DeprecatedCallout } from './deprecated_callout';
|
||||
import { useGetAppInfo } from './use_get_app_info';
|
||||
import { ApplicationRequiredCallout } from './application_required_callout';
|
||||
import { isRESTApiError } from './helpers';
|
||||
import { InstallationCallout } from './installation_callout';
|
||||
import { UpdateConnector } from './update_connector';
|
||||
import { UpdateConnector, UpdateConnectorFormSchema } from './update_connector';
|
||||
import { updateActionConnector } from '../../../lib/action_connector_api';
|
||||
import { Credentials } from './credentials';
|
||||
import * as i18n from './translations';
|
||||
import { ServiceNowActionConnector, ServiceNowConfig, ServiceNowSecrets } from './types';
|
||||
import { HiddenField } from '../../hidden_field';
|
||||
import { ConnectorFormSchema } from '../../../sections/action_connector_form/types';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ServiceNowConnectorFields as default };
|
||||
|
||||
const ServiceNowConnectorFields: React.FC<
|
||||
ActionConnectorFieldsProps<ServiceNowActionConnector>
|
||||
> = ({ action, editActionSecrets, editActionConfig, errors, readOnly, setCallbacks }) => {
|
||||
const ServiceNowConnectorFields: React.FC<ActionConnectorFieldsProps> = ({
|
||||
readOnly,
|
||||
registerPreSubmitValidator,
|
||||
isEdit,
|
||||
}) => {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const { config, secrets } = action;
|
||||
const requiresNewApplication = !action.isDeprecated;
|
||||
const { updateFieldValues } = useFormContext();
|
||||
const [{ id, isDeprecated, actionTypeId, name, config, secrets }] = useFormData<
|
||||
ConnectorFormSchema<ServiceNowConfig, ServiceNowSecrets>
|
||||
>({
|
||||
watch: [
|
||||
'id',
|
||||
'isDeprecated',
|
||||
'actionTypeId',
|
||||
'name',
|
||||
'config.apiUrl',
|
||||
'config.isOAuth',
|
||||
'secrets.username',
|
||||
'secrets.password',
|
||||
],
|
||||
});
|
||||
|
||||
const requiresNewApplication = isDeprecated != null ? !isDeprecated : true;
|
||||
const { isOAuth = false } = config ?? {};
|
||||
|
||||
const action = useMemo(
|
||||
() => ({
|
||||
name,
|
||||
actionTypeId,
|
||||
config,
|
||||
secrets,
|
||||
}),
|
||||
[name, actionTypeId, config, secrets]
|
||||
) as ServiceNowActionConnector;
|
||||
|
||||
const [showUpdateConnector, setShowUpdateConnector] = useState(false);
|
||||
|
||||
const [updateErrorMessage, setUpdateErrorMessage] = useState<string | null>(null);
|
||||
const { fetchAppInfo, isLoading } = useGetAppInfo({
|
||||
actionTypeId: action.actionTypeId,
|
||||
actionTypeId,
|
||||
http,
|
||||
});
|
||||
|
||||
const [showApplicationRequiredCallout, setShowApplicationRequiredCallout] =
|
||||
useState<boolean>(false);
|
||||
const [applicationInfoErrorMsg, setApplicationInfoErrorMsg] = useState<string | null>(null);
|
||||
const getApplicationInfo = useCallback(
|
||||
async (connector: ServiceNowActionConnector) => {
|
||||
try {
|
||||
const res = await fetchAppInfo(connector);
|
||||
if (isRESTApiError(res)) {
|
||||
throw new Error(res.error?.message ?? i18n.UNKNOWN);
|
||||
}
|
||||
|
||||
const getApplicationInfo = useCallback(async () => {
|
||||
setShowApplicationRequiredCallout(false);
|
||||
setApplicationInfoErrorMsg(null);
|
||||
|
||||
try {
|
||||
const res = await fetchAppInfo(action);
|
||||
if (isRESTApiError(res)) {
|
||||
throw new Error(res.error?.message ?? i18n.UNKNOWN);
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
[fetchAppInfo]
|
||||
);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
setShowApplicationRequiredCallout(true);
|
||||
setApplicationInfoErrorMsg(e.message);
|
||||
// We need to throw here so the connector will be not be saved.
|
||||
throw e;
|
||||
}
|
||||
}, [action, fetchAppInfo]);
|
||||
|
||||
const beforeActionConnectorSave = useCallback(async () => {
|
||||
const preSubmitValidator = useCallback(async () => {
|
||||
if (requiresNewApplication) {
|
||||
await getApplicationInfo();
|
||||
try {
|
||||
await getApplicationInfo(action);
|
||||
} catch (error) {
|
||||
return {
|
||||
message: (
|
||||
<ApplicationRequiredCallout
|
||||
appId={actionTypeId != null ? snExternalServiceConfig[actionTypeId]?.appId : ''}
|
||||
message={error.message}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [getApplicationInfo, requiresNewApplication]);
|
||||
}, [action, actionTypeId, getApplicationInfo, requiresNewApplication]);
|
||||
|
||||
useEffect(
|
||||
() => setCallbacks({ beforeActionConnectorSave }),
|
||||
[beforeActionConnectorSave, setCallbacks]
|
||||
() => registerPreSubmitValidator(preSubmitValidator),
|
||||
[preSubmitValidator, registerPreSubmitValidator]
|
||||
);
|
||||
|
||||
const onMigrateClick = useCallback(() => setShowUpdateConnector(true), []);
|
||||
const onModalCancel = useCallback(() => setShowUpdateConnector(false), []);
|
||||
|
||||
const onUpdateConnectorConfirm = useCallback(async () => {
|
||||
try {
|
||||
await getApplicationInfo();
|
||||
const onUpdateConnectorConfirm = useCallback(
|
||||
async (updatedConnector: UpdateConnectorFormSchema['updatedConnector']) => {
|
||||
const connectorToUpdate = {
|
||||
name: name ?? '',
|
||||
config: { ...updatedConnector.config, usesTableApi: false },
|
||||
secrets: { ...updatedConnector.secrets },
|
||||
id: id ?? '',
|
||||
};
|
||||
|
||||
await updateActionConnector({
|
||||
http,
|
||||
connector: {
|
||||
name: action.name,
|
||||
config: { ...config, usesTableApi: false },
|
||||
secrets: { ...secrets },
|
||||
},
|
||||
id: action.id,
|
||||
});
|
||||
try {
|
||||
await getApplicationInfo({
|
||||
...connectorToUpdate,
|
||||
isDeprecated,
|
||||
isPreconfigured: false,
|
||||
actionTypeId,
|
||||
});
|
||||
|
||||
editActionConfig('usesTableApi', false);
|
||||
setShowUpdateConnector(false);
|
||||
const res = await updateActionConnector({
|
||||
http,
|
||||
connector: connectorToUpdate,
|
||||
id: id ?? '',
|
||||
});
|
||||
|
||||
toasts.addSuccess({
|
||||
title: i18n.UPDATE_SUCCESS_TOAST_TITLE(action.name),
|
||||
text: i18n.UPDATE_SUCCESS_TOAST_TEXT,
|
||||
});
|
||||
} catch (err) {
|
||||
/**
|
||||
* getApplicationInfo may throw an error if the request
|
||||
* fails or if there is a REST api error.
|
||||
*
|
||||
* We silent the errors as a callout will show and inform the user
|
||||
*/
|
||||
}
|
||||
}, [getApplicationInfo, http, action.name, action.id, secrets, config, editActionConfig, toasts]);
|
||||
toasts.addSuccess({
|
||||
title: i18n.UPDATE_SUCCESS_TOAST_TITLE(name ?? ''),
|
||||
text: i18n.UPDATE_SUCCESS_TOAST_TEXT,
|
||||
});
|
||||
|
||||
/**
|
||||
* Defaults the usesTableApi attribute to false
|
||||
* if it is not defined. The usesTableApi attribute
|
||||
* will be undefined only at the creation of
|
||||
* the connector.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (config.usesTableApi == null) {
|
||||
editActionConfig('usesTableApi', false);
|
||||
}
|
||||
});
|
||||
setShowUpdateConnector(false);
|
||||
|
||||
updateFieldValues({
|
||||
isDeprecated: res.isDeprecated,
|
||||
config: updatedConnector.config,
|
||||
});
|
||||
} catch (err) {
|
||||
setUpdateErrorMessage(err.message);
|
||||
}
|
||||
},
|
||||
[name, id, getApplicationInfo, isDeprecated, actionTypeId, http, updateFieldValues, toasts]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showUpdateConnector && (
|
||||
{actionTypeId && showUpdateConnector && (
|
||||
<UpdateConnector
|
||||
action={action}
|
||||
applicationInfoErrorMsg={applicationInfoErrorMsg}
|
||||
errors={errors}
|
||||
actionTypeId={actionTypeId}
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
editActionSecrets={editActionSecrets}
|
||||
editActionConfig={editActionConfig}
|
||||
updateErrorMessage={updateErrorMessage}
|
||||
onConfirm={onUpdateConnectorConfirm}
|
||||
onCancel={onModalCancel}
|
||||
isOAuth={isOAuth}
|
||||
/>
|
||||
)}
|
||||
{requiresNewApplication && (
|
||||
<InstallationCallout appId={snExternalServiceConfig[action.actionTypeId].appId ?? ''} />
|
||||
<InstallationCallout appId={snExternalServiceConfig[action.actionTypeId]?.appId ?? ''} />
|
||||
)}
|
||||
{!requiresNewApplication && <SpacedDeprecatedCallout onMigrate={onMigrateClick} />}
|
||||
<Credentials
|
||||
action={action}
|
||||
errors={errors}
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
editActionSecrets={editActionSecrets}
|
||||
editActionConfig={editActionConfig}
|
||||
/>
|
||||
{showApplicationRequiredCallout && requiresNewApplication && (
|
||||
<ApplicationRequiredCallout
|
||||
message={applicationInfoErrorMsg}
|
||||
appId={snExternalServiceConfig[action.actionTypeId].appId ?? ''}
|
||||
/>
|
||||
)}
|
||||
<HiddenField path={'config.usesTableApi'} config={{ defaultValue: false }} />
|
||||
<Credentials readOnly={readOnly} isLoading={isLoading} isOAuth={isOAuth} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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 { act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { AppMockRenderer, ConnectorFormTestProvider, createAppMockRenderer } from '../test_utils';
|
||||
import ServiceNowConnectorFieldsNoApp from './servicenow_connectors_no_app';
|
||||
|
||||
describe('ServiceNowActionConnectorFields renders', () => {
|
||||
const basicAuthConnector = {
|
||||
id: 'test',
|
||||
actionTypeId: '.servicenow',
|
||||
isDeprecated: true,
|
||||
name: 'SN',
|
||||
config: {
|
||||
apiUrl: 'https://test.com',
|
||||
isOAuth: false,
|
||||
usesTableApi: false,
|
||||
},
|
||||
secrets: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
};
|
||||
|
||||
const oauthConnector = {
|
||||
id: 'test',
|
||||
actionTypeId: '.servicenow',
|
||||
isDeprecated: true,
|
||||
name: 'SN',
|
||||
config: {
|
||||
apiUrl: 'https://test.com',
|
||||
isOAuth: true,
|
||||
usesTableApi: false,
|
||||
clientId: 'test-id',
|
||||
userIdentifierValue: 'email',
|
||||
jwtKeyId: 'test-id',
|
||||
},
|
||||
secrets: {
|
||||
clientSecret: 'secret',
|
||||
privateKey: 'secret-key',
|
||||
privateKeyPassword: 'secret-pass',
|
||||
},
|
||||
};
|
||||
|
||||
let appMockRenderer: AppMockRenderer;
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
appMockRenderer = createAppMockRenderer();
|
||||
});
|
||||
|
||||
it('renders a basic auth connector', () => {
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider connector={basicAuthConnector}>
|
||||
<ServiceNowConnectorFieldsNoApp
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId('credentialsApiUrlFromInput')).toBeInTheDocument();
|
||||
expect(getByTestId('connector-servicenow-username-form-input')).toBeInTheDocument();
|
||||
expect(getByTestId('connector-servicenow-password-form-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an oauth connector', () => {
|
||||
const { getByTestId } = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider connector={oauthConnector}>
|
||||
<ServiceNowConnectorFieldsNoApp
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId('credentialsApiUrlFromInput')).toBeInTheDocument();
|
||||
expect(getByTestId('connector-servicenow-client-id-form-input')).toBeInTheDocument();
|
||||
expect(getByTestId('connector-servicenow-user-identifier-form-input')).toBeInTheDocument();
|
||||
expect(getByTestId('connector-servicenow-jwt-key-id-form-input')).toBeInTheDocument();
|
||||
expect(getByTestId('connector-servicenow-client-secret-form-input')).toBeInTheDocument();
|
||||
expect(getByTestId('connector-servicenow-private-key-form-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const basicAuthTests: Array<[string, string]> = [
|
||||
['credentialsApiUrlFromInput', 'not-valid'],
|
||||
['connector-servicenow-username-form-input', ''],
|
||||
['connector-servicenow-password-form-input', ''],
|
||||
];
|
||||
|
||||
const oauthTests: Array<[string, string]> = [
|
||||
['credentialsApiUrlFromInput', 'not-valid'],
|
||||
['connector-servicenow-client-id-form-input', ''],
|
||||
['connector-servicenow-user-identifier-form-input', ''],
|
||||
['connector-servicenow-jwt-key-id-form-input', ''],
|
||||
['connector-servicenow-client-secret-form-input', ''],
|
||||
['connector-servicenow-private-key-form-input', ''],
|
||||
];
|
||||
|
||||
it.each(basicAuthTests)('validates correctly %p', async (field, value) => {
|
||||
const res = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider connector={basicAuthConnector} onSubmit={onSubmit}>
|
||||
<ServiceNowConnectorFieldsNoApp
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
|
||||
it.each(oauthTests)('validates correctly %p', async (field, value) => {
|
||||
const res = appMockRenderer.render(
|
||||
<ConnectorFormTestProvider connector={oauthConnector} onSubmit={onSubmit}>
|
||||
<ServiceNowConnectorFieldsNoApp
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,27 +6,23 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
|
||||
import { ServiceNowActionConnector } from './types';
|
||||
import { Credentials } from './credentials';
|
||||
import { ConnectorFormSchema } from '../../../sections/action_connector_form/types';
|
||||
import { ServiceNowConfig, ServiceNowSecrets } from './types';
|
||||
|
||||
const ServiceNowConnectorFieldsNoApp: React.FC<
|
||||
ActionConnectorFieldsProps<ServiceNowActionConnector>
|
||||
> = ({ action, editActionSecrets, editActionConfig, errors, readOnly }) => {
|
||||
return (
|
||||
<>
|
||||
<Credentials
|
||||
action={action}
|
||||
errors={errors}
|
||||
readOnly={readOnly}
|
||||
isLoading={false}
|
||||
editActionSecrets={editActionSecrets}
|
||||
editActionConfig={editActionConfig}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
const ServiceNowConnectorFieldsNoApp: React.FC<ActionConnectorFieldsProps> = ({
|
||||
isEdit,
|
||||
readOnly,
|
||||
}) => {
|
||||
const [{ config }] = useFormData<ConnectorFormSchema<ServiceNowConfig, ServiceNowSecrets>>({
|
||||
watch: ['config.isOAuth'],
|
||||
});
|
||||
const { isOAuth = false } = config ?? {};
|
||||
|
||||
return <Credentials readOnly={readOnly} isLoading={false} isOAuth={isOAuth} />;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
|
@ -14,11 +14,11 @@ const getStoreURL = (appId: string): string =>
|
|||
`https://store.servicenow.com/sn_appstore_store.do#!/store/application/${appId}`;
|
||||
|
||||
interface Props {
|
||||
appId: string;
|
||||
appId?: string;
|
||||
color: EuiButtonProps['color'];
|
||||
}
|
||||
|
||||
const SNStoreButtonComponent: React.FC<Props> = ({ color, appId }) => {
|
||||
const SNStoreButtonComponent: React.FC<Props> = ({ color, appId = '' }) => {
|
||||
return (
|
||||
<EuiButton
|
||||
href={getStoreURL(appId)}
|
||||
|
@ -34,7 +34,7 @@ const SNStoreButtonComponent: React.FC<Props> = ({ color, appId }) => {
|
|||
|
||||
export const SNStoreButton = memo(SNStoreButtonComponent);
|
||||
|
||||
const SNStoreLinkComponent: React.FC<Pick<Props, 'appId'>> = ({ appId }) => (
|
||||
const SNStoreLinkComponent: React.FC<Pick<Props, 'appId'>> = ({ appId = '' }) => (
|
||||
<EuiLink href={getStoreURL(appId)} target="_blank">
|
||||
{i18n.VISIT_SN_STORE}
|
||||
</EuiLink>
|
||||
|
|
|
@ -14,20 +14,6 @@ export const API_URL_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const API_URL_HELPTEXT = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlHelpText',
|
||||
{
|
||||
defaultMessage: 'Include the full URL.',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const API_URL_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField',
|
||||
{
|
||||
|
@ -35,13 +21,6 @@ export const API_URL_INVALID = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const API_URL_REQUIRE_HTTPS = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL must start with https://.',
|
||||
}
|
||||
);
|
||||
|
||||
export const AUTHENTICATION_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.authenticationLabel',
|
||||
{
|
||||
|
@ -49,13 +28,6 @@ export const AUTHENTICATION_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const REENTER_VALUES_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel',
|
||||
{
|
||||
defaultMessage: 'You must authenticate each time you edit the connector.',
|
||||
}
|
||||
);
|
||||
|
||||
export const USERNAME_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel',
|
||||
{
|
||||
|
@ -77,13 +49,6 @@ export const PASSWORD_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField',
|
||||
{
|
||||
defaultMessage: 'Password is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const TITLE_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredShortDescTextField',
|
||||
{
|
||||
|
@ -181,13 +146,6 @@ export const API_INFO_ERROR = (status: number) =>
|
|||
defaultMessage: 'Received status: {status} when attempting to get application information',
|
||||
});
|
||||
|
||||
export const INSTALL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.install',
|
||||
{
|
||||
defaultMessage: 'install',
|
||||
}
|
||||
);
|
||||
|
||||
export const INSTALLATION_CALLOUT_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutTitle',
|
||||
{
|
||||
|
@ -350,7 +308,7 @@ export const KEY_ID_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const USER_EMAIL_LABEL = i18n.translate(
|
||||
export const USER_IDENTIFIER_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.userEmailTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'User Identifier',
|
||||
|
@ -392,20 +350,6 @@ export const PRIVATE_KEY_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CLIENTSECRET_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredClientSecretTextField',
|
||||
{
|
||||
defaultMessage: 'Client Secret is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const USER_EMAIL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUserEmailTextField',
|
||||
{
|
||||
defaultMessage: 'User Identifier is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const KEYID_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredKeyIdTextField',
|
||||
{
|
||||
|
@ -413,6 +357,13 @@ export const KEYID_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const USER_IDENTIFIER_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUserIdentifierTextField',
|
||||
{
|
||||
defaultMessage: 'User Identifier is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const IS_OAUTH = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.useOAuth',
|
||||
{
|
||||
|
|
|
@ -6,62 +6,21 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { Props, UpdateConnector } from './update_connector';
|
||||
import { ServiceNowActionConnector } from './types';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { render, act as reactAct } from '@testing-library/react';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
const actionConnectorBasicAuth: ServiceNowActionConnector = {
|
||||
secrets: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.servicenow',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'servicenow',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
usesTableApi: true,
|
||||
isOAuth: false,
|
||||
},
|
||||
};
|
||||
|
||||
const actionConnectorOAuth: ServiceNowActionConnector = {
|
||||
secrets: {
|
||||
clientSecret: 'clientSecret',
|
||||
privateKey: 'privateKey',
|
||||
privateKeyPassword: 'privateKeyPassword',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.servicenow',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'servicenow',
|
||||
config: {
|
||||
apiUrl: 'https://test/',
|
||||
usesTableApi: true,
|
||||
isOAuth: true,
|
||||
clientId: 'cid',
|
||||
userIdentifierValue: 'test@testuserIdentifierValue.com',
|
||||
jwtKeyId: 'jwtKeyId',
|
||||
},
|
||||
};
|
||||
|
||||
const mountUpdateConnector = (
|
||||
props: Partial<Props> = {},
|
||||
action: ActionConnectorFieldsProps<ServiceNowActionConnector>['action'] = actionConnectorBasicAuth
|
||||
) => {
|
||||
const mountUpdateConnector = (props: Partial<Props> = {}, isOAuth: boolean = false) => {
|
||||
return mountWithIntl(
|
||||
<UpdateConnector
|
||||
action={action}
|
||||
applicationInfoErrorMsg={null}
|
||||
errors={{ apiUrl: [], username: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
actionTypeId=".servicenow"
|
||||
isOAuth={isOAuth}
|
||||
updateErrorMessage={null}
|
||||
readOnly={false}
|
||||
isLoading={false}
|
||||
onConfirm={() => {}}
|
||||
|
@ -87,7 +46,7 @@ describe('UpdateConnector renders', () => {
|
|||
});
|
||||
|
||||
it('should render update connector fields for OAuth', () => {
|
||||
const wrapper = mountUpdateConnector({}, actionConnectorOAuth);
|
||||
const wrapper = mountUpdateConnector({}, true);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="connector-servicenow-client-id-form-input"]').exists()
|
||||
).toBeTruthy();
|
||||
|
@ -114,8 +73,9 @@ describe('UpdateConnector renders', () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should disable inputs on loading', () => {
|
||||
it('should disable inputs on loading', async () => {
|
||||
const wrapper = mountUpdateConnector({ isLoading: true });
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="credentialsApiUrlFromInput"]').first().prop('disabled')
|
||||
).toBeTruthy();
|
||||
|
@ -134,7 +94,7 @@ describe('UpdateConnector renders', () => {
|
|||
});
|
||||
|
||||
it('should disable inputs on loading for OAuth', () => {
|
||||
const wrapper = mountUpdateConnector({ isLoading: true }, actionConnectorOAuth);
|
||||
const wrapper = mountUpdateConnector({ isLoading: true }, true);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
|
@ -199,7 +159,7 @@ describe('UpdateConnector renders', () => {
|
|||
});
|
||||
|
||||
it('should set inputs as read-only for OAuth', () => {
|
||||
const wrapper = mountUpdateConnector({ readOnly: true }, actionConnectorOAuth);
|
||||
const wrapper = mountUpdateConnector({ readOnly: true }, true);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
|
@ -243,116 +203,58 @@ describe('UpdateConnector renders', () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should disable submit button if errors or fields missing', () => {
|
||||
const wrapper = mountUpdateConnector(
|
||||
{
|
||||
errors: { apiUrl: ['some error'], username: [], password: [] },
|
||||
},
|
||||
actionConnectorBasicAuth
|
||||
);
|
||||
it('should disable submit button on form errors', async () => {
|
||||
const wrapper = mountUpdateConnector();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().prop('disabled')
|
||||
).toBeTruthy();
|
||||
|
||||
wrapper.setProps({ ...wrapper.props(), errors: { apiUrl: [], username: [], password: [] } });
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().prop('disabled')
|
||||
).toBeFalsy();
|
||||
|
||||
wrapper.setProps({
|
||||
...wrapper.props(),
|
||||
action: {
|
||||
...actionConnectorBasicAuth,
|
||||
secrets: { ...actionConnectorBasicAuth.secrets, username: undefined },
|
||||
},
|
||||
await act(async () => {
|
||||
wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().simulate('click');
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().prop('disabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call editActionConfig when editing api url', () => {
|
||||
const editActionConfig = jest.fn();
|
||||
const wrapper = mountUpdateConnector({ editActionConfig });
|
||||
|
||||
expect(editActionConfig).not.toHaveBeenCalled();
|
||||
wrapper
|
||||
.find('input[data-test-subj="credentialsApiUrlFromInput"]')
|
||||
.simulate('change', { target: { value: 'newUrl' } });
|
||||
expect(editActionConfig).toHaveBeenCalledWith('apiUrl', 'newUrl');
|
||||
});
|
||||
|
||||
it('should call editActionSecrets when editing username or password', () => {
|
||||
const editActionSecrets = jest.fn();
|
||||
const wrapper = mountUpdateConnector({ editActionSecrets });
|
||||
|
||||
expect(editActionSecrets).not.toHaveBeenCalled();
|
||||
wrapper
|
||||
.find('input[data-test-subj="connector-servicenow-username-form-input"]')
|
||||
.simulate('change', { target: { value: 'new username' } });
|
||||
expect(editActionSecrets).toHaveBeenCalledWith('username', 'new username');
|
||||
|
||||
wrapper
|
||||
.find('input[data-test-subj="connector-servicenow-password-form-input"]')
|
||||
.simulate('change', { target: { value: 'new pass' } });
|
||||
|
||||
expect(editActionSecrets).toHaveBeenCalledTimes(2);
|
||||
expect(editActionSecrets).toHaveBeenLastCalledWith('password', 'new pass');
|
||||
});
|
||||
|
||||
it('should call editActionSecrets and/or editActionConfig when editing oAuth fields', () => {
|
||||
const editActionSecrets = jest.fn();
|
||||
const editActionConfig = jest.fn();
|
||||
const wrapper = mountUpdateConnector(
|
||||
{ editActionSecrets, editActionConfig },
|
||||
actionConnectorOAuth
|
||||
);
|
||||
|
||||
expect(editActionSecrets).not.toHaveBeenCalled();
|
||||
|
||||
wrapper
|
||||
.find('input[data-test-subj="connector-servicenow-client-id-form-input"]')
|
||||
.simulate('change', { target: { value: 'new-value' } });
|
||||
expect(editActionConfig).toHaveBeenCalledWith('clientId', 'new-value');
|
||||
|
||||
wrapper
|
||||
.find('input[data-test-subj="connector-servicenow-user-identifier-form-input"]')
|
||||
.simulate('change', { target: { value: 'new-value' } });
|
||||
expect(editActionConfig).toHaveBeenCalledWith('userIdentifierValue', 'new-value');
|
||||
|
||||
wrapper
|
||||
.find('input[data-test-subj="connector-servicenow-jwt-key-id-form-input"]')
|
||||
.simulate('change', { target: { value: 'new-value' } });
|
||||
expect(editActionConfig).toHaveBeenCalledWith('jwtKeyId', 'new-value');
|
||||
|
||||
wrapper
|
||||
.find('input[data-test-subj="connector-servicenow-client-secret-form-input"]')
|
||||
.simulate('change', { target: { value: 'new-value' } });
|
||||
expect(editActionSecrets).toHaveBeenCalledWith('clientSecret', 'new-value');
|
||||
|
||||
wrapper
|
||||
.find('textarea[data-test-subj="connector-servicenow-private-key-form-input"]')
|
||||
.simulate('change', { target: { value: 'new-value' } });
|
||||
expect(editActionSecrets).toHaveBeenCalledWith('privateKey', 'new-value');
|
||||
|
||||
wrapper
|
||||
.find('input[data-test-subj="connector-servicenow-private-key-password-form-input"]')
|
||||
.simulate('change', { target: { value: 'new-value' } });
|
||||
expect(editActionSecrets).toHaveBeenCalledWith('privateKeyPassword', 'new-value');
|
||||
|
||||
expect(editActionConfig).toHaveBeenCalledTimes(3);
|
||||
expect(editActionSecrets).toHaveBeenCalledTimes(3);
|
||||
expect(editActionSecrets).toHaveBeenLastCalledWith('privateKeyPassword', 'new-value');
|
||||
});
|
||||
|
||||
it('should confirm the update when submit button clicked', () => {
|
||||
it('should confirm the update when submit button clicked', async () => {
|
||||
const onConfirm = jest.fn();
|
||||
const wrapper = mountUpdateConnector({ onConfirm });
|
||||
|
||||
const { getByTestId } = render(
|
||||
<I18nProvider>
|
||||
<UpdateConnector
|
||||
actionTypeId=".servicenow"
|
||||
isOAuth={false}
|
||||
updateErrorMessage={null}
|
||||
readOnly={false}
|
||||
isLoading={false}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={() => {}}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().simulate('click');
|
||||
expect(onConfirm).toHaveBeenCalled();
|
||||
|
||||
await reactAct(async () => {
|
||||
const urlInput = getByTestId('credentialsApiUrlFromInput');
|
||||
const usernameInput = getByTestId('connector-servicenow-username-form-input');
|
||||
const passwordInput = getByTestId('connector-servicenow-password-form-input');
|
||||
|
||||
await userEvent.type(urlInput, 'https://example.com', { delay: 100 });
|
||||
await userEvent.type(usernameInput, 'user', { delay: 100 });
|
||||
await userEvent.type(passwordInput, 'pass', { delay: 100 });
|
||||
userEvent.click(getByTestId('snUpdateInstallationSubmit'));
|
||||
});
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith({
|
||||
config: {
|
||||
apiUrl: 'https://example.com',
|
||||
},
|
||||
secrets: {
|
||||
password: 'pass',
|
||||
username: 'user',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel the update when cancel button clicked', () => {
|
||||
|
@ -365,14 +267,14 @@ describe('UpdateConnector renders', () => {
|
|||
});
|
||||
|
||||
it('should show error message if present', () => {
|
||||
const applicationInfoErrorMsg = 'some application error';
|
||||
const updateErrorMessage = 'some application error';
|
||||
const wrapper = mountUpdateConnector({
|
||||
applicationInfoErrorMsg,
|
||||
updateErrorMessage,
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="snApplicationCallout"]').first().text()).toContain(
|
||||
applicationInfoErrorMsg
|
||||
updateErrorMessage
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
|
@ -22,13 +22,12 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { snExternalServiceConfig } from '@kbn/actions-plugin/common';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { ServiceNowActionConnector } from './types';
|
||||
import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { CredentialsApiUrl } from './credentials_api_url';
|
||||
import { isFieldInvalid } from './helpers';
|
||||
import { ApplicationRequiredCallout } from './application_required_callout';
|
||||
import { SNStoreLink } from './sn_store_button';
|
||||
import { CredentialsAuth, OAuth } from './auth_types';
|
||||
import { SNStoreLink } from './sn_store_button';
|
||||
import { ApplicationRequiredCallout } from './application_required_callout';
|
||||
import { ServiceNowConfig, ServiceNowSecrets } from './types';
|
||||
|
||||
const title = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.updateFormTitle',
|
||||
|
@ -78,167 +77,142 @@ const warningMessage = i18n.translate(
|
|||
defaultMessage: 'This updates all instances of this connector and cannot be reversed.',
|
||||
}
|
||||
);
|
||||
|
||||
export interface Props {
|
||||
action: ActionConnectorFieldsProps<ServiceNowActionConnector>['action'];
|
||||
applicationInfoErrorMsg: string | null;
|
||||
errors: ActionConnectorFieldsProps<ServiceNowActionConnector>['errors'];
|
||||
isLoading: boolean;
|
||||
readOnly: boolean;
|
||||
editActionSecrets: ActionConnectorFieldsProps<ServiceNowActionConnector>['editActionSecrets'];
|
||||
editActionConfig: ActionConnectorFieldsProps<ServiceNowActionConnector>['editActionConfig'];
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
export interface UpdateConnectorFormSchema {
|
||||
updatedConnector: {
|
||||
config: ServiceNowConfig;
|
||||
secrets: ServiceNowSecrets;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
actionTypeId: string;
|
||||
isOAuth: boolean;
|
||||
isLoading: boolean;
|
||||
readOnly: boolean;
|
||||
updateErrorMessage?: string | null;
|
||||
onCancel: () => void;
|
||||
onConfirm: (connector: UpdateConnectorFormSchema['updatedConnector']) => void;
|
||||
}
|
||||
|
||||
const PATH_PREFIX = 'updatedConnector.';
|
||||
|
||||
const UpdateConnectorComponent: React.FC<Props> = ({
|
||||
action,
|
||||
applicationInfoErrorMsg,
|
||||
errors,
|
||||
actionTypeId,
|
||||
isOAuth,
|
||||
isLoading,
|
||||
readOnly,
|
||||
editActionSecrets,
|
||||
editActionConfig,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
updateErrorMessage,
|
||||
}) => {
|
||||
const { apiUrl, isOAuth, jwtKeyId, userIdentifierValue, clientId } = action.config;
|
||||
const { username, password, privateKeyPassword, privateKey, clientSecret } = action.secrets;
|
||||
const { form } = useForm<UpdateConnectorFormSchema>();
|
||||
const { submit, isValid } = form;
|
||||
|
||||
let hasErrorsOrEmptyFields;
|
||||
const onSubmit = useCallback(async () => {
|
||||
const { data, isValid: isSubmitValid } = await submit();
|
||||
if (!isSubmitValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasErrorsOrEmptyFields = apiUrl === undefined || isFieldInvalid(apiUrl, errors.apiUrl);
|
||||
|
||||
if (isOAuth) {
|
||||
hasErrorsOrEmptyFields =
|
||||
hasErrorsOrEmptyFields ||
|
||||
jwtKeyId === undefined ||
|
||||
userIdentifierValue === undefined ||
|
||||
clientId === undefined ||
|
||||
privateKeyPassword === undefined ||
|
||||
privateKey === undefined ||
|
||||
clientSecret === undefined ||
|
||||
isFieldInvalid(jwtKeyId, errors.apiUrl) ||
|
||||
isFieldInvalid(userIdentifierValue, errors.userIdentifierValue) ||
|
||||
isFieldInvalid(clientId, errors.clientId) ||
|
||||
isFieldInvalid(privateKeyPassword, errors.privateKeyPassword) ||
|
||||
isFieldInvalid(privateKey, errors.privateKey) ||
|
||||
isFieldInvalid(clientSecret, errors.clientSecret);
|
||||
} else {
|
||||
hasErrorsOrEmptyFields =
|
||||
hasErrorsOrEmptyFields ||
|
||||
username === undefined ||
|
||||
password === undefined ||
|
||||
isFieldInvalid(username, errors.username) ||
|
||||
isFieldInvalid(password, errors.password);
|
||||
}
|
||||
const { updatedConnector } = data;
|
||||
onConfirm(updatedConnector);
|
||||
}, [onConfirm, submit]);
|
||||
|
||||
return (
|
||||
<EuiFlyout ownFocus onClose={onCancel} data-test-subj="updateConnectorForm">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h1>{title}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody
|
||||
banner={
|
||||
<EuiCallOut
|
||||
size="m"
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
data-test-subj="snUpdateInstallationCallout"
|
||||
title={warningMessage}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiSteps
|
||||
steps={[
|
||||
{
|
||||
title: step1InstallTitle,
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.serviceNowAction.serviceNowAppRunning"
|
||||
defaultMessage="The Elastic App from the ServiceNow app store must be installed prior to running the update. {visitLink} to install the app"
|
||||
values={{
|
||||
visitLink: (
|
||||
<SNStoreLink
|
||||
appId={snExternalServiceConfig[action.actionTypeId].appId ?? ''}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: step2InstanceUrlTitle,
|
||||
children: (
|
||||
<CredentialsApiUrl
|
||||
action={action}
|
||||
errors={errors}
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
editActionConfig={editActionConfig}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: step3CredentialsTitle,
|
||||
children: isOAuth ? (
|
||||
<OAuth
|
||||
action={action}
|
||||
errors={errors}
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
editActionSecrets={editActionSecrets}
|
||||
editActionConfig={editActionConfig}
|
||||
/>
|
||||
) : (
|
||||
<CredentialsAuth
|
||||
action={action}
|
||||
errors={errors}
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
editActionSecrets={editActionSecrets}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
{applicationInfoErrorMsg && (
|
||||
<ApplicationRequiredCallout
|
||||
message={applicationInfoErrorMsg}
|
||||
appId={snExternalServiceConfig[action.actionTypeId].appId ?? ''}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty data-test-subj="snUpdateInstallationCancel" onClick={onCancel}>
|
||||
{cancelButtonText}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="snUpdateInstallationSubmit"
|
||||
onClick={onConfirm}
|
||||
<Form form={form}>
|
||||
<EuiFlyout ownFocus onClose={onCancel} data-test-subj="updateConnectorForm">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h1>{title}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody
|
||||
banner={
|
||||
<EuiCallOut
|
||||
size="m"
|
||||
color="danger"
|
||||
fill
|
||||
disabled={hasErrorsOrEmptyFields}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{confirmButtonText}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
iconType="alert"
|
||||
data-test-subj="snUpdateInstallationCallout"
|
||||
title={warningMessage}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiSteps
|
||||
steps={[
|
||||
{
|
||||
title: step1InstallTitle,
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.serviceNowAction.serviceNowAppRunning"
|
||||
defaultMessage="The Elastic App from the ServiceNow app store must be installed prior to running the update. {visitLink} to install the app"
|
||||
values={{
|
||||
visitLink: (
|
||||
<SNStoreLink appId={snExternalServiceConfig[actionTypeId].appId ?? ''} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: step2InstanceUrlTitle,
|
||||
children: (
|
||||
<CredentialsApiUrl
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
pathPrefix={PATH_PREFIX}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: step3CredentialsTitle,
|
||||
children: isOAuth ? (
|
||||
<OAuth readOnly={readOnly} isLoading={isLoading} pathPrefix={PATH_PREFIX} />
|
||||
) : (
|
||||
<CredentialsAuth
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
pathPrefix={PATH_PREFIX}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
{updateErrorMessage != null ? (
|
||||
<ApplicationRequiredCallout
|
||||
message={updateErrorMessage}
|
||||
appId={snExternalServiceConfig[actionTypeId].appId ?? ''}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty data-test-subj="snUpdateInstallationCancel" onClick={onCancel}>
|
||||
{cancelButtonText}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="snUpdateInstallationSubmit"
|
||||
onClick={onSubmit}
|
||||
color="danger"
|
||||
fill
|
||||
disabled={!isValid}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{confirmButtonText}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -5,18 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { HttpStart } from '@kbn/core/public';
|
||||
import { getAppInfo } from './api';
|
||||
import { AppInfo, RESTApiError, ServiceNowActionConnector } from './types';
|
||||
|
||||
export interface UseGetAppInfoProps {
|
||||
actionTypeId: string;
|
||||
actionTypeId?: string;
|
||||
http: HttpStart;
|
||||
}
|
||||
|
||||
export interface UseGetAppInfo {
|
||||
fetchAppInfo: (connector: ServiceNowActionConnector) => Promise<AppInfo | RESTApiError>;
|
||||
fetchAppInfo: (
|
||||
connector: ServiceNowActionConnector
|
||||
) => Promise<AppInfo | RESTApiError | undefined>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
|
@ -28,6 +31,10 @@ export const useGetAppInfo = ({ actionTypeId, http }: UseGetAppInfoProps): UseGe
|
|||
const fetchAppInfo = useCallback(
|
||||
async (connector) => {
|
||||
try {
|
||||
if (!actionTypeId || isEmpty(actionTypeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
didCancel.current = false;
|
||||
abortCtrl.current.abort();
|
||||
abortCtrl.current = new AbortController();
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { SlackActionConnector } from '../types';
|
||||
import { registrationServicesMock } from '../../../../mocks';
|
||||
|
||||
const ACTION_TYPE_ID = '.slack';
|
||||
|
@ -30,98 +29,6 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('slack connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'https:\\test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
} as SlackActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
webhookUrl: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - no webhook url', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
} as SlackActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
webhookUrl: ['Webhook URL is required.'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - invalid webhook protocol', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http:\\test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
} as SlackActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
webhookUrl: ['Webhook URL must start with https://.'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - invalid webhook url', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'h',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
} as SlackActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
webhookUrl: ['Webhook URL is invalid.'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('slack action params validation', () => {
|
||||
test('if action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
|
|
|
@ -7,13 +7,8 @@
|
|||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
GenericValidationResult,
|
||||
ConnectorValidationResult,
|
||||
} from '../../../../types';
|
||||
import { SlackActionParams, SlackSecrets, SlackActionConnector } from '../types';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
import { ActionTypeModel, GenericValidationResult } from '../../../../types';
|
||||
import { SlackActionParams, SlackSecrets } from '../types';
|
||||
|
||||
export function getActionType(): ActionTypeModel<unknown, SlackSecrets, SlackActionParams> {
|
||||
return {
|
||||
|
@ -31,25 +26,6 @@ export function getActionType(): ActionTypeModel<unknown, SlackSecrets, SlackAct
|
|||
defaultMessage: 'Send to Slack',
|
||||
}
|
||||
),
|
||||
validateConnector: async (
|
||||
action: SlackActionConnector
|
||||
): 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(translations.WEBHOOK_URL_REQUIRED);
|
||||
} else if (action.secrets.webhookUrl) {
|
||||
if (!isValidUrl(action.secrets.webhookUrl)) {
|
||||
secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_INVALID);
|
||||
} else if (!isValidUrl(action.secrets.webhookUrl, 'https:')) {
|
||||
secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_HTTP_INVALID);
|
||||
}
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: async (
|
||||
actionParams: SlackActionParams
|
||||
): Promise<GenericValidationResult<SlackActionParams>> => {
|
||||
|
|
|
@ -7,108 +7,127 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { act } from '@testing-library/react';
|
||||
import { SlackActionConnector } from '../types';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import SlackActionFields from './slack_connectors';
|
||||
import { ConnectorFormTestProvider } from '../test_utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
describe('SlackActionFields renders', () => {
|
||||
test('all connector fields is rendered', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http:\\test',
|
||||
webhookUrl: 'http://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
} as SlackActionConnector;
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SlackActionFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], webhookUrl: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<SlackActionFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').first().prop('value')).toBe(
|
||||
'http:\\test'
|
||||
'http://test.com'
|
||||
);
|
||||
});
|
||||
|
||||
test('should display a message on create to remember credentials', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.email',
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as SlackActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<SlackActionFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], webhookUrl: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
test('should display a message for missing secrets after import', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.email',
|
||||
isMissingSecrets: true,
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as SlackActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<SlackActionFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], webhookUrl: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0);
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should display a message on edit to re-enter credentials', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http:\\test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
config: {},
|
||||
} as SlackActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<SlackActionFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], webhookUrl: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
it('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<SlackActionFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
secrets: {
|
||||
webhookUrl: 'http://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('validates teh web hook url field correctly', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack',
|
||||
config: {},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<SlackActionFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(
|
||||
getByTestId('slackWebhookUrlInput'),
|
||||
`{selectall}{backspace}no-valid`,
|
||||
{
|
||||
delay: 10,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,73 +6,55 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { SlackActionConnector } from '../types';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label';
|
||||
import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { DocLinksStart } from '@kbn/core/public';
|
||||
|
||||
const SlackActionFields: React.FunctionComponent<
|
||||
ActionConnectorFieldsProps<SlackActionConnector>
|
||||
> = ({ action, editActionSecrets, errors, readOnly }) => {
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const { urlField } = fieldValidators;
|
||||
|
||||
const getWebhookUrlConfig = (docLinks: DocLinksStart): FieldConfig => ({
|
||||
label: i18n.WEBHOOK_URL_LABEL,
|
||||
helpText: (
|
||||
<EuiLink href={docLinks.links.alerting.slackAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlHelpLabel"
|
||||
defaultMessage="Create a Slack Webhook URL"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: urlField(i18n.WEBHOOK_URL_INVALID),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const SlackActionFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
isEdit,
|
||||
readOnly,
|
||||
}) => {
|
||||
const { docLinks } = useKibana().services;
|
||||
const { webhookUrl } = action.secrets;
|
||||
const isWebhookUrlInvalid: boolean =
|
||||
errors.webhookUrl !== undefined && errors.webhookUrl.length > 0 && webhookUrl !== undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
id="webhookUrl"
|
||||
fullWidth
|
||||
helpText={
|
||||
<EuiLink href={docLinks.links.alerting.slackAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlHelpLabel"
|
||||
defaultMessage="Create a Slack Webhook URL"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
error={errors.webhookUrl}
|
||||
isInvalid={isWebhookUrlInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Webhook URL',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
1,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.reenterValueLabel',
|
||||
{ defaultMessage: 'This URL is encrypted. Please reenter a value for this field.' }
|
||||
)
|
||||
)}
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isWebhookUrlInvalid}
|
||||
name="webhookUrl"
|
||||
readOnly={readOnly}
|
||||
value={webhookUrl || ''}
|
||||
data-test-subj="slackWebhookUrlInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('webhookUrl', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!webhookUrl) {
|
||||
editActionSecrets('webhookUrl', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
<UseField
|
||||
path="secrets.webhookUrl"
|
||||
config={getWebhookUrlConfig(docLinks)}
|
||||
component={Field}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
readOnly,
|
||||
'data-test-subj': 'slackWebhookUrlInput',
|
||||
fullWidth: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,13 +7,6 @@
|
|||
|
||||
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',
|
||||
{
|
||||
|
@ -21,16 +14,16 @@ export const WEBHOOK_URL_INVALID = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
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.',
|
||||
}
|
||||
);
|
||||
|
||||
export const WEBHOOK_URL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Webhook URL',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { SwimlaneConnectorType, SwimlaneMappingConfig, MappingConfigurationKeys } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -18,7 +19,7 @@ const casesFields = [...casesRequiredFields];
|
|||
const alertsRequiredFields: MappingConfigurationKeys[] = ['ruleNameConfig', 'alertIdConfig'];
|
||||
const alertsFields = ['severityConfig', 'commentsConfig', ...alertsRequiredFields];
|
||||
|
||||
const translationMapping: Record<string, string> = {
|
||||
export const translationMapping: Record<string, string> = {
|
||||
caseIdConfig: i18n.SW_REQUIRED_CASE_ID,
|
||||
alertIdConfig: i18n.SW_REQUIRED_ALERT_ID,
|
||||
caseNameConfig: i18n.SW_REQUIRED_CASE_NAME,
|
||||
|
@ -29,16 +30,33 @@ const translationMapping: Record<string, string> = {
|
|||
};
|
||||
|
||||
export const isValidFieldForConnector = (
|
||||
connector: SwimlaneConnectorType,
|
||||
field: MappingConfigurationKeys
|
||||
connectorType: SwimlaneConnectorType,
|
||||
fieldId: MappingConfigurationKeys
|
||||
): boolean => {
|
||||
if (connector === SwimlaneConnectorType.All) {
|
||||
if (connectorType === SwimlaneConnectorType.All) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return connector === SwimlaneConnectorType.Alerts
|
||||
? alertsFields.includes(field)
|
||||
: casesFields.includes(field);
|
||||
return connectorType === SwimlaneConnectorType.Alerts
|
||||
? alertsFields.includes(fieldId)
|
||||
: casesFields.includes(fieldId);
|
||||
};
|
||||
|
||||
export const isRequiredField = (
|
||||
connectorType: SwimlaneConnectorType,
|
||||
fieldId: MappingConfigurationKeys | undefined
|
||||
) => {
|
||||
if (connectorType === SwimlaneConnectorType.All) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fieldId == null || isEmpty(fieldId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return connectorType === SwimlaneConnectorType.Alerts
|
||||
? alertsFields.includes(fieldId)
|
||||
: casesFields.includes(fieldId);
|
||||
};
|
||||
|
||||
export const validateMappingForConnector = (
|
||||
|
|
|
@ -4,115 +4,69 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiFieldPassword,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import React from 'react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import * as i18n from '../translations';
|
||||
import { PasswordField } from '../../../password_field';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { SwimlaneActionConnector } from '../types';
|
||||
import { IErrorObject } from '../../../../../types';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
interface Props {
|
||||
action: SwimlaneActionConnector;
|
||||
editActionConfig: (property: string, value: any) => void;
|
||||
editActionSecrets: (property: string, value: any) => void;
|
||||
errors: IErrorObject;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
const SwimlaneConnectionComponent: React.FunctionComponent<Props> = ({
|
||||
action,
|
||||
editActionConfig,
|
||||
editActionSecrets,
|
||||
errors,
|
||||
readOnly,
|
||||
}) => {
|
||||
const { apiUrl, appId } = action.config;
|
||||
const { apiToken } = action.secrets;
|
||||
const { emptyField, urlField } = fieldValidators;
|
||||
|
||||
const SwimlaneConnectionComponent: React.FunctionComponent<Props> = ({ readOnly }) => {
|
||||
const { docLinks } = useKibana().services;
|
||||
|
||||
const onChangeConfig = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>, key: 'apiUrl' | 'appId') => {
|
||||
editActionConfig(key, e.target.value);
|
||||
},
|
||||
[editActionConfig]
|
||||
);
|
||||
|
||||
const onBlurConfig = useCallback(
|
||||
(key: 'apiUrl' | 'appId') => {
|
||||
if (!action.config[key]) {
|
||||
editActionConfig(key, '');
|
||||
}
|
||||
},
|
||||
[action.config, editActionConfig]
|
||||
);
|
||||
|
||||
const onChangeSecrets = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
editActionSecrets('apiToken', e.target.value);
|
||||
},
|
||||
[editActionSecrets]
|
||||
);
|
||||
|
||||
const onBlurSecrets = useCallback(() => {
|
||||
if (!apiToken) {
|
||||
editActionSecrets('apiToken', '');
|
||||
}
|
||||
}, [apiToken, editActionSecrets]);
|
||||
|
||||
const isApiUrlInvalid = errors.apiUrl?.length > 0 && apiToken !== undefined;
|
||||
const isAppIdInvalid = errors.appId?.length > 0 && apiToken !== undefined;
|
||||
const isApiTokenInvalid = errors.apiToken?.length > 0 && apiToken !== undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
id="apiUrl"
|
||||
fullWidth
|
||||
label={i18n.SW_API_URL_TEXT_FIELD_LABEL}
|
||||
error={errors.apiUrl}
|
||||
isInvalid={isApiUrlInvalid}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
name="apiUrl"
|
||||
value={apiUrl ?? ''}
|
||||
readOnly={readOnly}
|
||||
isInvalid={isApiUrlInvalid}
|
||||
data-test-subj="swimlaneApiUrlInput"
|
||||
onChange={(e) => onChangeConfig(e, 'apiUrl')}
|
||||
onBlur={() => onBlurConfig('apiUrl')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="appId"
|
||||
fullWidth
|
||||
label={i18n.SW_APP_ID_TEXT_FIELD_LABEL}
|
||||
error={errors.appId}
|
||||
isInvalid={isAppIdInvalid}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
name="appId"
|
||||
value={appId ?? ''}
|
||||
readOnly={readOnly}
|
||||
isInvalid={isAppIdInvalid}
|
||||
data-test-subj="swimlaneAppIdInput"
|
||||
onChange={(e) => onChangeConfig(e, 'appId')}
|
||||
onBlur={() => onBlurConfig('appId')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="apiToken"
|
||||
fullWidth
|
||||
<UseField
|
||||
path="config.apiUrl"
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.SW_API_URL_TEXT_FIELD_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: urlField(i18n.SW_API_URL_INVALID),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
disabled: readOnly,
|
||||
'data-test-subj': 'swimlaneApiUrlInput',
|
||||
readOnly,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path="config.appId"
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.SW_APP_ID_TEXT_FIELD_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.SW_REQUIRED_APP_ID_TEXT),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
disabled: readOnly,
|
||||
'data-test-subj': 'swimlaneAppIdInput',
|
||||
readOnly,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<PasswordField
|
||||
path="secrets.apiToken"
|
||||
label={i18n.SW_API_TOKEN_TEXT_FIELD_LABEL}
|
||||
readOnly={readOnly}
|
||||
helpText={
|
||||
<EuiLink
|
||||
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/swimlane-action-type.html`}
|
||||
|
@ -124,42 +78,8 @@ const SwimlaneConnectionComponent: React.FunctionComponent<Props> = ({
|
|||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
error={errors.apiToken}
|
||||
isInvalid={isApiTokenInvalid}
|
||||
label={i18n.SW_API_TOKEN_TEXT_FIELD_LABEL}
|
||||
>
|
||||
<>
|
||||
{!action.id ? (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s" data-test-subj="rememberValuesMessage">
|
||||
{i18n.SW_REMEMBER_VALUE_LABEL}
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
iconType="iInCircle"
|
||||
data-test-subj="reenterValuesMessage"
|
||||
title={i18n.SW_REENTER_VALUE_LABEL}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
isInvalid={isApiTokenInvalid}
|
||||
readOnly={readOnly}
|
||||
value={apiToken ?? ''}
|
||||
data-test-subj="swimlaneApiTokenInput"
|
||||
onChange={onChangeSecrets}
|
||||
onBlur={onBlurSecrets}
|
||||
/>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
data-test-subj="swimlaneApiTokenInput"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,17 +5,25 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption, EuiButtonGroup } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiComboBoxProps, EuiFormRow } from '@elastic/eui';
|
||||
import {
|
||||
FieldConfig,
|
||||
getFieldValidityAndErrorMessage,
|
||||
UseField,
|
||||
useFormData,
|
||||
VALIDATION_TYPES,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { ComboBoxField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
import {
|
||||
SwimlaneActionConnector,
|
||||
MappingConfigurationKeys,
|
||||
SwimlaneConnectorType,
|
||||
SwimlaneFieldMappingConfig,
|
||||
SwimlaneMappingConfig,
|
||||
} from '../types';
|
||||
import { IErrorObject } from '../../../../../types';
|
||||
import { isValidFieldForConnector } from '../helpers';
|
||||
import { isRequiredField, isValidFieldForConnector } from '../helpers';
|
||||
import { ButtonGroupField } from '../../../button_group_field';
|
||||
|
||||
const SINGLE_SELECTION = { asPlainText: true };
|
||||
const EMPTY_COMBO_BOX_ARRAY: Array<EuiComboBoxOptionOption<string>> | undefined = [];
|
||||
|
@ -29,11 +37,8 @@ const createSelectedOption = (field: SwimlaneFieldMappingConfig | null | undefin
|
|||
field != null ? [formatOption(field)] : EMPTY_COMBO_BOX_ARRAY;
|
||||
|
||||
interface Props {
|
||||
action: SwimlaneActionConnector;
|
||||
editActionConfig: (property: string, value: any) => void;
|
||||
updateCurrentStep: (step: number) => void;
|
||||
readOnly: boolean;
|
||||
fields: SwimlaneFieldMappingConfig[];
|
||||
errors: IErrorObject;
|
||||
}
|
||||
|
||||
const connectorTypeButtons = [
|
||||
|
@ -42,16 +47,107 @@ const connectorTypeButtons = [
|
|||
{ id: SwimlaneConnectorType.Cases, label: 'Cases' },
|
||||
];
|
||||
|
||||
const SwimlaneFieldsComponent: React.FC<Props> = ({
|
||||
action,
|
||||
editActionConfig,
|
||||
updateCurrentStep,
|
||||
fields,
|
||||
errors,
|
||||
}) => {
|
||||
const { mappings, connectorType = SwimlaneConnectorType.All } = action.config;
|
||||
const prevConnectorType = useRef<SwimlaneConnectorType>(connectorType);
|
||||
const hasChangedConnectorType = connectorType !== prevConnectorType.current;
|
||||
const mappingConfig: FieldConfig<SwimlaneFieldMappingConfig | null> = {
|
||||
defaultValue: null,
|
||||
validations: [
|
||||
{
|
||||
validator: ({ value, customData }) => {
|
||||
const data = customData.value as {
|
||||
connectorType: SwimlaneConnectorType;
|
||||
validationLabel: string;
|
||||
};
|
||||
if (isRequiredField(data.connectorType, value?.id as MappingConfigurationKeys)) {
|
||||
return {
|
||||
message: data.validationLabel,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const MappingField: React.FC<{
|
||||
path: string;
|
||||
label: string;
|
||||
validationLabel: string;
|
||||
options: EuiComboBoxProps<string>['options'];
|
||||
fieldIdMap: Map<string, SwimlaneFieldMappingConfig>;
|
||||
connectorType: SwimlaneConnectorType;
|
||||
readOnly: boolean;
|
||||
dataTestSubj?: string;
|
||||
}> = React.memo(
|
||||
({
|
||||
path,
|
||||
options,
|
||||
label,
|
||||
validationLabel,
|
||||
dataTestSubj,
|
||||
fieldIdMap,
|
||||
connectorType,
|
||||
readOnly,
|
||||
}) => {
|
||||
return (
|
||||
<UseField<SwimlaneFieldMappingConfig | null>
|
||||
path={path}
|
||||
component={ComboBoxField}
|
||||
config={mappingConfig}
|
||||
validationData={{ connectorType, validationLabel }}
|
||||
>
|
||||
{(field) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
const onComboChange = (opt: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
const option = opt[0];
|
||||
|
||||
const item = fieldIdMap.get(option?.value ?? '');
|
||||
if (!item) {
|
||||
field.setValue(null);
|
||||
return;
|
||||
}
|
||||
|
||||
field.setValue({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
key: item.key,
|
||||
fieldType: item.fieldType,
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchComboChange = (value: string) => {
|
||||
if (value !== undefined) {
|
||||
field.clearErrors(VALIDATION_TYPES.ARRAY_ITEM);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedOptions = createSelectedOption(fieldIdMap.get(field.value?.id ?? ''));
|
||||
|
||||
return (
|
||||
<EuiFormRow label={label} error={errorMessage} isInvalid={isInvalid} fullWidth>
|
||||
<EuiComboBox
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onComboChange}
|
||||
onSearchChange={onSearchComboChange}
|
||||
fullWidth
|
||||
noSuggestions={false}
|
||||
data-test-subj={dataTestSubj}
|
||||
options={options}
|
||||
isDisabled={readOnly}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const SwimlaneFieldsComponent: React.FC<Props> = ({ fields, readOnly }) => {
|
||||
const [{ config }] = useFormData({
|
||||
watch: ['config.connectorType'],
|
||||
});
|
||||
|
||||
const connectorType = config?.connectorType ?? SwimlaneConnectorType.All;
|
||||
|
||||
const [fieldTypeMap, fieldIdMap] = useMemo(
|
||||
() =>
|
||||
|
@ -78,214 +174,98 @@ const SwimlaneFieldsComponent: React.FC<Props> = ({
|
|||
const textOptions = useMemo(() => fieldTypeMap.get('text') ?? [], [fieldTypeMap]);
|
||||
const commentsOptions = useMemo(() => fieldTypeMap.get('comments') ?? [], [fieldTypeMap]);
|
||||
|
||||
const state = useMemo(
|
||||
() => ({
|
||||
alertIdConfig: createSelectedOption(mappings?.alertIdConfig),
|
||||
severityConfig: createSelectedOption(mappings?.severityConfig),
|
||||
ruleNameConfig: createSelectedOption(mappings?.ruleNameConfig),
|
||||
caseIdConfig: createSelectedOption(mappings?.caseIdConfig),
|
||||
caseNameConfig: createSelectedOption(mappings?.caseNameConfig),
|
||||
commentsConfig: createSelectedOption(mappings?.commentsConfig),
|
||||
descriptionConfig: createSelectedOption(mappings?.descriptionConfig),
|
||||
}),
|
||||
[mappings]
|
||||
);
|
||||
|
||||
const mappingErrors: Record<string, string> = useMemo(
|
||||
() => (Array.isArray(errors?.mappings) ? errors?.mappings[0] : {}),
|
||||
[errors]
|
||||
);
|
||||
|
||||
const editMappings = useCallback(
|
||||
(key: keyof SwimlaneMappingConfig, e: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
if (e.length === 0) {
|
||||
const newProps = {
|
||||
...mappings,
|
||||
[key]: null,
|
||||
};
|
||||
editActionConfig('mappings', newProps);
|
||||
return;
|
||||
}
|
||||
|
||||
const option = e[0];
|
||||
const item = fieldIdMap.get(option.value ?? '');
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newProps = {
|
||||
...mappings,
|
||||
[key]: { id: item.id, name: item.name, key: item.key, fieldType: item.fieldType },
|
||||
};
|
||||
editActionConfig('mappings', newProps);
|
||||
},
|
||||
[editActionConfig, fieldIdMap, mappings]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectorType !== prevConnectorType.current) {
|
||||
prevConnectorType.current = connectorType;
|
||||
}
|
||||
}, [connectorType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow id="connectorType" fullWidth label={i18n.SW_CONNECTOR_TYPE_LABEL}>
|
||||
<EuiButtonGroup
|
||||
name="connectorType"
|
||||
legend={i18n.SW_CONNECTOR_TYPE_LABEL}
|
||||
options={connectorTypeButtons}
|
||||
idSelected={connectorType}
|
||||
onChange={(type) => editActionConfig('connectorType', type)}
|
||||
buttonSize="compressed"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<ButtonGroupField
|
||||
defaultValue={SwimlaneConnectorType.All}
|
||||
path={'config.connectorType'}
|
||||
label={i18n.SW_CONNECTOR_TYPE_LABEL}
|
||||
legend={i18n.SW_CONNECTOR_TYPE_LABEL}
|
||||
options={connectorTypeButtons}
|
||||
/>
|
||||
{isValidFieldForConnector(connectorType as SwimlaneConnectorType.All, 'alertIdConfig') && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
id="alertIdConfig"
|
||||
fullWidth
|
||||
label={i18n.SW_ALERT_ID_FIELD_LABEL}
|
||||
error={mappingErrors?.alertIdConfig}
|
||||
isInvalid={mappingErrors?.alertIdConfig != null && !hasChangedConnectorType}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
selectedOptions={state.alertIdConfig}
|
||||
options={textOptions}
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
data-test-subj="swimlaneAlertIdInput"
|
||||
onChange={(e) => editMappings('alertIdConfig', e)}
|
||||
isInvalid={mappingErrors?.alertIdConfig != null && !hasChangedConnectorType}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
<MappingField
|
||||
path="config.mappings.alertIdConfig"
|
||||
label={i18n.SW_ALERT_ID_FIELD_LABEL}
|
||||
validationLabel={i18n.SW_REQUIRED_ALERT_ID}
|
||||
options={textOptions}
|
||||
fieldIdMap={fieldIdMap}
|
||||
connectorType={connectorType}
|
||||
dataTestSubj="swimlaneAlertIdInput"
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)}
|
||||
{isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'ruleNameConfig') && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
id="ruleNameConfig"
|
||||
fullWidth
|
||||
label={i18n.SW_RULE_NAME_FIELD_LABEL}
|
||||
error={mappingErrors?.ruleNameConfig}
|
||||
isInvalid={mappingErrors?.ruleNameConfig != null && !hasChangedConnectorType}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
selectedOptions={state.ruleNameConfig}
|
||||
options={textOptions}
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
data-test-subj="swimlaneAlertNameInput"
|
||||
onChange={(e) => editMappings('ruleNameConfig', e)}
|
||||
isInvalid={mappingErrors?.ruleNameConfig != null && !hasChangedConnectorType}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
<MappingField
|
||||
path="config.mappings.ruleNameConfig"
|
||||
label={i18n.SW_RULE_NAME_FIELD_LABEL}
|
||||
validationLabel={i18n.SW_REQUIRED_ALERT_ID}
|
||||
options={textOptions}
|
||||
dataTestSubj="swimlaneAlertNameInput"
|
||||
fieldIdMap={fieldIdMap}
|
||||
connectorType={connectorType}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)}
|
||||
{isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'severityConfig') && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
id="severityConfig"
|
||||
fullWidth
|
||||
label={i18n.SW_SEVERITY_FIELD_LABEL}
|
||||
error={mappingErrors?.severityConfig}
|
||||
isInvalid={mappingErrors?.severityConfig != null && !hasChangedConnectorType}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
selectedOptions={state.severityConfig}
|
||||
options={textOptions}
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
data-test-subj="swimlaneSeverityInput"
|
||||
onChange={(e) => editMappings('severityConfig', e)}
|
||||
isInvalid={mappingErrors?.severityConfig != null && !hasChangedConnectorType}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
<MappingField
|
||||
path="config.mappings.severityConfig"
|
||||
label={i18n.SW_SEVERITY_FIELD_LABEL}
|
||||
validationLabel={i18n.SW_REQUIRED_SEVERITY}
|
||||
options={textOptions}
|
||||
dataTestSubj="swimlaneSeverityInput"
|
||||
fieldIdMap={fieldIdMap}
|
||||
connectorType={connectorType}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)}
|
||||
{isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'caseIdConfig') && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
id="caseIdConfig"
|
||||
fullWidth
|
||||
label={i18n.SW_CASE_ID_FIELD_LABEL}
|
||||
error={mappingErrors?.caseIdConfig}
|
||||
isInvalid={mappingErrors?.caseIdConfig != null && !hasChangedConnectorType}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
selectedOptions={state.caseIdConfig}
|
||||
options={textOptions}
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
data-test-subj="swimlaneCaseIdConfig"
|
||||
onChange={(e) => editMappings('caseIdConfig', e)}
|
||||
isInvalid={mappingErrors?.caseIdConfig != null && !hasChangedConnectorType}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
<MappingField
|
||||
path="config.mappings.caseIdConfig"
|
||||
label={i18n.SW_CASE_ID_FIELD_LABEL}
|
||||
validationLabel={i18n.SW_REQUIRED_CASE_ID}
|
||||
options={textOptions}
|
||||
dataTestSubj="swimlaneCaseIdConfig"
|
||||
fieldIdMap={fieldIdMap}
|
||||
connectorType={connectorType}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)}
|
||||
{isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'caseNameConfig') && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
id="caseNameConfig"
|
||||
fullWidth
|
||||
label={i18n.SW_CASE_NAME_FIELD_LABEL}
|
||||
error={mappingErrors?.caseNameConfig}
|
||||
isInvalid={mappingErrors?.caseNameConfig != null && !hasChangedConnectorType}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
selectedOptions={state.caseNameConfig}
|
||||
options={textOptions}
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
data-test-subj="swimlaneCaseNameConfig"
|
||||
onChange={(e) => editMappings('caseNameConfig', e)}
|
||||
isInvalid={mappingErrors?.caseNameConfig != null && !hasChangedConnectorType}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
<MappingField
|
||||
path="config.mappings.caseNameConfig"
|
||||
label={i18n.SW_CASE_NAME_FIELD_LABEL}
|
||||
validationLabel={i18n.SW_REQUIRED_CASE_NAME}
|
||||
options={textOptions}
|
||||
dataTestSubj="swimlaneCaseNameConfig"
|
||||
fieldIdMap={fieldIdMap}
|
||||
connectorType={connectorType}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)}
|
||||
{isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'commentsConfig') && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
id="commentsConfig"
|
||||
fullWidth
|
||||
label={i18n.SW_COMMENTS_FIELD_LABEL}
|
||||
error={mappingErrors?.commentsConfig}
|
||||
isInvalid={mappingErrors?.commentsConfig != null && !hasChangedConnectorType}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
selectedOptions={state.commentsConfig}
|
||||
options={commentsOptions}
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
data-test-subj="swimlaneCommentsConfig"
|
||||
onChange={(e) => editMappings('commentsConfig', e)}
|
||||
isInvalid={mappingErrors?.commentsConfig != null && !hasChangedConnectorType}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
<MappingField
|
||||
path="config.mappings.commentsConfig"
|
||||
label={i18n.SW_COMMENTS_FIELD_LABEL}
|
||||
validationLabel={i18n.SW_REQUIRED_COMMENTS}
|
||||
options={commentsOptions}
|
||||
dataTestSubj="swimlaneCommentsConfig"
|
||||
fieldIdMap={fieldIdMap}
|
||||
connectorType={connectorType}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)}
|
||||
{isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'descriptionConfig') && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
id="descriptionConfig"
|
||||
fullWidth
|
||||
label={i18n.SW_DESCRIPTION_FIELD_LABEL}
|
||||
error={mappingErrors?.descriptionConfig}
|
||||
isInvalid={mappingErrors?.descriptionConfig != null && !hasChangedConnectorType}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
selectedOptions={state.descriptionConfig}
|
||||
options={textOptions}
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
data-test-subj="swimlaneDescriptionConfig"
|
||||
onChange={(e) => editMappings('descriptionConfig', e)}
|
||||
isInvalid={mappingErrors?.descriptionConfig != null && !hasChangedConnectorType}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
<MappingField
|
||||
path="config.mappings.descriptionConfig"
|
||||
label={i18n.SW_DESCRIPTION_FIELD_LABEL}
|
||||
validationLabel={i18n.SW_REQUIRED_DESCRIPTION}
|
||||
options={textOptions}
|
||||
dataTestSubj="swimlaneDescriptionConfig"
|
||||
fieldIdMap={fieldIdMap}
|
||||
connectorType={connectorType}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { SwimlaneActionConnector } from './types';
|
||||
import { registrationServicesMock } from '../../../../mocks';
|
||||
|
||||
const ACTION_TYPE_ID = '.swimlane';
|
||||
|
@ -29,152 +28,6 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('swimlane connector validation', () => {
|
||||
test('connector validation succeeds when connector is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {
|
||||
apiUrl: 'http:\\test',
|
||||
appId: '1234567asbd32',
|
||||
connectorType: 'all',
|
||||
mappings: {
|
||||
alertIdConfig: { id: '1234' },
|
||||
severityConfig: { id: '1234' },
|
||||
ruleNameConfig: { id: '1234' },
|
||||
caseIdConfig: { id: '1234' },
|
||||
caseNameConfig: { id: '1234' },
|
||||
descriptionConfig: { id: '1234' },
|
||||
commentsConfig: { id: '1234' },
|
||||
},
|
||||
},
|
||||
} as SwimlaneActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } },
|
||||
secrets: { errors: { apiToken: [] } },
|
||||
});
|
||||
});
|
||||
|
||||
test('it validates correctly when connectorType=all', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {
|
||||
apiUrl: 'http:\\test',
|
||||
appId: '1234567asbd32',
|
||||
connectorType: 'all',
|
||||
mappings: {},
|
||||
},
|
||||
} as SwimlaneActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } },
|
||||
secrets: { errors: { apiToken: [] } },
|
||||
});
|
||||
});
|
||||
|
||||
test('it validates correctly when connectorType=cases', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {
|
||||
apiUrl: 'http:\\test',
|
||||
appId: '1234567asbd32',
|
||||
connectorType: 'cases',
|
||||
mappings: {},
|
||||
},
|
||||
} as SwimlaneActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
appId: [],
|
||||
mappings: [
|
||||
{
|
||||
caseIdConfig: 'Case ID is required.',
|
||||
caseNameConfig: 'Case name is required.',
|
||||
commentsConfig: 'Comments are required.',
|
||||
descriptionConfig: 'Description is required.',
|
||||
},
|
||||
],
|
||||
connectorType: [],
|
||||
},
|
||||
},
|
||||
secrets: { errors: { apiToken: [] } },
|
||||
});
|
||||
});
|
||||
|
||||
test('it validates correctly when connectorType=alerts', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {
|
||||
apiUrl: 'http:\\test',
|
||||
appId: '1234567asbd32',
|
||||
connectorType: 'alerts',
|
||||
mappings: {},
|
||||
},
|
||||
} as SwimlaneActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: [],
|
||||
appId: [],
|
||||
mappings: [
|
||||
{
|
||||
alertIdConfig: 'Alert ID is required.',
|
||||
ruleNameConfig: 'Rule name is required.',
|
||||
},
|
||||
],
|
||||
connectorType: [],
|
||||
},
|
||||
},
|
||||
secrets: { errors: { apiToken: [] } },
|
||||
});
|
||||
});
|
||||
|
||||
test('it validates correctly required config/secrets fields', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {},
|
||||
} as SwimlaneActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
apiUrl: ['URL is required.'],
|
||||
appId: ['An App ID is required.'],
|
||||
mappings: [],
|
||||
connectorType: [],
|
||||
},
|
||||
},
|
||||
secrets: { errors: { apiToken: ['An API token is required.'] } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('swimlane action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
|
|
|
@ -5,22 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
ConnectorValidationResult,
|
||||
GenericValidationResult,
|
||||
} from '../../../../types';
|
||||
import {
|
||||
SwimlaneActionConnector,
|
||||
SwimlaneConfig,
|
||||
SwimlaneSecrets,
|
||||
SwimlaneActionParams,
|
||||
} from './types';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
import { validateMappingForConnector } from './helpers';
|
||||
import { ActionTypeModel, GenericValidationResult } from '../../../../types';
|
||||
import { SwimlaneConfig, SwimlaneSecrets, SwimlaneActionParams } from './types';
|
||||
|
||||
export const SW_SELECT_MESSAGE_TEXT = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText',
|
||||
|
@ -46,55 +34,6 @@ export function getActionType(): ActionTypeModel<
|
|||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: SW_SELECT_MESSAGE_TEXT,
|
||||
actionTypeTitle: SW_ACTION_TYPE_TITLE,
|
||||
validateConnector: async (
|
||||
action: SwimlaneActionConnector
|
||||
): Promise<ConnectorValidationResult<SwimlaneConfig, SwimlaneSecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
apiUrl: new Array<string>(),
|
||||
appId: new Array<string>(),
|
||||
connectorType: new Array<string>(),
|
||||
mappings: new Array<Record<string, string>>(),
|
||||
};
|
||||
const secretsErrors = {
|
||||
apiToken: new Array<string>(),
|
||||
};
|
||||
|
||||
const validationResult = {
|
||||
config: { errors: configErrors },
|
||||
secrets: { errors: secretsErrors },
|
||||
};
|
||||
|
||||
if (!action.config.apiUrl) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.SW_API_URL_REQUIRED];
|
||||
} else if (action.config.apiUrl) {
|
||||
if (!isValidUrl(action.config.apiUrl)) {
|
||||
configErrors.apiUrl = [...configErrors.apiUrl, translations.SW_API_URL_INVALID];
|
||||
}
|
||||
}
|
||||
|
||||
if (!action.secrets.apiToken) {
|
||||
secretsErrors.apiToken = [
|
||||
...secretsErrors.apiToken,
|
||||
translations.SW_REQUIRED_API_TOKEN_TEXT,
|
||||
];
|
||||
}
|
||||
|
||||
if (!action.config.appId) {
|
||||
configErrors.appId = [...configErrors.appId, translations.SW_REQUIRED_APP_ID_TEXT];
|
||||
}
|
||||
|
||||
const mappingErrors = validateMappingForConnector(
|
||||
action.config.connectorType,
|
||||
action.config.mappings
|
||||
);
|
||||
|
||||
if (!isEmpty(mappingErrors)) {
|
||||
configErrors.mappings = [...configErrors.mappings, mappingErrors];
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: async (
|
||||
actionParams: SwimlaneActionParams
|
||||
): Promise<GenericValidationResult<unknown>> => {
|
||||
|
|
|
@ -8,10 +8,13 @@
|
|||
import React from 'react';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { SwimlaneActionConnector } from './types';
|
||||
import SwimlaneActionConnectorFields from './swimlane_connectors';
|
||||
import { useGetApplication } from './use_get_application';
|
||||
import { applicationFields, mappings } from './mocks';
|
||||
import { ConnectorFormTestProvider } from '../test_utils';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('./use_get_application');
|
||||
|
@ -29,10 +32,6 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
|
||||
test('all connector fields are rendered', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {
|
||||
|
@ -41,18 +40,20 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
connectorType: 'all',
|
||||
mappings,
|
||||
},
|
||||
} as SwimlaneActionConnector;
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SwimlaneActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<SwimlaneActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -65,69 +66,12 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
expect(wrapper.find('[data-test-subj="swimlaneApiTokenInput"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display a message on create to remember credentials', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.swimlane',
|
||||
secrets: {},
|
||||
config: {},
|
||||
} as SwimlaneActionConnector;
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SwimlaneActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('should display a message on edit to re-enter credentials', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {
|
||||
apiUrl: 'http:\\test',
|
||||
appId: '1234567asbd32',
|
||||
connectorType: 'all',
|
||||
mappings,
|
||||
},
|
||||
} as SwimlaneActionConnector;
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SwimlaneActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('renders the mappings correctly - connector type all', async () => {
|
||||
getApplication.mockResolvedValue({
|
||||
fields: applicationFields,
|
||||
});
|
||||
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {
|
||||
|
@ -136,18 +80,20 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
connectorType: 'all',
|
||||
mappings,
|
||||
},
|
||||
} as SwimlaneActionConnector;
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SwimlaneActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<SwimlaneActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -156,13 +102,15 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('renders the mappings correctly - connector type cases', async () => {
|
||||
|
@ -171,10 +119,6 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
});
|
||||
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {
|
||||
|
@ -183,18 +127,20 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
connectorType: 'cases',
|
||||
mappings,
|
||||
},
|
||||
} as SwimlaneActionConnector;
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SwimlaneActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<SwimlaneActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -203,6 +149,7 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
await waitFor(() => {});
|
||||
expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeFalsy();
|
||||
|
@ -218,10 +165,6 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
});
|
||||
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {
|
||||
|
@ -230,18 +173,20 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
connectorType: 'alerts',
|
||||
mappings,
|
||||
},
|
||||
} as SwimlaneActionConnector;
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SwimlaneActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<SwimlaneActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -250,13 +195,15 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeFalsy();
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
test('renders the correct options per field', async () => {
|
||||
|
@ -265,19 +212,19 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
});
|
||||
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {
|
||||
apiUrl: 'http:\\test',
|
||||
apiUrl: 'http://test.com',
|
||||
appId: '1234567asbd32',
|
||||
connectorType: 'all',
|
||||
mappings,
|
||||
},
|
||||
} as SwimlaneActionConnector;
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const textOptions = [
|
||||
{ label: 'Alert Id (alert-id)', value: 'a6ide' },
|
||||
|
@ -291,17 +238,23 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
const commentOptions = [{ label: 'Comments (notes)', value: 'a6fdf' }];
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<SwimlaneActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<SwimlaneActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneApiUrlInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneAppIdInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="swimlaneApiTokenInput"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click');
|
||||
await nextTick();
|
||||
|
@ -330,4 +283,157 @@ describe('SwimlaneActionConnectorFields renders', () => {
|
|||
wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').first().prop('options')
|
||||
).toEqual(textOptions);
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
getApplication.mockResolvedValue({
|
||||
fields: applicationFields,
|
||||
});
|
||||
});
|
||||
|
||||
const getConnector = (connectorType: string = 'all') => ({
|
||||
actionTypeId: '.swimlane',
|
||||
name: 'swimlane',
|
||||
config: {
|
||||
apiUrl: 'http://test.com',
|
||||
appId: '1234567asbd32',
|
||||
connectorType: 'all',
|
||||
mappings,
|
||||
},
|
||||
secrets: {
|
||||
apiToken: 'test',
|
||||
},
|
||||
isDeprecated: false,
|
||||
});
|
||||
|
||||
const getConnectorWithEmptyMappings = (connectorType: string = 'all') => {
|
||||
const actionConnector = getConnector(connectorType);
|
||||
return {
|
||||
...actionConnector,
|
||||
config: {
|
||||
...actionConnector.config,
|
||||
connectorType,
|
||||
mappings: {},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const tests: Array<[string, string]> = [
|
||||
['swimlaneApiUrlInput', 'not-valid'],
|
||||
['swimlaneAppIdInput', ''],
|
||||
['swimlaneApiTokenInput', ''],
|
||||
];
|
||||
|
||||
it.each([['cases'], ['alerts']])(
|
||||
'connector validation succeeds when connector config is valid for connectorType=%p',
|
||||
async (connectorType) => {
|
||||
const connector = getConnector(connectorType);
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<SwimlaneActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: { ...connector }, isValid: true });
|
||||
}
|
||||
);
|
||||
|
||||
it.each(tests)('validates correctly %p', async (field, value) => {
|
||||
const connector = getConnector();
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<SwimlaneActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
|
||||
it('connector validation succeeds when when connectorType=all with empty mappings', async () => {
|
||||
const connector = getConnectorWithEmptyMappings();
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<SwimlaneActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...connector,
|
||||
config: {
|
||||
...connector.config,
|
||||
mappings: {
|
||||
alertIdConfig: null,
|
||||
caseIdConfig: null,
|
||||
caseNameConfig: null,
|
||||
commentsConfig: null,
|
||||
descriptionConfig: null,
|
||||
ruleNameConfig: null,
|
||||
severityConfig: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([['cases'], ['alerts']])(
|
||||
'validates correctly when when connectorType=%p',
|
||||
async (connectorType) => {
|
||||
const connector = getConnectorWithEmptyMappings(connectorType);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<SwimlaneActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
data: {},
|
||||
isValid: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,46 +5,42 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import React, { Fragment, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiForm,
|
||||
EuiSpacer,
|
||||
EuiStepsHorizontal,
|
||||
EuiStepStatus,
|
||||
EuiButton,
|
||||
EuiFormRow,
|
||||
EuiStepStatus,
|
||||
} from '@elastic/eui';
|
||||
import { useFormContext, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import {
|
||||
SwimlaneActionConnector,
|
||||
SwimlaneConnectorType,
|
||||
SwimlaneFieldMappingConfig,
|
||||
} from './types';
|
||||
import { SwimlaneFieldMappingConfig } from './types';
|
||||
import { SwimlaneConnection, SwimlaneFields } from './steps';
|
||||
import { useGetApplication } from './use_get_application';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const SwimlaneActionConnectorFields: React.FunctionComponent<
|
||||
ActionConnectorFieldsProps<SwimlaneActionConnector>
|
||||
> = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => {
|
||||
const SwimlaneActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
readOnly,
|
||||
}) => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const { apiUrl, appId, mappings, connectorType } = action.config;
|
||||
const { apiToken } = action.secrets;
|
||||
|
||||
const [hasConfigurationErrors, setHasConfigurationError] = useState(false);
|
||||
const { isValid, validateFields } = useFormContext();
|
||||
const [{ config, secrets }] = useFormData({
|
||||
watch: ['config.apiUrl', 'config.appId', 'secrets.apiToken'],
|
||||
});
|
||||
const { getApplication, isLoading: isLoadingApplication } = useGetApplication({
|
||||
toastNotifications: toasts,
|
||||
apiToken,
|
||||
appId,
|
||||
apiUrl,
|
||||
});
|
||||
|
||||
const hasConfigurationErrors =
|
||||
errors.apiUrl?.length > 0 || errors.appId?.length > 0 || errors.apiToken?.length > 0;
|
||||
|
||||
const apiUrl = config?.apiUrl ?? '';
|
||||
const appId = config?.appId ?? '';
|
||||
const apiToken = secrets?.apiToken ?? '';
|
||||
const [currentStep, setCurrentStep] = useState<number>(1);
|
||||
const [fields, setFields] = useState<SwimlaneFieldMappingConfig[]>([]);
|
||||
|
||||
|
@ -53,25 +49,37 @@ const SwimlaneActionConnectorFields: React.FunctionComponent<
|
|||
}, []);
|
||||
|
||||
const onNextStep = useCallback(async () => {
|
||||
setHasConfigurationError(false);
|
||||
|
||||
const { areFieldsValid } = await validateFields([
|
||||
'config.apiUrl',
|
||||
'config.appId',
|
||||
'secrets.apiToken',
|
||||
]);
|
||||
|
||||
if (!areFieldsValid) {
|
||||
setHasConfigurationError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch swimlane application configuration
|
||||
const application = await getApplication();
|
||||
const application = await getApplication({
|
||||
apiUrl,
|
||||
appId,
|
||||
apiToken,
|
||||
});
|
||||
|
||||
if (application?.fields) {
|
||||
const allFields = application.fields;
|
||||
setFields(allFields);
|
||||
setCurrentStep(2);
|
||||
}
|
||||
}, [getApplication]);
|
||||
}, [apiToken, apiUrl, appId, getApplication, validateFields]);
|
||||
|
||||
const resetConnection = useCallback(() => {
|
||||
setCurrentStep(1);
|
||||
}, []);
|
||||
|
||||
const hasMappingErrors = useMemo(
|
||||
() => Object.values(errors?.mappings ?? {}).some((mappingError) => mappingError.length !== 0),
|
||||
[errors?.mappings]
|
||||
);
|
||||
|
||||
const steps = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -88,7 +96,7 @@ const SwimlaneActionConnectorFields: React.FunctionComponent<
|
|||
title: i18n.SW_MAPPING_TITLE_TEXT_FIELD_LABEL,
|
||||
disabled: hasConfigurationErrors || isLoadingApplication,
|
||||
onClick: onNextStep,
|
||||
status: (hasMappingErrors
|
||||
status: (!isValid
|
||||
? 'danger'
|
||||
: currentStep === 2
|
||||
? 'selected'
|
||||
|
@ -98,68 +106,40 @@ const SwimlaneActionConnectorFields: React.FunctionComponent<
|
|||
[
|
||||
currentStep,
|
||||
hasConfigurationErrors,
|
||||
hasMappingErrors,
|
||||
isLoadingApplication,
|
||||
isValid,
|
||||
onNextStep,
|
||||
updateCurrentStep,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* Connector type needs to be updated on mount to All.
|
||||
* Otherwise it is undefined and this will cause an error
|
||||
* if the user saves the connector without going to the
|
||||
* second step. Same for mapping.
|
||||
*/
|
||||
useEffect(() => {
|
||||
editActionConfig('connectorType', connectorType ?? SwimlaneConnectorType.All);
|
||||
editActionConfig('mappings', mappings ?? {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiStepsHorizontal steps={steps} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiForm>
|
||||
{currentStep === 1 && (
|
||||
<>
|
||||
<SwimlaneConnection
|
||||
action={action}
|
||||
editActionConfig={editActionConfig}
|
||||
editActionSecrets={editActionSecrets}
|
||||
readOnly={readOnly}
|
||||
errors={errors}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow fullWidth helpText={i18n.SW_FIELDS_BUTTON_HELP_TEXT}>
|
||||
<EuiButton
|
||||
disabled={hasConfigurationErrors || isLoadingApplication}
|
||||
isLoading={isLoadingApplication}
|
||||
onClick={onNextStep}
|
||||
data-test-subj="swimlaneConfigureMapping"
|
||||
iconType="arrowRight"
|
||||
iconSide="right"
|
||||
>
|
||||
{i18n.SW_NEXT}
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<>
|
||||
<SwimlaneFields
|
||||
action={action}
|
||||
editActionConfig={editActionConfig}
|
||||
updateCurrentStep={updateCurrentStep}
|
||||
fields={fields}
|
||||
errors={errors}
|
||||
/>
|
||||
<EuiButton onClick={resetConnection} iconType="arrowLeft">
|
||||
{i18n.SW_BACK}
|
||||
<div style={{ display: currentStep === 1 ? 'block' : 'none' }}>
|
||||
<SwimlaneConnection readOnly={readOnly} />
|
||||
<EuiSpacer />
|
||||
<EuiFormRow fullWidth helpText={i18n.SW_FIELDS_BUTTON_HELP_TEXT}>
|
||||
<EuiButton
|
||||
// disabled={hasConfigurationErrors || isLoadingApplication}
|
||||
isLoading={isLoadingApplication}
|
||||
onClick={onNextStep}
|
||||
data-test-subj="swimlaneConfigureMapping"
|
||||
iconType="arrowRight"
|
||||
iconSide="right"
|
||||
>
|
||||
{i18n.SW_NEXT}
|
||||
</EuiButton>
|
||||
</>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
<div style={{ display: currentStep === 2 ? 'block' : 'none' }}>
|
||||
<SwimlaneFields fields={fields} readOnly={readOnly} />
|
||||
<EuiButton onClick={resetConnection} iconType="arrowLeft">
|
||||
{i18n.SW_BACK}
|
||||
</EuiButton>
|
||||
</div>
|
||||
</EuiForm>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -21,20 +21,6 @@ export const SW_REQUIRED_APP_ID_TEXT = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SW_REQUIRED_FIELD_MAPPINGS_TEXT = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredFieldMappingsText',
|
||||
{
|
||||
defaultMessage: 'Field mappings are required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SW_REQUIRED_API_TOKEN_TEXT = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredApiTokenText',
|
||||
{
|
||||
defaultMessage: 'An API token is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SW_GET_APPLICATION_API_ERROR = (id: string | null) =>
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage',
|
||||
|
@ -58,13 +44,6 @@ export const SW_API_URL_TEXT_FIELD_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SW_API_URL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.requiredApiUrlTextField',
|
||||
{
|
||||
defaultMessage: 'URL is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SW_API_URL_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.invalidApiUrlTextField',
|
||||
{
|
||||
|
@ -93,13 +72,6 @@ export const SW_MAPPING_TITLE_TEXT_FIELD_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SW_ALERT_SOURCE_FIELD_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Alert source',
|
||||
}
|
||||
);
|
||||
|
||||
export const SW_SEVERITY_FIELD_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.severityFieldLabel',
|
||||
{
|
||||
|
@ -107,13 +79,6 @@ export const SW_SEVERITY_FIELD_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SW_MAPPING_DESCRIPTION_TEXT_FIELD_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingDescriptionTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Used to specify the field names in the Swimlane Application',
|
||||
}
|
||||
);
|
||||
|
||||
export const SW_RULE_NAME_FIELD_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.ruleNameFieldLabel',
|
||||
{
|
||||
|
@ -156,26 +121,11 @@ export const SW_DESCRIPTION_FIELD_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SW_REMEMBER_VALUE_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel',
|
||||
{ defaultMessage: 'Remember this value. You must reenter it each time you edit the connector.' }
|
||||
);
|
||||
|
||||
export const SW_REENTER_VALUE_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.reenterValueLabel',
|
||||
{ defaultMessage: 'This key is encrypted. Please reenter a value for this field.' }
|
||||
);
|
||||
|
||||
export const SW_CONFIGURE_CONNECTION_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.configureConnectionLabel',
|
||||
{ defaultMessage: 'Configure API Connection' }
|
||||
);
|
||||
|
||||
export const SW_RETRIEVE_CONFIGURATION_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.retrieveConfigurationLabel',
|
||||
{ defaultMessage: 'Configure Fields' }
|
||||
);
|
||||
|
||||
export const SW_CONNECTOR_TYPE_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.connectorType',
|
||||
{
|
||||
|
@ -183,13 +133,6 @@ export const SW_CONNECTOR_TYPE_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SW_FIELD_MAPPING_IS_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingFieldRequired',
|
||||
{
|
||||
defaultMessage: 'Field mapping is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle',
|
||||
{
|
||||
|
@ -205,13 +148,6 @@ export const EMPTY_MAPPING_WARNING_DESC = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SW_REQUIRED_ALERT_SOURCE = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource',
|
||||
{
|
||||
defaultMessage: 'Alert source is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SW_REQUIRED_SEVERITY = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredSeverity',
|
||||
{
|
||||
|
|
|
@ -9,7 +9,6 @@ import { renderHook, act } from '@testing-library/react-hooks';
|
|||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { getApplication } from './api';
|
||||
import { SwimlaneActionConnector } from './types';
|
||||
import { useGetApplication, UseGetApplication } from './use_get_application';
|
||||
|
||||
jest.mock('./api');
|
||||
|
@ -30,7 +29,7 @@ const action = {
|
|||
appId: 'bcq16kdTbz5jlwM6h',
|
||||
mappings: {},
|
||||
},
|
||||
} as SwimlaneActionConnector;
|
||||
};
|
||||
|
||||
describe('useGetApplication', () => {
|
||||
const { services } = useKibanaMock();
|
||||
|
@ -48,9 +47,6 @@ describe('useGetApplication', () => {
|
|||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() =>
|
||||
useGetApplication({
|
||||
appId: action.config.appId,
|
||||
apiToken: action.secrets.apiToken,
|
||||
apiUrl: action.config.apiUrl,
|
||||
toastNotifications: services.notifications.toasts,
|
||||
})
|
||||
);
|
||||
|
@ -67,16 +63,18 @@ describe('useGetApplication', () => {
|
|||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() =>
|
||||
useGetApplication({
|
||||
appId: action.config.appId,
|
||||
apiToken: action.secrets.apiToken,
|
||||
apiUrl: action.config.apiUrl,
|
||||
toastNotifications: services.notifications.toasts,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.getApplication();
|
||||
result.current.getApplication({
|
||||
appId: action.config.appId,
|
||||
apiToken: action.secrets.apiToken,
|
||||
apiUrl: action.config.apiUrl,
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
expect(getApplicationMock).toBeCalledWith({
|
||||
signal: abortCtrl.signal,
|
||||
|
@ -91,15 +89,16 @@ describe('useGetApplication', () => {
|
|||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() =>
|
||||
useGetApplication({
|
||||
appId: action.config.appId,
|
||||
apiToken: action.secrets.apiToken,
|
||||
apiUrl: action.config.apiUrl,
|
||||
toastNotifications: services.notifications.toasts,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
result.current.getApplication();
|
||||
result.current.getApplication({
|
||||
appId: action.config.appId,
|
||||
apiToken: action.secrets.apiToken,
|
||||
apiUrl: action.config.apiUrl,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({
|
||||
|
@ -113,15 +112,16 @@ describe('useGetApplication', () => {
|
|||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() =>
|
||||
useGetApplication({
|
||||
appId: action.config.appId,
|
||||
apiToken: action.secrets.apiToken,
|
||||
apiUrl: action.config.apiUrl,
|
||||
toastNotifications: services.notifications.toasts,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
result.current.getApplication();
|
||||
result.current.getApplication({
|
||||
appId: action.config.appId,
|
||||
apiToken: action.secrets.apiToken,
|
||||
apiUrl: action.config.apiUrl,
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
});
|
||||
|
@ -135,14 +135,15 @@ describe('useGetApplication', () => {
|
|||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() =>
|
||||
useGetApplication({
|
||||
appId: action.config.appId,
|
||||
apiToken: action.secrets.apiToken,
|
||||
apiUrl: action.config.apiUrl,
|
||||
toastNotifications: services.notifications.toasts,
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
result.current.getApplication();
|
||||
result.current.getApplication({
|
||||
appId: action.config.appId,
|
||||
apiToken: action.secrets.apiToken,
|
||||
apiUrl: action.config.apiUrl,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
|
@ -162,14 +163,15 @@ describe('useGetApplication', () => {
|
|||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() =>
|
||||
useGetApplication({
|
||||
appId: action.config.appId,
|
||||
apiToken: action.secrets.apiToken,
|
||||
apiUrl: action.config.apiUrl,
|
||||
toastNotifications: services.notifications.toasts,
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
result.current.getApplication();
|
||||
result.current.getApplication({
|
||||
appId: action.config.appId,
|
||||
apiToken: action.secrets.apiToken,
|
||||
apiUrl: action.config.apiUrl,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ToastsApi } from '@kbn/core/public';
|
||||
import { getApplication as getApplicationApi } from './api';
|
||||
import * as i18n from './translations';
|
||||
|
@ -22,58 +23,64 @@ interface Props {
|
|||
}
|
||||
|
||||
export interface UseGetApplication {
|
||||
getApplication: () => Promise<{ fields?: SwimlaneFieldMappingConfig[] } | undefined>;
|
||||
getApplication: (
|
||||
args: Omit<Props, 'toastNotifications'>
|
||||
) => Promise<{ fields?: SwimlaneFieldMappingConfig[] } | undefined>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const useGetApplication = ({
|
||||
toastNotifications,
|
||||
appId,
|
||||
apiToken,
|
||||
apiUrl,
|
||||
}: Props): UseGetApplication => {
|
||||
}: Pick<Props, 'toastNotifications'>): UseGetApplication => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const isCancelledRef = useRef(false);
|
||||
const abortCtrlRef = useRef(new AbortController());
|
||||
|
||||
const getApplication = useCallback(async () => {
|
||||
try {
|
||||
isCancelledRef.current = false;
|
||||
abortCtrlRef.current.abort();
|
||||
abortCtrlRef.current = new AbortController();
|
||||
setIsLoading(true);
|
||||
|
||||
const data = await getApplicationApi({
|
||||
signal: abortCtrlRef.current.signal,
|
||||
appId,
|
||||
apiToken,
|
||||
url: apiUrl,
|
||||
});
|
||||
|
||||
if (!isCancelledRef.current) {
|
||||
setIsLoading(false);
|
||||
if (!data.fields) {
|
||||
// If the response was malformed and fields doesn't exist, show an error toast
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.SW_GET_APPLICATION_API_ERROR(appId),
|
||||
text: i18n.SW_GET_APPLICATION_API_NO_FIELDS_ERROR,
|
||||
});
|
||||
const getApplication = useCallback(
|
||||
async ({ appId, apiToken, apiUrl }: Omit<Props, 'toastNotifications'>) => {
|
||||
try {
|
||||
if (isEmpty(appId) || isEmpty(apiToken) || isEmpty(apiUrl)) {
|
||||
return;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCancelledRef.current) {
|
||||
if (error.name !== 'AbortError') {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.SW_GET_APPLICATION_API_ERROR(appId),
|
||||
text: error.message,
|
||||
});
|
||||
|
||||
isCancelledRef.current = false;
|
||||
abortCtrlRef.current.abort();
|
||||
abortCtrlRef.current = new AbortController();
|
||||
setIsLoading(true);
|
||||
|
||||
const data = await getApplicationApi({
|
||||
signal: abortCtrlRef.current.signal,
|
||||
appId,
|
||||
apiToken,
|
||||
url: apiUrl,
|
||||
});
|
||||
|
||||
if (!isCancelledRef.current) {
|
||||
setIsLoading(false);
|
||||
if (!data.fields) {
|
||||
// If the response was malformed and fields doesn't exist, show an error toast
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.SW_GET_APPLICATION_API_ERROR(appId),
|
||||
text: i18n.SW_GET_APPLICATION_API_NO_FIELDS_ERROR,
|
||||
});
|
||||
return;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCancelledRef.current) {
|
||||
if (error.name !== 'AbortError') {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.SW_GET_APPLICATION_API_ERROR(appId),
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}, [apiToken, apiUrl, appId, toastNotifications]);
|
||||
},
|
||||
[toastNotifications]
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { TeamsActionConnector } from '../types';
|
||||
import { registrationServicesMock } from '../../../../mocks';
|
||||
|
||||
const ACTION_TYPE_ID = '.teams';
|
||||
|
@ -29,98 +28,6 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('teams connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'https:\\test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'team',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
webhookUrl: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - empty webhook url', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'team',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
webhookUrl: ['Webhook URL is required.'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - invalid webhook url', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'h',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'team',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
webhookUrl: ['Webhook URL is invalid.'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - invalid webhook url protocol', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http://insecure',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'team',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
webhookUrl: ['Webhook URL must start with https://.'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('teams action params validation', () => {
|
||||
test('if action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
|
|
|
@ -7,13 +7,8 @@
|
|||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
GenericValidationResult,
|
||||
ConnectorValidationResult,
|
||||
} from '../../../../types';
|
||||
import { TeamsActionParams, TeamsSecrets, TeamsActionConnector } from '../types';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
import { ActionTypeModel, GenericValidationResult } from '../../../../types';
|
||||
import { TeamsActionParams, TeamsSecrets } from '../types';
|
||||
|
||||
export function getActionType(): ActionTypeModel<unknown, TeamsSecrets, TeamsActionParams> {
|
||||
return {
|
||||
|
@ -31,25 +26,6 @@ export function getActionType(): ActionTypeModel<unknown, TeamsSecrets, TeamsAct
|
|||
defaultMessage: 'Send a message to a Microsoft Teams channel.',
|
||||
}
|
||||
),
|
||||
validateConnector: async (
|
||||
action: TeamsActionConnector
|
||||
): 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(translations.WEBHOOK_URL_REQUIRED);
|
||||
} else if (action.secrets.webhookUrl) {
|
||||
if (!isValidUrl(action.secrets.webhookUrl)) {
|
||||
secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_INVALID);
|
||||
} else if (!isValidUrl(action.secrets.webhookUrl, 'https:')) {
|
||||
secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_HTTP_INVALID);
|
||||
}
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: async (
|
||||
actionParams: TeamsActionParams
|
||||
): Promise<GenericValidationResult<TeamsActionParams>> => {
|
||||
|
|
|
@ -7,112 +7,126 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { act } from '@testing-library/react';
|
||||
import { TeamsActionConnector } from '../types';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import TeamsActionFields from './teams_connectors';
|
||||
import { ConnectorFormTestProvider } from '../test_utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
describe('TeamsActionFields renders', () => {
|
||||
test('all connector fields are rendered', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'https:\\test',
|
||||
webhookUrl: 'https://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'teams',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<TeamsActionFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], webhookUrl: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<TeamsActionFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="teamsWebhookUrlInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="teamsWebhookUrlInput"]').first().prop('value')).toBe(
|
||||
'https:\\test'
|
||||
'https://test.com'
|
||||
);
|
||||
});
|
||||
|
||||
test('should display a message on create to remember credentials', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.teams',
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as TeamsActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<TeamsActionFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], webhookUrl: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
test('should display a message on edit to re-enter credentials', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http:\\test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'teams',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<TeamsActionFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], webhookUrl: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should display a message for missing secrets after import', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http:\\test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
isMissingSecrets: true,
|
||||
name: 'teams',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<TeamsActionFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], webhookUrl: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0);
|
||||
it('connector validation succeeds when connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'https://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'teams',
|
||||
config: {},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<TeamsActionFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
secrets: {
|
||||
webhookUrl: 'https://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'teams',
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('validates teh web hook url field correctly', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'https://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'teams',
|
||||
config: {},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<TeamsActionFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(
|
||||
getByTestId('teamsWebhookUrlInput'),
|
||||
`{selectall}{backspace}no-valid`,
|
||||
{
|
||||
delay: 10,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,74 +6,54 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { DocLinksStart } from '@kbn/core/public';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { TeamsActionConnector } from '../types';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const TeamsActionFields: React.FunctionComponent<
|
||||
ActionConnectorFieldsProps<TeamsActionConnector>
|
||||
> = ({ action, editActionSecrets, errors, readOnly }) => {
|
||||
const { webhookUrl } = action.secrets;
|
||||
const { urlField } = fieldValidators;
|
||||
|
||||
const getWebhookUrlConfig = (docLinks: DocLinksStart): FieldConfig => ({
|
||||
label: i18n.WEBHOOK_URL_LABEL,
|
||||
helpText: (
|
||||
<EuiLink href={docLinks.links.alerting.teamsAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel"
|
||||
defaultMessage="Create a Microsoft Teams Webhook URL"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: urlField(i18n.WEBHOOK_URL_INVALID),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const TeamsActionFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
readOnly,
|
||||
isEdit,
|
||||
}) => {
|
||||
const { docLinks } = useKibana().services;
|
||||
|
||||
const isWebhookUrlInvalid: boolean =
|
||||
errors.webhookUrl !== undefined && errors.webhookUrl.length > 0 && webhookUrl !== undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
id="webhookUrl"
|
||||
fullWidth
|
||||
helpText={
|
||||
<EuiLink href={docLinks.links.alerting.teamsAction} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel"
|
||||
defaultMessage="Create a Microsoft Teams Webhook URL"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
error={errors.webhookUrl}
|
||||
isInvalid={isWebhookUrlInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Webhook URL',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
1,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.reenterValueLabel',
|
||||
{ defaultMessage: 'This URL is encrypted. Please reenter a value for this field.' }
|
||||
)
|
||||
)}
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isWebhookUrlInvalid}
|
||||
name="webhookUrl"
|
||||
readOnly={readOnly}
|
||||
value={webhookUrl || ''}
|
||||
data-test-subj="teamsWebhookUrlInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('webhookUrl', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!webhookUrl) {
|
||||
editActionSecrets('webhookUrl', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
<UseField
|
||||
path="secrets.webhookUrl"
|
||||
config={getWebhookUrlConfig(docLinks)}
|
||||
component={Field}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
readOnly,
|
||||
'data-test-subj': 'teamsWebhookUrlInput',
|
||||
fullWidth: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const WEBHOOK_URL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText',
|
||||
export const WEBHOOK_URL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.webhookUrlTextLabel',
|
||||
{
|
||||
defaultMessage: 'Webhook URL is required.',
|
||||
defaultMessage: 'Webhook URL',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -21,13 +21,6 @@ export const WEBHOOK_URL_INVALID = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
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',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { of } from 'rxjs';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { Form, useForm, FormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { ConnectorServices } from '../../../types';
|
||||
import { TriggersAndActionsUiServices } from '../../..';
|
||||
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
|
||||
import { ConnectorFormSchema } from '../../sections/action_connector_form/types';
|
||||
import { ConnectorFormFieldsGlobal } from '../../sections/action_connector_form/connector_form_fields_global';
|
||||
import { ConnectorProvider } from '../../context/connector_context';
|
||||
|
||||
interface FormTestProviderProps {
|
||||
children: React.ReactNode;
|
||||
defaultValue?: Record<string, unknown>;
|
||||
onSubmit?: ({ data, isValid }: { data: FormData; isValid: boolean }) => Promise<void>;
|
||||
connectorServices?: ConnectorServices;
|
||||
}
|
||||
|
||||
type ConnectorFormTestProviderProps = Omit<FormTestProviderProps, 'defaultValue'> & {
|
||||
connector: ConnectorFormSchema;
|
||||
};
|
||||
|
||||
const ConnectorFormTestProviderComponent: React.FC<ConnectorFormTestProviderProps> = ({
|
||||
children,
|
||||
connector,
|
||||
onSubmit,
|
||||
connectorServices,
|
||||
}) => {
|
||||
return (
|
||||
<FormTestProviderComponent
|
||||
defaultValue={connector}
|
||||
onSubmit={onSubmit}
|
||||
connectorServices={connectorServices}
|
||||
>
|
||||
<ConnectorFormFieldsGlobal canSave={true} />
|
||||
{children}
|
||||
</FormTestProviderComponent>
|
||||
);
|
||||
};
|
||||
|
||||
ConnectorFormTestProviderComponent.displayName = 'ConnectorFormTestProvider';
|
||||
export const ConnectorFormTestProvider = React.memo(ConnectorFormTestProviderComponent);
|
||||
|
||||
const FormTestProviderComponent: React.FC<FormTestProviderProps> = ({
|
||||
children,
|
||||
defaultValue,
|
||||
onSubmit,
|
||||
connectorServices = { validateEmailAddresses: jest.fn() },
|
||||
}) => {
|
||||
const { form } = useForm({ defaultValue });
|
||||
const { submit } = form;
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
const res = await submit();
|
||||
if (onSubmit) {
|
||||
onSubmit(res);
|
||||
}
|
||||
}, [onSubmit, submit]);
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<ConnectorProvider value={{ services: connectorServices }}>
|
||||
<Form form={form}>{children}</Form>
|
||||
<EuiButton data-test-subj="form-test-provide-submit" onClick={onClick} />
|
||||
</ConnectorProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
FormTestProviderComponent.displayName = 'FormTestProvider';
|
||||
export const FormTestProvider = React.memo(FormTestProviderComponent);
|
||||
|
||||
export async function waitForComponentToPaint<P = {}>(wrapper: ReactWrapper<P>, amount = 0) {
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, amount));
|
||||
wrapper.update();
|
||||
});
|
||||
}
|
||||
|
||||
export const waitForComponentToUpdate = async () =>
|
||||
await act(async () => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
|
||||
export interface AppMockRenderer {
|
||||
render: UiRender;
|
||||
coreStart: TriggersAndActionsUiServices;
|
||||
}
|
||||
|
||||
export const createAppMockRenderer = (): AppMockRenderer => {
|
||||
const services = createStartServicesMock();
|
||||
const theme$ = of({ darkMode: false });
|
||||
|
||||
const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => (
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider services={services}>
|
||||
<KibanaThemeProvider theme$={theme$}>{children}</KibanaThemeProvider>
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
AppWrapper.displayName = 'AppWrapper';
|
||||
const render: UiRender = (ui, options) => {
|
||||
return reactRender(ui, {
|
||||
wrapper: AppWrapper,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
return {
|
||||
coreStart: services,
|
||||
render,
|
||||
};
|
||||
};
|
|
@ -7,10 +7,73 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const URL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText',
|
||||
export const METHOD_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.methodTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'URL is required.',
|
||||
defaultMessage: 'Method',
|
||||
}
|
||||
);
|
||||
|
||||
export const HAS_AUTH_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.hasAuthSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Require authentication for this webhook',
|
||||
}
|
||||
);
|
||||
|
||||
export const URL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.urlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'URL',
|
||||
}
|
||||
);
|
||||
|
||||
export const USERNAME_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Username',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Password',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_HEADERS_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch',
|
||||
{
|
||||
defaultMessage: 'Add HTTP header',
|
||||
}
|
||||
);
|
||||
|
||||
export const HEADER_KEY_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.headerKeyTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Key',
|
||||
}
|
||||
);
|
||||
|
||||
export const REMOVE_ITEM_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.removeHeaderIconLabel',
|
||||
{
|
||||
defaultMessage: 'Key',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_HEADER_BTN = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add header',
|
||||
}
|
||||
);
|
||||
|
||||
export const HEADER_VALUE_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.headerValueTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Value',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -35,27 +98,6 @@ export const USERNAME_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
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',
|
||||
{
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { WebhookActionConnector } from '../types';
|
||||
import { registrationServicesMock } from '../../../../mocks';
|
||||
|
||||
const ACTION_TYPE_ID = '.webhook';
|
||||
|
@ -30,140 +29,6 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('webhook connector validation', () => {
|
||||
test('connector validation succeeds when hasAuth is true and connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.webhook',
|
||||
name: 'webhook',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'http://test.com',
|
||||
headers: { 'content-type': 'text' },
|
||||
hasAuth: true,
|
||||
},
|
||||
} as WebhookActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
url: [],
|
||||
method: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation succeeds when hasAuth is false and connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: '',
|
||||
password: '',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.webhook',
|
||||
name: 'webhook',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'http://test.com',
|
||||
headers: { 'content-type': 'text' },
|
||||
hasAuth: false,
|
||||
},
|
||||
} as WebhookActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
url: [],
|
||||
method: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.webhook',
|
||||
name: 'webhook',
|
||||
config: {
|
||||
method: 'PUT',
|
||||
hasAuth: true,
|
||||
},
|
||||
} as WebhookActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
url: ['URL is required.'],
|
||||
method: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: ['Password is required when username is used.'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when url in config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.webhook',
|
||||
name: 'webhook',
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'invalid.url',
|
||||
hasAuth: true,
|
||||
},
|
||||
} as WebhookActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
url: ['URL is invalid.'],
|
||||
method: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('webhook action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
|
|
|
@ -7,18 +7,8 @@
|
|||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
GenericValidationResult,
|
||||
ConnectorValidationResult,
|
||||
} from '../../../../types';
|
||||
import {
|
||||
WebhookActionParams,
|
||||
WebhookConfig,
|
||||
WebhookSecrets,
|
||||
WebhookActionConnector,
|
||||
} from '../types';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
import { ActionTypeModel, GenericValidationResult } from '../../../../types';
|
||||
import { WebhookActionParams, WebhookConfig, WebhookSecrets } from '../types';
|
||||
|
||||
export function getActionType(): ActionTypeModel<
|
||||
WebhookConfig,
|
||||
|
@ -40,47 +30,6 @@ export function getActionType(): ActionTypeModel<
|
|||
defaultMessage: 'Webhook data',
|
||||
}
|
||||
),
|
||||
validateConnector: async (
|
||||
action: WebhookActionConnector
|
||||
): Promise<
|
||||
ConnectorValidationResult<Pick<WebhookConfig, 'url' | 'method'>, WebhookSecrets>
|
||||
> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
url: new Array<string>(),
|
||||
method: new Array<string>(),
|
||||
};
|
||||
const secretsErrors = {
|
||||
user: new Array<string>(),
|
||||
password: new Array<string>(),
|
||||
};
|
||||
const validationResult = {
|
||||
config: { errors: configErrors },
|
||||
secrets: { errors: secretsErrors },
|
||||
};
|
||||
if (!action.config.url) {
|
||||
configErrors.url.push(translations.URL_REQUIRED);
|
||||
}
|
||||
if (action.config.url && !isValidUrl(action.config.url)) {
|
||||
configErrors.url = [...configErrors.url, translations.URL_INVALID];
|
||||
}
|
||||
if (!action.config.method) {
|
||||
configErrors.method.push(translations.METHOD_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.user.push(translations.USERNAME_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED);
|
||||
}
|
||||
if (action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED_FOR_USER);
|
||||
}
|
||||
if (!action.secrets.user && action.secrets.password) {
|
||||
secretsErrors.user.push(translations.USERNAME_REQUIRED_FOR_PASSWORD);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: async (
|
||||
actionParams: WebhookActionParams
|
||||
): Promise<GenericValidationResult<WebhookActionParams>> => {
|
||||
|
|
|
@ -7,41 +7,42 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { WebhookActionConnector } from '../types';
|
||||
import WebhookActionConnectorFields from './webhook_connectors';
|
||||
import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../test_utils';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('WebhookActionConnectorFields renders', () => {
|
||||
test('all connector fields is rendered', () => {
|
||||
test('all connector fields is rendered', async () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.webhook',
|
||||
name: 'webhook',
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'https://test.com',
|
||||
headers: [{ key: 'content-type', value: 'text' }],
|
||||
hasAuth: true,
|
||||
},
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.webhook',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'webhook',
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'http:\\test',
|
||||
headers: { 'content-type': 'text' },
|
||||
hasAuth: true,
|
||||
},
|
||||
} as WebhookActionConnector;
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<WebhookActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ url: [], method: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<WebhookActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy();
|
||||
wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').first().simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy();
|
||||
|
@ -49,94 +50,216 @@ describe('WebhookActionConnectorFields renders', () => {
|
|||
expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display a message on create to remember credentials', () => {
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
actionTypeId: '.webhook',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'webhook',
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'https://test.com',
|
||||
headers: [{ key: 'content-type', value: 'text' }],
|
||||
hasAuth: true,
|
||||
},
|
||||
} as WebhookActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<WebhookActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ url: [], method: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('should display a message on edit to re-enter credentials', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.webhook',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'webhook',
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'http:\\test',
|
||||
headers: { 'content-type': 'text' },
|
||||
hasAuth: true,
|
||||
},
|
||||
} as WebhookActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<WebhookActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ url: [], method: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
};
|
||||
|
||||
test('should display a message for missing secrets after import', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.webhook',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: true,
|
||||
name: 'webhook',
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'http:\\test',
|
||||
headers: { 'content-type': 'text' },
|
||||
hasAuth: true,
|
||||
},
|
||||
} as WebhookActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<WebhookActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ url: [], method: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0);
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const tests: Array<[string, string]> = [
|
||||
['webhookUrlText', 'not-valid'],
|
||||
['webhookUserInput', ''],
|
||||
['webhookPasswordInput', ''],
|
||||
];
|
||||
|
||||
it('connector validation succeeds when connector config is valid', async () => {
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<WebhookActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.webhook',
|
||||
name: 'webhook',
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'https://test.com',
|
||||
headers: [{ key: 'content-type', value: 'text' }],
|
||||
hasAuth: true,
|
||||
},
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
__internal__: {
|
||||
hasHeaders: true,
|
||||
},
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('connector validation succeeds when auth=false', async () => {
|
||||
const connector = {
|
||||
...actionConnector,
|
||||
config: {
|
||||
...actionConnector.config,
|
||||
hasAuth: false,
|
||||
},
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<WebhookActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.webhook',
|
||||
name: 'webhook',
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'https://test.com',
|
||||
headers: [{ key: 'content-type', value: 'text' }],
|
||||
hasAuth: false,
|
||||
},
|
||||
__internal__: {
|
||||
hasHeaders: true,
|
||||
},
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('connector validation succeeds without headers', async () => {
|
||||
const connector = {
|
||||
...actionConnector,
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'https://test.com',
|
||||
hasAuth: true,
|
||||
},
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<WebhookActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.webhook',
|
||||
name: 'webhook',
|
||||
config: {
|
||||
method: 'PUT',
|
||||
url: 'https://test.com',
|
||||
hasAuth: true,
|
||||
},
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
__internal__: {
|
||||
hasHeaders: false,
|
||||
},
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('validates correctly if the method is empty', async () => {
|
||||
const connector = {
|
||||
...actionConnector,
|
||||
config: {
|
||||
...actionConnector.config,
|
||||
method: '',
|
||||
},
|
||||
};
|
||||
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<WebhookActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
|
||||
it.each(tests)('validates correctly %p', async (field, value) => {
|
||||
const connector = {
|
||||
...actionConnector,
|
||||
config: {
|
||||
...actionConnector.config,
|
||||
headers: [],
|
||||
},
|
||||
};
|
||||
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<WebhookActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,285 +5,92 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import {
|
||||
EuiFieldPassword,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiButtonIcon,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListDescription,
|
||||
EuiDescriptionListTitle,
|
||||
EuiTitle,
|
||||
EuiSwitch,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
UseArray,
|
||||
UseField,
|
||||
useFormContext,
|
||||
useFormData,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import {
|
||||
Field,
|
||||
SelectField,
|
||||
TextField,
|
||||
ToggleField,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { WebhookActionConnector } from '../types';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label';
|
||||
import * as i18n from './translations';
|
||||
import { PasswordField } from '../../password_field';
|
||||
|
||||
const HTTP_VERBS = ['post', 'put'];
|
||||
const { emptyField, urlField } = fieldValidators;
|
||||
|
||||
const WebhookActionConnectorFields: React.FunctionComponent<
|
||||
ActionConnectorFieldsProps<WebhookActionConnector>
|
||||
> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => {
|
||||
const { user, password } = action.secrets;
|
||||
const { method, url, headers, hasAuth } = action.config;
|
||||
|
||||
const [httpHeaderKey, setHttpHeaderKey] = useState<string>('');
|
||||
const [httpHeaderValue, setHttpHeaderValue] = useState<string>('');
|
||||
const [hasHeaders, setHasHeaders] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!action.id) {
|
||||
editActionConfig('hasAuth', true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (!method) {
|
||||
editActionConfig('method', 'post'); // set method to POST by default
|
||||
}
|
||||
|
||||
const headerErrors = {
|
||||
keyHeader: new Array<string>(),
|
||||
valueHeader: new Array<string>(),
|
||||
};
|
||||
if (!httpHeaderKey && httpHeaderValue) {
|
||||
headerErrors.keyHeader.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderKeyText',
|
||||
{
|
||||
defaultMessage: 'Key is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
if (httpHeaderKey && !httpHeaderValue) {
|
||||
headerErrors.valueHeader.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText',
|
||||
{
|
||||
defaultMessage: 'Value is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
const updatedHeaders = headers
|
||||
? { ...headers, [httpHeaderKey]: httpHeaderValue }
|
||||
: { [httpHeaderKey]: httpHeaderValue };
|
||||
editActionConfig('headers', updatedHeaders);
|
||||
setHttpHeaderKey('');
|
||||
setHttpHeaderValue('');
|
||||
}
|
||||
|
||||
function viewHeaders() {
|
||||
setHasHeaders(!hasHeaders);
|
||||
if (!hasHeaders && !headers) {
|
||||
editActionConfig('headers', {});
|
||||
}
|
||||
}
|
||||
|
||||
function removeHeader(keyToRemove: string) {
|
||||
const updatedHeaders = Object.keys(headers)
|
||||
.filter((key) => key !== keyToRemove)
|
||||
.reduce((headerToRemove: Record<string, string>, key: string) => {
|
||||
headerToRemove[key] = headers[key];
|
||||
return headerToRemove;
|
||||
}, {});
|
||||
editActionConfig('headers', updatedHeaders);
|
||||
}
|
||||
|
||||
let headerControl;
|
||||
if (hasHeaders) {
|
||||
headerControl = (
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add header"
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup gutterSize="s" alignItems="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
id="webhookHeaderKey"
|
||||
fullWidth
|
||||
error={headerErrors.keyHeader}
|
||||
isInvalid={hasHeaderErrors && httpHeaderKey !== undefined}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.keyTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Key',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={hasHeaderErrors && httpHeaderKey !== undefined}
|
||||
name="keyHeader"
|
||||
readOnly={readOnly}
|
||||
value={httpHeaderKey}
|
||||
data-test-subj="webhookHeadersKeyInput"
|
||||
onChange={(e) => {
|
||||
setHttpHeaderKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
id="webhookHeaderValue"
|
||||
fullWidth
|
||||
error={headerErrors.valueHeader}
|
||||
isInvalid={hasHeaderErrors && httpHeaderValue !== undefined}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.valueTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Value',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={hasHeaderErrors && httpHeaderValue !== undefined}
|
||||
name="valueHeader"
|
||||
readOnly={readOnly}
|
||||
value={httpHeaderValue}
|
||||
data-test-subj="webhookHeadersValueInput"
|
||||
onChange={(e) => {
|
||||
setHttpHeaderValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow hasEmptyLabelSpace>
|
||||
<EuiButtonEmpty
|
||||
isDisabled={hasHeaders && (hasHeaderErrors || !httpHeaderKey || !httpHeaderValue)}
|
||||
data-test-subj="webhookAddHeaderButton"
|
||||
onClick={() => addHeader()}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add"
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const headersList = Object.keys(headers || {}).map((key: string) => {
|
||||
return (
|
||||
<EuiFlexGroup key={key} data-test-subj="webhookHeaderText" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.deleteHeaderButton',
|
||||
{
|
||||
defaultMessage: 'Delete',
|
||||
description: 'Delete HTTP header',
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={() => removeHeader(key)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiDescriptionList compressed>
|
||||
<EuiDescriptionListTitle>{key}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{headers[key]}</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
const WebhookActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
readOnly,
|
||||
}) => {
|
||||
const { getFieldDefaultValue } = useFormContext();
|
||||
const [{ config, __internal__ }] = useFormData({
|
||||
watch: ['config.hasAuth', '__internal__.hasHeaders'],
|
||||
});
|
||||
|
||||
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;
|
||||
const hasHeadersDefaultValue = !!getFieldDefaultValue<boolean | undefined>('config.headers');
|
||||
|
||||
const hasAuth = config == null ? true : config.hasAuth;
|
||||
const hasHeaders = __internal__ != null ? __internal__.hasHeaders : false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.methodTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Method',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiSelect
|
||||
name="method"
|
||||
value={method || 'post'}
|
||||
disabled={readOnly}
|
||||
data-test-subj="webhookMethodSelect"
|
||||
options={HTTP_VERBS.map((verb) => ({ text: verb.toUpperCase(), value: verb }))}
|
||||
onChange={(e) => {
|
||||
editActionConfig('method', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path="config.method"
|
||||
component={SelectField}
|
||||
config={{
|
||||
label: i18n.METHOD_LABEL,
|
||||
defaultValue: 'post',
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.METHOD_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'webhookMethodSelect',
|
||||
options: HTTP_VERBS.map((verb) => ({ text: verb.toUpperCase(), value: verb })),
|
||||
fullWidth: true,
|
||||
readOnly,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="url"
|
||||
fullWidth
|
||||
error={errors.url}
|
||||
isInvalid={isUrlInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.urlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'URL',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
name="url"
|
||||
isInvalid={isUrlInvalid}
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
value={url || ''}
|
||||
data-test-subj="webhookUrlText"
|
||||
onChange={(e) => {
|
||||
editActionConfig('url', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!url) {
|
||||
editActionConfig('url', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path="config.url"
|
||||
config={{
|
||||
label: i18n.URL_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: urlField(i18n.URL_INVALID),
|
||||
},
|
||||
],
|
||||
}}
|
||||
component={Field}
|
||||
componentProps={{
|
||||
euiFieldProps: { readOnly, 'data-test-subj': 'webhookUrlText', fullWidth: true },
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
|
@ -298,140 +105,115 @@ const WebhookActionConnectorFields: React.FunctionComponent<
|
|||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.hasAuthSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Require authentication for this webhook',
|
||||
}
|
||||
)}
|
||||
disabled={readOnly}
|
||||
checked={hasAuth}
|
||||
onChange={(e) => {
|
||||
editActionConfig('hasAuth', e.target.checked);
|
||||
if (!e.target.checked) {
|
||||
editActionSecrets('user', null);
|
||||
editActionSecrets('password', null);
|
||||
}
|
||||
<UseField
|
||||
path="config.hasAuth"
|
||||
component={ToggleField}
|
||||
config={{ defaultValue: true }}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
label: i18n.HAS_AUTH_LABEL,
|
||||
disabled: readOnly,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{hasAuth ? (
|
||||
<>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
2,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.reenterValuesLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Username and password are encrypted. Please reenter values for these fields.',
|
||||
}
|
||||
)
|
||||
)}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="webhookUser"
|
||||
fullWidth
|
||||
error={errors.user}
|
||||
isInvalid={isUserInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel',
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path="secrets.user"
|
||||
config={{
|
||||
label: i18n.USERNAME_LABEL,
|
||||
validations: [
|
||||
{
|
||||
defaultMessage: 'Username',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isUserInvalid}
|
||||
name="user"
|
||||
readOnly={readOnly}
|
||||
value={user || ''}
|
||||
data-test-subj="webhookUserInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('user', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!user) {
|
||||
editActionSecrets('user', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="webhookPassword"
|
||||
fullWidth
|
||||
error={errors.password}
|
||||
isInvalid={isPasswordInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Password',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
name="password"
|
||||
readOnly={readOnly}
|
||||
isInvalid={isPasswordInvalid}
|
||||
value={password || ''}
|
||||
data-test-subj="webhookPasswordInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('password', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!password) {
|
||||
editActionSecrets('password', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
validator: emptyField(i18n.USERNAME_REQUIRED),
|
||||
},
|
||||
],
|
||||
}}
|
||||
component={Field}
|
||||
componentProps={{
|
||||
euiFieldProps: { readOnly, 'data-test-subj': 'webhookUserInput', fullWidth: true },
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PasswordField
|
||||
path="secrets.password"
|
||||
label={i18n.PASSWORD_LABEL}
|
||||
readOnly={readOnly}
|
||||
data-test-subj="webhookPasswordInput"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSwitch
|
||||
data-test-subj="webhookViewHeadersSwitch"
|
||||
disabled={readOnly}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch',
|
||||
{
|
||||
defaultMessage: 'Add HTTP header',
|
||||
}
|
||||
)}
|
||||
checked={hasHeaders}
|
||||
onChange={() => viewHeaders()}
|
||||
<UseField
|
||||
path="__internal__.hasHeaders"
|
||||
component={ToggleField}
|
||||
config={{ defaultValue: hasHeadersDefaultValue, label: i18n.ADD_HEADERS_LABEL }}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
disabled: readOnly,
|
||||
'data-test-subj': 'webhookViewHeadersSwitch',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<div>
|
||||
{Object.keys(headers || {}).length > 0 ? (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="xxs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
defaultMessage="Headers in use"
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.httpHeadersTitle"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
{headersList}
|
||||
</>
|
||||
) : null}
|
||||
<EuiSpacer size="m" />
|
||||
{hasHeaders && headerControl}
|
||||
<EuiSpacer size="m" />
|
||||
</div>
|
||||
{hasHeaders ? (
|
||||
<UseArray path="config.headers" initialNumberOfItems={1}>
|
||||
{({ items, addItem, removeItem }) => {
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<EuiFlexGroup key={item.id}>
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path={`${item.path}.key`}
|
||||
config={{
|
||||
label: i18n.HEADER_KEY_LABEL,
|
||||
}}
|
||||
component={TextField}
|
||||
// This is needed because when you delete
|
||||
// a row and add a new one, the stale values will appear
|
||||
readDefaultValueOnForm={!item.isNew}
|
||||
componentProps={{
|
||||
euiFieldProps: { readOnly },
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path={`${item.path}.value`}
|
||||
config={{ label: i18n.HEADER_VALUE_LABEL }}
|
||||
component={TextField}
|
||||
readDefaultValueOnForm={!item.isNew}
|
||||
componentProps={{
|
||||
euiFieldProps: { readOnly },
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
onClick={() => removeItem(item.id)}
|
||||
iconType="minusInCircle"
|
||||
aria-label={i18n.REMOVE_ITEM_LABEL}
|
||||
style={{ marginTop: '28px' }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
))}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButtonEmpty iconType="plusInCircle" onClick={addItem}>
|
||||
{i18n.ADD_HEADER_BTN}
|
||||
</EuiButtonEmpty>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</UseArray>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,55 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const BASIC_AUTH_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsLabel',
|
||||
{
|
||||
defaultMessage: 'Select the authentication method used when setting up the xMatters trigger.',
|
||||
}
|
||||
);
|
||||
|
||||
export const BASIC_AUTH_BUTTON_GROUP_LEGEND = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthButtonGroupLegend',
|
||||
{
|
||||
defaultMessage: 'Basic Authentication',
|
||||
}
|
||||
);
|
||||
|
||||
export const URL_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.urlLabel',
|
||||
{
|
||||
defaultMessage: 'Initiation URL',
|
||||
}
|
||||
);
|
||||
|
||||
export const USERNAME_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.userTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Username',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.passwordTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Password',
|
||||
}
|
||||
);
|
||||
|
||||
export const BASIC_AUTH_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthLabel',
|
||||
{
|
||||
defaultMessage: 'Basic Authentication',
|
||||
}
|
||||
);
|
||||
|
||||
export const URL_AUTH_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.urlAuthLabel',
|
||||
{
|
||||
defaultMessage: 'URL Authentication',
|
||||
}
|
||||
);
|
||||
|
||||
export const URL_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.requiredUrlText',
|
||||
{
|
||||
|
@ -21,30 +70,9 @@ export const URL_INVALID = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const USERNAME_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthUserNameText',
|
||||
export const USERNAME_INVALID = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.invalidUsernameTextField',
|
||||
{
|
||||
defaultMessage: 'Username is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthPasswordText',
|
||||
{
|
||||
defaultMessage: 'Password is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PASSWORD_REQUIRED_FOR_USER = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredPasswordText',
|
||||
{
|
||||
defaultMessage: 'Password is required when username is used.',
|
||||
}
|
||||
);
|
||||
|
||||
export const USERNAME_REQUIRED_FOR_PASSWORD = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredUserText',
|
||||
{
|
||||
defaultMessage: 'Username is required when password is used.',
|
||||
defaultMessage: 'Username is invalid.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '..';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { XmattersActionConnector } from '../types';
|
||||
import { registrationServicesMock } from '../../../../mocks';
|
||||
|
||||
const ACTION_TYPE_ID = '.xmatters';
|
||||
|
@ -30,138 +29,6 @@ describe('actionTypeRegistry.get() works', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('xmatters connector validation', () => {
|
||||
test('connector validation succeeds when usesBasic is true and connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.xmatters',
|
||||
name: 'xmatters',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
configUrl: 'http://test.com',
|
||||
usesBasic: true,
|
||||
},
|
||||
} as XmattersActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
configUrl: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
secretsUrl: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation succeeds when usesBasic is false and connector config is valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: '',
|
||||
password: '',
|
||||
secretsUrl: 'https://test.com?apiKey=someKey',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.xmatters',
|
||||
name: 'xmatters',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
config: {
|
||||
usesBasic: false,
|
||||
},
|
||||
} as XmattersActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
configUrl: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
secretsUrl: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.xmatters',
|
||||
name: 'xmatters',
|
||||
config: {
|
||||
usesBasic: true,
|
||||
},
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
} as XmattersActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
configUrl: ['URL is required.'],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: ['Password is required when username is used.'],
|
||||
secretsUrl: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when url in config is not valid', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.xmatters',
|
||||
name: 'xmatters',
|
||||
config: {
|
||||
configUrl: 'invalid.url',
|
||||
usesBasic: true,
|
||||
},
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
} as XmattersActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
configUrl: ['URL is invalid.'],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
secretsUrl: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('xmatters action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
|
|
|
@ -7,18 +7,8 @@
|
|||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ActionTypeModel,
|
||||
GenericValidationResult,
|
||||
ConnectorValidationResult,
|
||||
} from '../../../../types';
|
||||
import {
|
||||
XmattersActionParams,
|
||||
XmattersConfig,
|
||||
XmattersSecrets,
|
||||
XmattersActionConnector,
|
||||
} from '../types';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
import { ActionTypeModel, GenericValidationResult } from '../../../../types';
|
||||
import { XmattersActionParams, XmattersConfig, XmattersSecrets } from '../types';
|
||||
|
||||
export function getActionType(): ActionTypeModel<
|
||||
XmattersConfig,
|
||||
|
@ -40,48 +30,6 @@ export function getActionType(): ActionTypeModel<
|
|||
defaultMessage: 'xMatters data',
|
||||
}
|
||||
),
|
||||
validateConnector: async (
|
||||
action: XmattersActionConnector
|
||||
): Promise<ConnectorValidationResult<Pick<XmattersConfig, 'configUrl'>, XmattersSecrets>> => {
|
||||
const translations = await import('./translations');
|
||||
const configErrors = {
|
||||
configUrl: new Array<string>(),
|
||||
};
|
||||
const secretsErrors = {
|
||||
user: new Array<string>(),
|
||||
password: new Array<string>(),
|
||||
secretsUrl: new Array<string>(),
|
||||
};
|
||||
const validationResult = {
|
||||
config: { errors: configErrors },
|
||||
secrets: { errors: secretsErrors },
|
||||
};
|
||||
// basic auth validation
|
||||
if (!action.config.configUrl && action.config.usesBasic) {
|
||||
configErrors.configUrl.push(translations.URL_REQUIRED);
|
||||
}
|
||||
if (action.config.usesBasic && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.user.push(translations.USERNAME_REQUIRED);
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED);
|
||||
}
|
||||
if (action.config.configUrl && !isValidUrl(action.config.configUrl)) {
|
||||
configErrors.configUrl = [...configErrors.configUrl, translations.URL_INVALID];
|
||||
}
|
||||
if (action.config.usesBasic && action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED_FOR_USER);
|
||||
}
|
||||
if (action.config.usesBasic && !action.secrets.user && action.secrets.password) {
|
||||
secretsErrors.user.push(translations.USERNAME_REQUIRED_FOR_PASSWORD);
|
||||
}
|
||||
// API Key auth validation
|
||||
if (!action.config.usesBasic && !action.secrets.secretsUrl) {
|
||||
secretsErrors.secretsUrl.push(translations.URL_REQUIRED);
|
||||
}
|
||||
if (action.secrets.secretsUrl && !isValidUrl(action.secrets.secretsUrl)) {
|
||||
secretsErrors.secretsUrl.push(translations.URL_INVALID);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: async (
|
||||
actionParams: XmattersActionParams
|
||||
): Promise<
|
||||
|
|
|
@ -7,70 +7,72 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { XmattersActionConnector } from '../types';
|
||||
import XmattersActionConnectorFields from './xmatters_connectors';
|
||||
import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../test_utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
describe('XmattersActionConnectorFields renders', () => {
|
||||
test('all connector fields is rendered', () => {
|
||||
test('all connector fields is rendered', async () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.xmatters',
|
||||
name: 'xmatters',
|
||||
config: {
|
||||
configUrl: 'https://test.com',
|
||||
usesBasic: true,
|
||||
},
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.xmatters',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'xmatters',
|
||||
config: {
|
||||
configUrl: 'http:\\test',
|
||||
usesBasic: true,
|
||||
},
|
||||
} as XmattersActionConnector;
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<XmattersActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ url: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<XmattersActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="xmattersUrlText"]').length > 0).toBeTruthy();
|
||||
|
||||
await waitForComponentToUpdate();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="config.configUrl"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="xmattersUserInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="xmattersPasswordInput"]').length > 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should show only basic auth info when basic selected', () => {
|
||||
const actionConnector = {
|
||||
id: 'test',
|
||||
actionTypeId: '.xmatters',
|
||||
name: 'xmatters',
|
||||
config: {
|
||||
configUrl: 'https://test.com',
|
||||
usesBasic: true,
|
||||
},
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.xmatters',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'xmatters',
|
||||
config: {
|
||||
configUrl: 'http:\\test',
|
||||
usesBasic: true,
|
||||
},
|
||||
} as XmattersActionConnector;
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<XmattersActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ url: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<XmattersActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="xmattersUrlText"]').length > 0).toBeTruthy();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="config.configUrl"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="xmattersUserInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="xmattersPasswordInput"]').length > 0).toBeTruthy();
|
||||
});
|
||||
|
@ -78,7 +80,7 @@ describe('XmattersActionConnectorFields renders', () => {
|
|||
test('should show only url auth info when url selected', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
secretsUrl: 'http:\\test',
|
||||
secretsUrl: 'https://test.com',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.xmatters',
|
||||
|
@ -88,136 +90,175 @@ describe('XmattersActionConnectorFields renders', () => {
|
|||
config: {
|
||||
usesBasic: false,
|
||||
},
|
||||
} as XmattersActionConnector;
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<XmattersActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ url: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<XmattersActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="xmattersUrlText"]').length > 0).toBeTruthy();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="secrets.secretsUrl"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="xmattersUserInput"]').length === 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="xmattersPasswordInput"]').length === 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display a message on create to remember credentials', () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
const basicAuthConnector = {
|
||||
actionTypeId: '.xmatters',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'xmatters',
|
||||
config: {
|
||||
configUrl: 'https://test.com',
|
||||
usesBasic: true,
|
||||
},
|
||||
} as XmattersActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<XmattersActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ url: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('should display a message on edit to re-enter credentials, Basic Auth', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.xmatters',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'xmatters',
|
||||
config: {
|
||||
configUrl: 'http:\\test',
|
||||
usesBasic: true,
|
||||
},
|
||||
} as XmattersActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<XmattersActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ url: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
};
|
||||
|
||||
test('should display a message for missing secrets after import', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.xmatters',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: true,
|
||||
name: 'xmatters',
|
||||
config: {
|
||||
configUrl: 'http:\\test',
|
||||
usesBasic: true,
|
||||
},
|
||||
} as XmattersActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<XmattersActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ url: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should display a message on edit to re-enter credentials, URL Auth', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
secretsUrl: 'http:\\test?apiKey=someKey',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.xmatters',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
name: 'xmatters',
|
||||
const urlAuthConnector = {
|
||||
...basicAuthConnector,
|
||||
config: {
|
||||
usesBasic: false,
|
||||
},
|
||||
} as XmattersActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<XmattersActionConnectorFields
|
||||
action={actionConnector}
|
||||
errors={{ url: [], user: [], password: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
setCallbacks={() => {}}
|
||||
isEdit={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
secrets: {
|
||||
secretsUrl: 'https://test.com',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const basicAuthTests: Array<[string, string]> = [
|
||||
['config.configUrl', 'not-valid'],
|
||||
['xmattersUserInput', ''],
|
||||
['xmattersPasswordInput', ''],
|
||||
];
|
||||
|
||||
const urlAuthTests: Array<[string, string]> = [['secrets.secretsUrl', 'not-valid']];
|
||||
|
||||
it('connector validation succeeds when connector config is valid and uses basic auth', async () => {
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={basicAuthConnector} onSubmit={onSubmit}>
|
||||
<XmattersActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.xmatters',
|
||||
name: 'xmatters',
|
||||
config: {
|
||||
configUrl: 'https://test.com',
|
||||
usesBasic: true,
|
||||
},
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
__internal__: {
|
||||
auth: 'Basic Authentication',
|
||||
},
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('connector validation succeeds when connector config is valid and uses url auth', async () => {
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={urlAuthConnector} onSubmit={onSubmit}>
|
||||
<XmattersActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.xmatters',
|
||||
name: 'xmatters',
|
||||
config: {
|
||||
usesBasic: false,
|
||||
},
|
||||
secrets: {
|
||||
secretsUrl: 'https://test.com',
|
||||
},
|
||||
__internal__: { auth: 'URL Authentication' },
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(basicAuthTests)('validates correctly %p', async (field, value) => {
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={basicAuthConnector} onSubmit={onSubmit}>
|
||||
<XmattersActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
|
||||
it.each(urlAuthTests)('validates correctly %p', async (field, value) => {
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={urlAuthConnector} onSubmit={onSubmit}>
|
||||
<XmattersActionConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,78 +5,94 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import {
|
||||
EuiFieldPassword,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiButtonGroup,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
UseField,
|
||||
useFormContext,
|
||||
useFormData,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { XmattersActionConnector, XmattersAuthenticationType } from '../types';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label';
|
||||
import { XmattersAuthenticationType } from '../types';
|
||||
import { ButtonGroupField } from '../../button_group_field';
|
||||
import * as i18n from './translations';
|
||||
import { PasswordField } from '../../password_field';
|
||||
import { HiddenField } from '../../hidden_field';
|
||||
|
||||
const XmattersActionConnectorFields: React.FunctionComponent<
|
||||
ActionConnectorFieldsProps<XmattersActionConnector>
|
||||
> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => {
|
||||
const { user, password, secretsUrl } = action.secrets;
|
||||
const { configUrl, usesBasic } = action.config;
|
||||
const { emptyField, urlField } = fieldValidators;
|
||||
|
||||
const isBasicAuth = (auth: { auth: string } | null | undefined) => {
|
||||
if (auth == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return auth.auth === XmattersAuthenticationType.Basic ? true : false;
|
||||
};
|
||||
|
||||
const authenticationButtons = [
|
||||
{
|
||||
id: XmattersAuthenticationType.Basic,
|
||||
label: i18n.BASIC_AUTH_BUTTON_LABEL,
|
||||
},
|
||||
{
|
||||
id: XmattersAuthenticationType.URL,
|
||||
label: i18n.URL_AUTH_BUTTON_LABEL,
|
||||
},
|
||||
];
|
||||
|
||||
const XmattersUrlField: React.FC<{ path: string; readOnly: boolean }> = ({ path, readOnly }) => {
|
||||
return (
|
||||
<UseField
|
||||
path={path}
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.URL_LABEL,
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.initiationUrlHelpText"
|
||||
defaultMessage="Include the full xMatters url."
|
||||
/>
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: urlField(i18n.URL_INVALID),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: { 'data-test-subj': path, readOnly },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const XmattersActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
readOnly,
|
||||
}) => {
|
||||
const { setFieldValue, getFieldDefaultValue } = useFormContext();
|
||||
const [{ config, __internal__ }] = useFormData({
|
||||
watch: ['config.usesBasic', '__internal__.auth'],
|
||||
});
|
||||
|
||||
const usesBasicDefaultValue =
|
||||
getFieldDefaultValue<boolean | undefined>('config.usesBasic') ?? true;
|
||||
|
||||
const selectedAuthDefaultValue = usesBasicDefaultValue
|
||||
? XmattersAuthenticationType.Basic
|
||||
: XmattersAuthenticationType.URL;
|
||||
|
||||
const selectedAuth =
|
||||
config != null && !config.usesBasic
|
||||
? XmattersAuthenticationType.URL
|
||||
: XmattersAuthenticationType.Basic;
|
||||
|
||||
useEffect(() => {
|
||||
if (!action.id) {
|
||||
editActionConfig('usesBasic', true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const isUrlInvalid: boolean = usesBasic
|
||||
? errors.configUrl !== undefined && errors.configUrl.length > 0 && configUrl !== undefined
|
||||
: errors.secretsUrl !== undefined && errors.secretsUrl.length > 0 && secretsUrl !== 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;
|
||||
|
||||
const authenticationButtons = [
|
||||
{
|
||||
id: XmattersAuthenticationType.Basic,
|
||||
label: i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthLabel',
|
||||
{
|
||||
defaultMessage: 'Basic Authentication',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
id: XmattersAuthenticationType.URL,
|
||||
label: i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.urlAuthLabel',
|
||||
{
|
||||
defaultMessage: 'URL Authentication',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
let initialState;
|
||||
if (typeof usesBasic === 'undefined') {
|
||||
initialState = XmattersAuthenticationType.Basic;
|
||||
} else {
|
||||
initialState = usesBasic ? XmattersAuthenticationType.Basic : XmattersAuthenticationType.URL;
|
||||
if (usesBasic) {
|
||||
editActionSecrets('secretsUrl', '');
|
||||
} else {
|
||||
editActionConfig('configUrl', '');
|
||||
}
|
||||
}
|
||||
const [selectedAuth, setSelectedAuth] = useState(initialState);
|
||||
setFieldValue('config.usesBasic', isBasicAuth(__internal__));
|
||||
}, [__internal__, setFieldValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -89,92 +105,29 @@ const XmattersActionConnectorFields: React.FunctionComponent<
|
|||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFormRow fullWidth>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsLabel"
|
||||
defaultMessage="Select the authentication method used when setting up the xMatters trigger."
|
||||
/>
|
||||
</p>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
buttonSize="m"
|
||||
legend="Basic Authentication"
|
||||
<ButtonGroupField
|
||||
defaultValue={selectedAuthDefaultValue}
|
||||
path={'__internal__.auth'}
|
||||
label={i18n.BASIC_AUTH_LABEL}
|
||||
legend={i18n.BASIC_AUTH_BUTTON_GROUP_LEGEND}
|
||||
options={authenticationButtons}
|
||||
color="primary"
|
||||
idSelected={selectedAuth}
|
||||
onChange={(id: string) => {
|
||||
if (id === XmattersAuthenticationType.Basic) {
|
||||
setSelectedAuth(XmattersAuthenticationType.Basic);
|
||||
editActionConfig('usesBasic', true);
|
||||
editActionSecrets('secretsUrl', '');
|
||||
} else {
|
||||
setSelectedAuth(XmattersAuthenticationType.URL);
|
||||
editActionConfig('usesBasic', false);
|
||||
editActionConfig('configUrl', '');
|
||||
editActionSecrets('user', '');
|
||||
editActionSecrets('password', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<HiddenField path={'config.usesBasic'} config={{ defaultValue: true }} />
|
||||
<EuiSpacer size="m" />
|
||||
{selectedAuth === XmattersAuthenticationType.URL ? (
|
||||
<>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
1,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterUrlAuthValuesLabel',
|
||||
{
|
||||
defaultMessage: 'URL is encrypted. Please reenter values for this field.',
|
||||
}
|
||||
)
|
||||
)}
|
||||
</>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<XmattersUrlField path="secrets.secretsUrl" readOnly={readOnly} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="url"
|
||||
fullWidth
|
||||
error={usesBasic ? errors.configUrl : errors.secretsUrl}
|
||||
isInvalid={isUrlInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Initiation URL',
|
||||
}
|
||||
)}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.initiationUrlHelpText"
|
||||
defaultMessage="Include the full xMatters url."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
name="url"
|
||||
isInvalid={isUrlInvalid}
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
value={usesBasic ? configUrl : secretsUrl}
|
||||
data-test-subj="xmattersUrlText"
|
||||
onChange={(e) => {
|
||||
if (selectedAuth === XmattersAuthenticationType.Basic) {
|
||||
editActionConfig('configUrl', e.target.value);
|
||||
} else {
|
||||
editActionSecrets('secretsUrl', e.target.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{selectedAuth === XmattersAuthenticationType.Basic ? (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<XmattersUrlField path="config.configUrl" readOnly={readOnly} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="xxs">
|
||||
<h4>
|
||||
|
@ -186,83 +139,38 @@ const XmattersActionConnectorFields: React.FunctionComponent<
|
|||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
2,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterBasicAuthValuesLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'User and password are encrypted. Please reenter values for these fields.',
|
||||
}
|
||||
)
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="xmattersUser"
|
||||
fullWidth
|
||||
error={errors.user}
|
||||
isInvalid={isUserInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.userTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Username',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isUserInvalid}
|
||||
name="user"
|
||||
readOnly={readOnly}
|
||||
value={user || ''}
|
||||
data-test-subj="xmattersUserInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('user', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!user) {
|
||||
editActionSecrets('user', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<UseField
|
||||
path="secrets.user"
|
||||
component={TextField}
|
||||
config={{
|
||||
label: i18n.USERNAME_LABEL,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.USERNAME_INVALID),
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
disabled: readOnly,
|
||||
'data-test-subj': 'xmattersUserInput',
|
||||
readOnly,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="xmattersPassword"
|
||||
fullWidth
|
||||
error={errors.password}
|
||||
isInvalid={isPasswordInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.passwordTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Password',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
name="password"
|
||||
readOnly={readOnly}
|
||||
isInvalid={isPasswordInvalid}
|
||||
value={password || ''}
|
||||
data-test-subj="xmattersPasswordInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('password', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!password) {
|
||||
editActionSecrets('password', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<PasswordField
|
||||
path="secrets.password"
|
||||
label={i18n.PASSWORD_LABEL}
|
||||
readOnly={readOnly}
|
||||
data-test-subj="xmattersPasswordInput"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, ReactNode } from 'react';
|
||||
import { EuiButtonGroup, EuiFormRow } from '@elastic/eui';
|
||||
import {
|
||||
getFieldValidityAndErrorMessage,
|
||||
UseField,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const { emptyField } = fieldValidators;
|
||||
|
||||
const getFieldConfig = ({ label, defaultValue }: { label: string; defaultValue?: string }) => ({
|
||||
label,
|
||||
defaultValue,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate('xpack.triggersActionsUI.components.buttonGroupField.error.requiredField', {
|
||||
values: { label },
|
||||
defaultMessage: '{label} is required.',
|
||||
})
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
label: string;
|
||||
defaultValue?: string;
|
||||
helpText?: string | ReactNode;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const ButtonGroupFieldComponent: React.FC<Props> = ({
|
||||
path,
|
||||
label,
|
||||
helpText,
|
||||
defaultValue,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<UseField<string> path={path} config={getFieldConfig({ label, defaultValue })}>
|
||||
{(field) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
helpText={helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
buttonSize="m"
|
||||
legend="Select"
|
||||
color="primary"
|
||||
options={[]}
|
||||
idSelected={field.value}
|
||||
onChange={field.setValue}
|
||||
{...rest}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
};
|
||||
|
||||
export const ButtonGroupField = memo(ButtonGroupFieldComponent);
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* 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 { getEncryptedFieldNotifyLabel } from './get_encrypted_field_notify_label';
|
||||
|
||||
describe('getEncryptedFieldNotifyLabel', () => {
|
||||
test('renders proper notify label when isCreate equals true', () => {
|
||||
const jsxObject = getEncryptedFieldNotifyLabel(true, 2, false, 'test');
|
||||
|
||||
expect(
|
||||
jsxObject.props.children.filter(
|
||||
(child: any) => child.props['data-test-subj'] === 'rememberValuesMessage'
|
||||
).length
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
jsxObject.props.children.filter(
|
||||
(child: any) => child.props['data-test-subj'] === 'missingSecretsMessage'
|
||||
).length
|
||||
).toBe(0);
|
||||
expect(
|
||||
jsxObject.props.children.filter(
|
||||
(child: any) => child.props['data-test-subj'] === 'reenterValuesMessage'
|
||||
).length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('renders proper notify label when secrets is missing', () => {
|
||||
const jsxObject = getEncryptedFieldNotifyLabel(false, 2, true, 'test');
|
||||
|
||||
expect(
|
||||
jsxObject.props.children.filter(
|
||||
(child: any) => child.props['data-test-subj'] === 'rememberValuesMessage'
|
||||
).length
|
||||
).toBe(0);
|
||||
expect(
|
||||
jsxObject.props.children.filter(
|
||||
(child: any) => child.props['data-test-subj'] === 'missingSecretsMessage'
|
||||
).length
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
jsxObject.props.children.filter(
|
||||
(child: any) => child.props['data-test-subj'] === 'reenterValuesMessage'
|
||||
).length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('renders proper notify label when isCreate false (edit mode) and isMissingSecrets false', () => {
|
||||
const jsxObject = getEncryptedFieldNotifyLabel(false, 2, false, 'test');
|
||||
|
||||
expect(
|
||||
jsxObject.props.children.filter(
|
||||
(child: any) => child.props['data-test-subj'] === 'rememberValuesMessage'
|
||||
).length
|
||||
).toBe(0);
|
||||
expect(
|
||||
jsxObject.props.children.filter(
|
||||
(child: any) => child.props['data-test-subj'] === 'missingSecretsMessage'
|
||||
).length
|
||||
).toBe(0);
|
||||
expect(
|
||||
jsxObject.props.children.filter(
|
||||
(child: any) => child.props['data-test-subj'] === 'reenterValuesMessage'
|
||||
).length
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiSpacer, EuiCallOut, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
export const getEncryptedFieldNotifyLabel = (
|
||||
isCreate: boolean,
|
||||
encryptedFieldsLength: number,
|
||||
isMissingSecrets: boolean,
|
||||
reEnterDefaultMessage: string
|
||||
) => {
|
||||
if (isMissingSecrets) {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
data-test-subj="missingSecretsMessage"
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.missingSecretsValuesLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Sensitive information is not imported. Please enter value{encryptedFieldsLength, plural, one {} other {s}} for the following field{encryptedFieldsLength, plural, one {} other {s}}.',
|
||||
values: { encryptedFieldsLength },
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (isCreate) {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s" data-test-subj="rememberValuesMessage">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.rememberValueLabel"
|
||||
defaultMessage="Remember {encryptedFieldsLength, plural, one {this} other {these}} value. You must reenter {encryptedFieldsLength, plural, one {it} other {them}} each time you edit the connector."
|
||||
values={{ encryptedFieldsLength }}
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
iconType="iInCircle"
|
||||
data-test-subj="reenterValuesMessage"
|
||||
title={reEnterDefaultMessage}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { UseField, UseFieldProps } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
|
||||
const HiddenFieldComponent = <T,>(props: UseFieldProps<T>) => {
|
||||
return (
|
||||
<UseField<T> {...props}>
|
||||
{(field) => {
|
||||
/**
|
||||
* This is a hidden field. We return null so we do not render
|
||||
* any field on the form
|
||||
*/
|
||||
return null;
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
};
|
||||
|
||||
export const HiddenField = memo(HiddenFieldComponent);
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, ReactNode } from 'react';
|
||||
import { EuiFieldPassword, EuiFormRow } from '@elastic/eui';
|
||||
import {
|
||||
getFieldValidityAndErrorMessage,
|
||||
UseField,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const { emptyField } = fieldValidators;
|
||||
|
||||
const getFieldConfig = ({ label, validate }: { label: string; validate: boolean }) => ({
|
||||
label,
|
||||
validations: [
|
||||
...(validate
|
||||
? [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.passwordField.error.requiredNameText',
|
||||
{
|
||||
values: { label },
|
||||
defaultMessage: '{label} is required.',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
|
||||
interface PasswordFieldProps {
|
||||
path: string;
|
||||
label: string;
|
||||
helpText?: string | ReactNode;
|
||||
validate?: boolean;
|
||||
isLoading?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const PasswordFieldComponent: React.FC<PasswordFieldProps> = ({
|
||||
path,
|
||||
label,
|
||||
helpText,
|
||||
validate = true,
|
||||
isLoading,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<UseField<string> path={path} config={getFieldConfig({ label, validate })}>
|
||||
{(field) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
helpText={helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldPassword
|
||||
isInvalid={isInvalid}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
isLoading={field.isValidating || isLoading === true}
|
||||
fullWidth
|
||||
{...rest}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
};
|
||||
|
||||
export const PasswordField = memo(PasswordFieldComponent);
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act, render, RenderResult } from '@testing-library/react';
|
||||
import { FormTestProvider } from './builtin_action_types/test_utils';
|
||||
import {
|
||||
ConfigFieldSchema,
|
||||
SecretsFieldSchema,
|
||||
SimpleConnectorForm,
|
||||
} from './simple_connector_form';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const fillForm = async ({ getByTestId }: RenderResult) => {
|
||||
await act(async () => {
|
||||
await userEvent.type(getByTestId('config.url-input'), 'https://example.com', {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(getByTestId('config.test-config-input'), 'My text field', {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(getByTestId('secrets.username-input'), 'elastic', {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(getByTestId('secrets.password-input'), 'changeme', {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('SimpleConnectorForm', () => {
|
||||
const configFormSchema: ConfigFieldSchema[] = [
|
||||
{ id: 'url', label: 'Url', isUrlField: true },
|
||||
{ id: 'test-config', label: 'Test config', helpText: 'Test help text' },
|
||||
];
|
||||
const secretsFormSchema: SecretsFieldSchema[] = [
|
||||
{ id: 'username', label: 'Username' },
|
||||
{ id: 'password', label: 'Password', isPasswordField: true },
|
||||
];
|
||||
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const { getByText } = render(
|
||||
<FormTestProvider onSubmit={onSubmit}>
|
||||
<SimpleConnectorForm
|
||||
isEdit={true}
|
||||
readOnly={false}
|
||||
configFormSchema={configFormSchema}
|
||||
secretsFormSchema={secretsFormSchema}
|
||||
/>
|
||||
</FormTestProvider>
|
||||
);
|
||||
|
||||
expect(getByText('Url')).toBeInTheDocument();
|
||||
expect(getByText('Test config')).toBeInTheDocument();
|
||||
expect(getByText('Test help text')).toBeInTheDocument();
|
||||
|
||||
expect(getByText('Authentication')).toBeInTheDocument();
|
||||
expect(getByText('Username')).toBeInTheDocument();
|
||||
expect(getByText('Password')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('submits correctly', async () => {
|
||||
const res = render(
|
||||
<FormTestProvider onSubmit={onSubmit}>
|
||||
<SimpleConnectorForm
|
||||
isEdit={true}
|
||||
readOnly={false}
|
||||
configFormSchema={configFormSchema}
|
||||
secretsFormSchema={secretsFormSchema}
|
||||
/>
|
||||
</FormTestProvider>
|
||||
);
|
||||
|
||||
await fillForm(res);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
data: {
|
||||
config: {
|
||||
'test-config': 'My text field',
|
||||
url: 'https://example.com',
|
||||
},
|
||||
secrets: {
|
||||
password: 'changeme',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
const tests: Array<[string, string]> = [
|
||||
['config.url-input', 'not-valid'],
|
||||
['config.test-config-input', ''],
|
||||
['secrets.username-input', ''],
|
||||
['secrets.password-input', ''],
|
||||
];
|
||||
|
||||
it.each(tests)('validates correctly %p', async (field, value) => {
|
||||
const res = render(
|
||||
<FormTestProvider onSubmit={onSubmit}>
|
||||
<SimpleConnectorForm
|
||||
isEdit={true}
|
||||
readOnly={false}
|
||||
configFormSchema={configFormSchema}
|
||||
secretsFormSchema={secretsFormSchema}
|
||||
/>
|
||||
</FormTestProvider>
|
||||
);
|
||||
|
||||
await fillForm(res);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { getUseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PasswordField } from './password_field';
|
||||
|
||||
export interface CommonFieldSchema {
|
||||
id: string;
|
||||
label: string;
|
||||
helpText?: string;
|
||||
}
|
||||
|
||||
export interface ConfigFieldSchema extends CommonFieldSchema {
|
||||
isUrlField?: boolean;
|
||||
}
|
||||
|
||||
export interface SecretsFieldSchema extends CommonFieldSchema {
|
||||
isPasswordField?: boolean;
|
||||
}
|
||||
|
||||
interface SimpleConnectorFormProps {
|
||||
isEdit: boolean;
|
||||
readOnly: boolean;
|
||||
configFormSchema: ConfigFieldSchema[];
|
||||
secretsFormSchema: SecretsFieldSchema[];
|
||||
}
|
||||
|
||||
type FormRowProps = ConfigFieldSchema & SecretsFieldSchema & { readOnly: boolean };
|
||||
|
||||
const UseField = getUseField({ component: Field });
|
||||
const { emptyField, urlField } = fieldValidators;
|
||||
|
||||
const getFieldConfig = ({
|
||||
label,
|
||||
isUrlField = false,
|
||||
}: {
|
||||
label: string;
|
||||
isUrlField?: boolean;
|
||||
}) => ({
|
||||
label,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionConnectorForm.error.requireFieldText',
|
||||
{
|
||||
values: { label },
|
||||
defaultMessage: `{label} is required.`,
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
...(isUrlField
|
||||
? [
|
||||
{
|
||||
validator: urlField(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionConnectorForm.error.invalidURL',
|
||||
{
|
||||
defaultMessage: 'Invalid URL',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
|
||||
const FormRow: React.FC<FormRowProps> = ({
|
||||
id,
|
||||
label,
|
||||
readOnly,
|
||||
isPasswordField,
|
||||
isUrlField,
|
||||
helpText,
|
||||
}) => {
|
||||
const dataTestSub = `${id}-input`;
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
{!isPasswordField ? (
|
||||
<UseField
|
||||
path={id}
|
||||
config={getFieldConfig({ label, isUrlField })}
|
||||
helpText={helpText}
|
||||
componentProps={{
|
||||
euiFieldProps: { readOnly, fullWidth: true, 'data-test-subj': dataTestSub },
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<PasswordField
|
||||
path={id}
|
||||
label={label}
|
||||
readOnly={readOnly}
|
||||
data-test-subj={dataTestSub}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SimpleConnectorFormComponent: React.FC<SimpleConnectorFormProps> = ({
|
||||
isEdit,
|
||||
readOnly,
|
||||
configFormSchema,
|
||||
secretsFormSchema,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{configFormSchema.map(({ id, ...restConfigSchema }, index) => (
|
||||
<React.Fragment key={`config.${id}`}>
|
||||
<FormRow id={`config.${id}`} {...restConfigSchema} readOnly={readOnly} />
|
||||
{index !== configFormSchema.length ? <EuiSpacer size="m" /> : null}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xxs">
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.triggersActionsUI.components.simpleConnectorForm.secrets.authenticationLabel',
|
||||
{
|
||||
defaultMessage: 'Authentication',
|
||||
}
|
||||
)}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
{secretsFormSchema.map(({ id, ...restSecretsSchema }, index) => (
|
||||
<React.Fragment key={`secrets.${id}`}>
|
||||
<FormRow
|
||||
id={`secrets.${id}`}
|
||||
key={`secrets.${id}`}
|
||||
{...restSecretsSchema}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
{index !== secretsFormSchema.length ? <EuiSpacer size="m" /> : null}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SimpleConnectorForm = memo(SimpleConnectorFormComponent);
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue