From 0af40a3066e5b8ee76eeaf4d76b982ae5d5dbdca Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 31 Aug 2023 04:04:14 +0200 Subject: [PATCH] [RAM] Autocomplete (#158454) ## Summary Solves this issue: https://github.com/elastic/kibana/issues/161763 This PR introduces autocomplete for mustache variables for email connector(next PR will add it to all connectors) under the feature flag. We decided keep old solution with button with all searchable options as well. How to test: Create an email connector in kibana.yml: xpack.actions.preconfigured: maildev: name: 'email: maildev' actionTypeId: '.email' config: from: 'guskova@example.com' host: 'localhost' port: '1025' How it should work: You start writing in Message window {{ and mustache variable name. And you should see autocomplete popup with all possible options to choose. When you click somewhere else, popup should disappeared. https://github.com/elastic/kibana/assets/26089545/061016a6-b8ca-497b-9bed-b8b012d31a95 e options to choose. When you click somewhere else, popup should disappeared. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Xavier Mouligneau --- package.json | 2 + packages/kbn-alerts-ui-shared/index.ts | 1 + packages/kbn-alerts-ui-shared/jest.config.js | 1 + packages/kbn-alerts-ui-shared/setup_tests.ts | 10 + .../add_message_variables.scss | 0 .../src/add_message_variables/index.test.tsx | 7 +- .../src/add_message_variables/index.tsx | 7 +- .../src/add_message_variables/translations.ts | 23 +- .../add_message_variables/truncated_text.tsx | 34 ++ .../maintenance_window_callout/index.test.tsx | 9 +- .../test_suites/core_plugins/rendering.ts | 1 + .../common/experimental_features.ts | 52 +++ .../plugins/stack_connectors/common/types.ts | 10 + x-pack/plugins/stack_connectors/kibana.jsonc | 1 + .../common/experimental_features_service.ts | 30 ++ .../common/get_experimental_features.ts | 27 ++ .../text_area_with_autocomplete.tsx | 366 ++++++++++++++++++ .../email/email_params.test.tsx | 119 ++++-- .../connector_types/email/email_params.tsx | 14 +- .../plugins/stack_connectors/public/index.ts | 3 +- ...ilter_suggestions_for_autocomplete.test.ts | 77 ++++ .../filter_suggestions_for_autocomplete.ts | 27 ++ .../lib/template_action_variable.test.ts | 25 ++ .../public/lib/template_action_variable.ts | 14 + .../plugins/stack_connectors/public/plugin.ts | 16 +- .../plugins/stack_connectors/server/config.ts | 49 +++ .../plugins/stack_connectors/server/index.ts | 10 +- x-pack/plugins/stack_connectors/tsconfig.json | 3 + .../translations/translations/fr-FR.json | 9 - .../translations/translations/ja-JP.json | 9 - .../translations/translations/zh-CN.json | 9 - .../public/application/components/index.ts | 1 - .../json_editor_with_message_variables.tsx | 3 +- .../text_area_with_message_variables.tsx | 3 +- .../text_field_with_message_variables.tsx | 3 +- .../lib/validate_params_for_warnings.ts | 6 +- yarn.lock | 10 + 37 files changed, 890 insertions(+), 101 deletions(-) create mode 100644 packages/kbn-alerts-ui-shared/setup_tests.ts rename {x-pack/plugins/triggers_actions_ui/public/application/components => packages/kbn-alerts-ui-shared/src/add_message_variables}/add_message_variables.scss (100%) rename x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx => packages/kbn-alerts-ui-shared/src/add_message_variables/index.test.tsx (95%) rename x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx => packages/kbn-alerts-ui-shared/src/add_message_variables/index.tsx (96%) rename x-pack/plugins/triggers_actions_ui/public/application/components/translations.js => packages/kbn-alerts-ui-shared/src/add_message_variables/translations.ts (57%) create mode 100644 packages/kbn-alerts-ui-shared/src/add_message_variables/truncated_text.tsx create mode 100644 x-pack/plugins/stack_connectors/common/experimental_features.ts create mode 100644 x-pack/plugins/stack_connectors/common/types.ts create mode 100644 x-pack/plugins/stack_connectors/public/common/experimental_features_service.ts create mode 100644 x-pack/plugins/stack_connectors/public/common/get_experimental_features.ts create mode 100644 x-pack/plugins/stack_connectors/public/components/text_area_with_autocomplete.tsx create mode 100644 x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.test.ts create mode 100644 x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.ts create mode 100644 x-pack/plugins/stack_connectors/public/lib/template_action_variable.test.ts create mode 100644 x-pack/plugins/stack_connectors/public/lib/template_action_variable.ts create mode 100644 x-pack/plugins/stack_connectors/server/config.ts diff --git a/package.json b/package.json index 29f34767ba409..acfeec966f0a0 100644 --- a/package.json +++ b/package.json @@ -1007,6 +1007,7 @@ "suricata-sid-db": "^1.0.2", "symbol-observable": "^1.2.0", "tar": "^6.1.15", + "textarea-caret": "^3.1.0", "tinycolor2": "1.4.1", "tinygradient": "0.4.3", "ts-easing": "^0.2.0", @@ -1368,6 +1369,7 @@ "@types/tar": "^6.1.5", "@types/tempy": "^0.2.0", "@types/testing-library__jest-dom": "^5.14.7", + "@types/textarea-caret": "^3.0.1", "@types/tinycolor2": "^1.4.1", "@types/tough-cookie": "^4.0.2", "@types/type-detect": "^4.0.1", diff --git a/packages/kbn-alerts-ui-shared/index.ts b/packages/kbn-alerts-ui-shared/index.ts index 6815a66bce902..6daec0f82b108 100644 --- a/packages/kbn-alerts-ui-shared/index.ts +++ b/packages/kbn-alerts-ui-shared/index.ts @@ -9,3 +9,4 @@ export { AlertLifecycleStatusBadge } from './src/alert_lifecycle_status_badge'; export type { AlertLifecycleStatusBadgeProps } from './src/alert_lifecycle_status_badge'; export { MaintenanceWindowCallout } from './src/maintenance_window_callout'; +export { AddMessageVariables } from './src/add_message_variables'; diff --git a/packages/kbn-alerts-ui-shared/jest.config.js b/packages/kbn-alerts-ui-shared/jest.config.js index 31062b3280e41..54f2c74a56d3a 100644 --- a/packages/kbn-alerts-ui-shared/jest.config.js +++ b/packages/kbn-alerts-ui-shared/jest.config.js @@ -10,4 +10,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../..', roots: ['/packages/kbn-alerts-ui-shared'], + setupFilesAfterEnv: ['/packages/kbn-alerts-ui-shared/setup_tests.ts'], }; diff --git a/packages/kbn-alerts-ui-shared/setup_tests.ts b/packages/kbn-alerts-ui-shared/setup_tests.ts new file mode 100644 index 0000000000000..8d1acb9232934 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/setup_tests.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import '@testing-library/jest-dom'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss b/packages/kbn-alerts-ui-shared/src/add_message_variables/add_message_variables.scss similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.scss rename to packages/kbn-alerts-ui-shared/src/add_message_variables/add_message_variables.scss diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx b/packages/kbn-alerts-ui-shared/src/add_message_variables/index.test.tsx similarity index 95% rename from x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx rename to packages/kbn-alerts-ui-shared/src/add_message_variables/index.test.tsx index 641587c349382..80603d9eb8615 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/add_message_variables/index.test.tsx @@ -1,13 +1,14 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { render, fireEvent, screen } from '@testing-library/react'; -import { AddMessageVariables } from './add_message_variables'; +import { AddMessageVariables } from '.'; describe('AddMessageVariables', () => { test('it renders variables and filter bar', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/packages/kbn-alerts-ui-shared/src/add_message_variables/index.tsx similarity index 96% rename from x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx rename to packages/kbn-alerts-ui-shared/src/add_message_variables/index.tsx index f986930470acc..0aa9bae65c29e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/packages/kbn-alerts-ui-shared/src/add_message_variables/index.tsx @@ -1,8 +1,9 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { useMemo, useState } from 'react'; @@ -23,7 +24,7 @@ import { } from '@elastic/eui'; import { ActionVariable } from '@kbn/alerting-plugin/common'; import './add_message_variables.scss'; -import { TruncatedText } from '../../common/truncated_text'; +import { TruncatedText } from './truncated_text'; import * as i18n from './translations'; interface Props { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/translations.js b/packages/kbn-alerts-ui-shared/src/add_message_variables/translations.ts similarity index 57% rename from x-pack/plugins/triggers_actions_ui/public/application/components/translations.js rename to packages/kbn-alerts-ui-shared/src/add_message_variables/translations.ts index 0e089f1a830e8..b19e9173797a9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/translations.js +++ b/packages/kbn-alerts-ui-shared/src/add_message_variables/translations.ts @@ -1,70 +1,71 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; export const LOADING_VARIABLES = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.loadingMessage', + 'alertsUIShared.components.addMessageVariables.loadingMessage', { defaultMessage: 'Loading variables', } ); export const NO_VARIABLES_FOUND = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.noVariablesFound', + 'alertsUIShared.components.addMessageVariables.noVariablesFound', { defaultMessage: 'No variables found', } ); export const NO_VARIABLES_AVAILABLE = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.noVariablesAvailable', + 'alertsUIShared.components.addMessageVariables.noVariablesAvailable', { defaultMessage: 'No variables available', } ); export const DEPRECATED_VARIABLES_ARE_SHOWN = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreShown', + 'alertsUIShared.components.addMessageVariables.deprecatedVariablesAreShown', { defaultMessage: 'Deprecated variables are shown', } ); export const DEPRECATED_VARIABLES_ARE_HIDDEN = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreHidden', + 'alertsUIShared.components.addMessageVariables.deprecatedVariablesAreHidden', { defaultMessage: 'Deprecated variables are hidden', } ); export const HIDE = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.hideDeprecatedVariables', + 'alertsUIShared.components.addMessageVariables.hideDeprecatedVariables', { defaultMessage: 'Hide', } ); export const SHOW_ALL = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.showAllDeprecatedVariables', + 'alertsUIShared.components.addMessageVariables.showAllDeprecatedVariables', { defaultMessage: 'Show all', } ); export const ADD_VARIABLE_POPOVER_BUTTON = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton', + 'alertsUIShared.components.addMessageVariables.addVariablePopoverButton', { defaultMessage: 'Add variable', } ); export const ADD_VARIABLE_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.addRuleVariableTitle', + 'alertsUIShared.components.addMessageVariables.addRuleVariableTitle', { defaultMessage: 'Add variable', } diff --git a/packages/kbn-alerts-ui-shared/src/add_message_variables/truncated_text.tsx b/packages/kbn-alerts-ui-shared/src/add_message_variables/truncated_text.tsx new file mode 100644 index 0000000000000..52535ab435e68 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/add_message_variables/truncated_text.tsx @@ -0,0 +1,34 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { css } from '@emotion/react'; +import { EuiText } from '@elastic/eui'; + +const LINE_CLAMP = 2; + +const styles = { + truncatedText: css` + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: ${LINE_CLAMP}; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-word; + `, +}; + +const TruncatedTextComponent: React.FC<{ text: string }> = ({ text }) => ( + + {text} + +); + +TruncatedTextComponent.displayName = 'TruncatedText'; + +export const TruncatedText = React.memo(TruncatedTextComponent); diff --git a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx index b29f0ddc613c4..9a05a6bd09222 100644 --- a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx @@ -106,7 +106,6 @@ describe('MaintenanceWindowCallout', () => { { wrapper: TestProviders } ); - // @ts-expect-error Jest types are incomplete in packages expect(await findByText('Maintenance window is running')).toBeInTheDocument(); expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1); }); @@ -119,7 +118,7 @@ describe('MaintenanceWindowCallout', () => { const { container } = render(, { wrapper: TestProviders, }); - // @ts-expect-error Jest types are incomplete in packages + expect(container).toBeEmptyDOMElement(); expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1); }); @@ -130,7 +129,7 @@ describe('MaintenanceWindowCallout', () => { const { container } = render(, { wrapper: TestProviders, }); - // @ts-expect-error Jest types are incomplete in packages + expect(container).toBeEmptyDOMElement(); expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1); }); @@ -192,7 +191,7 @@ describe('MaintenanceWindowCallout', () => { const { container } = render(, { wrapper: TestProviders, }); - // @ts-expect-error Jest types are incomplete in packages + expect(container).toBeEmptyDOMElement(); }); @@ -213,7 +212,7 @@ describe('MaintenanceWindowCallout', () => { const { findByText } = render(, { wrapper: TestProviders, }); - // @ts-expect-error Jest types are incomplete in packages + expect(await findByText('Maintenance window is running')).toBeInTheDocument(); }); }); diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 03928a378f6f3..729baf5023f58 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -280,6 +280,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.securitySolution.prebuiltRulesPackageVersion (string)', 'xpack.snapshot_restore.slm_ui.enabled (boolean)', 'xpack.snapshot_restore.ui.enabled (boolean)', + 'xpack.stack_connectors.enableExperimental (array)', 'xpack.trigger_actions_ui.enableExperimental (array)', 'xpack.trigger_actions_ui.enableGeoTrackingThresholdAlert (boolean)', 'xpack.upgrade_assistant.featureSet.migrateSystemIndices (boolean)', diff --git a/x-pack/plugins/stack_connectors/common/experimental_features.ts b/x-pack/plugins/stack_connectors/common/experimental_features.ts new file mode 100644 index 0000000000000..61b63cff732a6 --- /dev/null +++ b/x-pack/plugins/stack_connectors/common/experimental_features.ts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +export type ExperimentalFeatures = typeof allowedExperimentalValues; + +/** + * A list of allowed values that can be used in `xpack.stack_connectors.enableExperimental`. + * This object is then used to validate and parse the value entered. + */ +export const allowedExperimentalValues = Object.freeze({ + isMustacheAutocompleteOn: false, +}); + +type ExperimentalConfigKeys = Array; +type Mutable = { -readonly [P in keyof T]: T[P] }; + +const InvalidExperimentalValue = class extends Error {}; +const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly; + +/** + * Parses the string value used in `xpack.stack_connectors.enableExperimental` kibana configuration, + * which should be a string of values delimited by a comma (`,`) + * + * @param configValue + * @throws InvalidExperimentalValue + */ +export const parseExperimentalConfigValue = (configValue: string[]): ExperimentalFeatures => { + const enabledFeatures: Mutable> = {}; + + for (const value of configValue) { + if (!isValidExperimentalValue(value)) { + throw new InvalidExperimentalValue(`[${value}] is not valid.`); + } + // @ts-expect-error ts upgrade v4.7.4 + enabledFeatures[value as keyof ExperimentalFeatures] = true; + } + + return { + ...allowedExperimentalValues, + ...enabledFeatures, + }; +}; + +export const isValidExperimentalValue = (value: string): boolean => { + return allowedKeys.includes(value as keyof ExperimentalFeatures); +}; + +export const getExperimentalAllowedValues = (): string[] => [...allowedKeys]; diff --git a/x-pack/plugins/stack_connectors/common/types.ts b/x-pack/plugins/stack_connectors/common/types.ts new file mode 100644 index 0000000000000..6bc9ef7eb72e6 --- /dev/null +++ b/x-pack/plugins/stack_connectors/common/types.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export interface StackConnectorsConfigType { + enableExperimental: string[]; +} diff --git a/x-pack/plugins/stack_connectors/kibana.jsonc b/x-pack/plugins/stack_connectors/kibana.jsonc index dc4023890d656..da8e973b6f990 100644 --- a/x-pack/plugins/stack_connectors/kibana.jsonc +++ b/x-pack/plugins/stack_connectors/kibana.jsonc @@ -13,6 +13,7 @@ "requiredPlugins": [ "actions", "esUiShared", + "kibanaReact", "triggersActionsUi" ], "extraPublicDirs": [ diff --git a/x-pack/plugins/stack_connectors/public/common/experimental_features_service.ts b/x-pack/plugins/stack_connectors/public/common/experimental_features_service.ts new file mode 100644 index 0000000000000..c701af1376dbb --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/common/experimental_features_service.ts @@ -0,0 +1,30 @@ +/* + * 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 { ExperimentalFeatures } from '../../common/experimental_features'; + +export class ExperimentalFeaturesService { + private static experimentalFeatures?: ExperimentalFeatures; + + public static init({ experimentalFeatures }: { experimentalFeatures: ExperimentalFeatures }) { + this.experimentalFeatures = experimentalFeatures; + } + + public static get(): ExperimentalFeatures { + if (!this.experimentalFeatures) { + this.throwUninitializedError(); + } + + return this.experimentalFeatures; + } + + private static throwUninitializedError(): never { + throw new Error( + 'Experimental features services not initialized - are you trying to import this module from outside of the stack connectors?' + ); + } +} diff --git a/x-pack/plugins/stack_connectors/public/common/get_experimental_features.ts b/x-pack/plugins/stack_connectors/public/common/get_experimental_features.ts new file mode 100644 index 0000000000000..41a321bbd2981 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/common/get_experimental_features.ts @@ -0,0 +1,27 @@ +/* + * 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 { + ExperimentalFeatures, + isValidExperimentalValue, + getExperimentalAllowedValues, +} from '../../common/experimental_features'; +import { ExperimentalFeaturesService } from './experimental_features_service'; + +const allowedExperimentalValueKeys = getExperimentalAllowedValues(); + +export const getIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => { + if (!isValidExperimentalValue(feature)) { + throw new Error( + `Invalid enable value ${feature}. Allowed values are: ${allowedExperimentalValueKeys.join( + ', ' + )}` + ); + } + + return ExperimentalFeaturesService.get()[feature]; +}; diff --git a/x-pack/plugins/stack_connectors/public/components/text_area_with_autocomplete.tsx b/x-pack/plugins/stack_connectors/public/components/text_area_with_autocomplete.tsx new file mode 100644 index 0000000000000..0f51b3ced86c6 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/components/text_area_with_autocomplete.tsx @@ -0,0 +1,366 @@ +/* + * 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, { useState, useMemo, useCallback, useEffect } from 'react'; +import getCaretCoordinates from 'textarea-caret'; +import { Properties } from 'csstype'; +import { + EuiTextArea, + EuiFormRow, + EuiSelectable, + EuiSelectableOption, + EuiPortal, + EuiHighlight, + EuiOutsideClickDetector, + useEuiTheme, + useEuiBackgroundColor, +} from '@elastic/eui'; +import { ActionVariable } from '@kbn/alerting-plugin/common'; +import { AddMessageVariables } from '@kbn/alerts-ui-shared'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { filterSuggestions } from '../lib/filter_suggestions_for_autocomplete'; +import { templateActionVariable } from '../lib/template_action_variable'; + +export interface TextAreaWithAutocompleteProps { + editAction: (property: string, value: any, index: number) => void; + errors?: string[]; + index: number; + inputTargetValue?: string; + isDisabled?: boolean; + label: string; + messageVariables?: ActionVariable[]; + paramsProperty: string; +} +const selectableListProps = { className: 'euiSelectableMsgAutoComplete' }; + +export const TextAreaWithAutocomplete: React.FunctionComponent = ({ + editAction, + errors, + index, + inputTargetValue, + isDisabled = false, + label, + messageVariables, + paramsProperty, +}) => { + const { euiTheme } = useEuiTheme(); + const backgroundColor = useEuiBackgroundColor('plain'); + + const textAreaRef = React.useRef(null); + const selectableRef = React.useRef(null); + + const [matches, setMatches] = useState([]); + const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0, height: 0, width: 0 }); + const [isListOpen, setListOpen] = useState(false); + const [autoCompleteIndex, setAutoCompleteIndex] = useState(-1); + const [selectableHasFocus, setSelectableHasFocus] = useState(false); + const [searchWord, setSearchWord] = useState(''); + + const optionsToShow: EuiSelectableOption[] = useMemo(() => { + return matches?.map((variable) => ({ + label: variable, + data: { + description: variable, + }, + 'data-test-subj': `${variable}-selectableOption`, + })); + }, [matches]); + + const closeList = useCallback((doNotResetAutoCompleteIndex = false) => { + if (!doNotResetAutoCompleteIndex) { + setAutoCompleteIndex(-1); + } + setListOpen(false); + setSelectableHasFocus(false); + }, []); + + const onOptionPick = useCallback( + (newOptions: EuiSelectableOption[]) => { + if (!textAreaRef.current) return; + const { value, selectionStart, scrollTop } = textAreaRef.current; + const lastSpaceIndex = value.slice(0, selectionStart).lastIndexOf(' '); + const lastOpenDoubleCurlyBracketsIndex = value.slice(0, selectionStart).lastIndexOf('{{'); + const currentWordStartIndex = Math.max(lastSpaceIndex, lastOpenDoubleCurlyBracketsIndex); + + const checkedElement = newOptions.find(({ checked }) => checked === 'on'); + if (checkedElement) { + const newInputText = + value.slice(0, currentWordStartIndex) + + '{{' + + checkedElement.label + + '}}' + + value.slice(selectionStart); + + editAction(paramsProperty, newInputText.trim(), index); + setMatches([]); + closeList(); + textAreaRef.current.focus(); + // We use setTimeout here, because editAction is async function and we need to wait before it executes + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.selectionStart = + currentWordStartIndex + checkedElement.label.length + 4; + textAreaRef.current.selectionEnd = textAreaRef.current.selectionStart; + textAreaRef.current.scrollTop = scrollTop; + } + }, 0); + } + }, + [editAction, index, paramsProperty, closeList] + ); + + const recalcMenuPosition = useCallback(() => { + if (!textAreaRef.current) return; + const newPosition = getCaretCoordinates( + textAreaRef.current, + textAreaRef.current.selectionStart + ); + const textAreaClientRect = textAreaRef.current?.getBoundingClientRect(); + + const top = + textAreaClientRect.top - + textAreaRef.current.scrollTop + + window.scrollY + + newPosition.top + + newPosition.height; + const left = textAreaClientRect.left + window.pageXOffset; + const height = newPosition.height; + const width = textAreaClientRect.width; + setPopupPosition({ top, left, width, height }); + setListOpen(true); + }, []); + + const onChangeWithMessageVariable = useCallback(() => { + if (!textAreaRef.current) return; + const { value, selectionStart } = textAreaRef.current; + const lastTwoLetter = value.slice(selectionStart - 2, selectionStart); + + const currentWord = + autoCompleteIndex !== -1 ? value.slice(autoCompleteIndex, selectionStart) : ''; + + if (lastTwoLetter === '{{' || currentWord.startsWith('{{')) { + if (lastTwoLetter === '{{') { + setAutoCompleteIndex(selectionStart - 2); + } + const filteredMatches = filterSuggestions({ + actionVariablesList: messageVariables + ?.filter(({ deprecated }) => !deprecated) + .map(({ name }) => name), + propertyPath: currentWord.slice(2), + }); + setSearchWord(currentWord.slice(2)); + setMatches(filteredMatches); + setTimeout(() => recalcMenuPosition(), 0); + } else if (lastTwoLetter === '}}') { + closeList(); + } else { + setMatches([]); + } + editAction(paramsProperty, value, index); + }, [ + autoCompleteIndex, + closeList, + editAction, + index, + messageVariables, + paramsProperty, + recalcMenuPosition, + ]); + + const textareaOnKeyPress = useCallback( + (event) => { + if (selectableRef.current && isListOpen) { + if (!selectableHasFocus && (event.code === 'ArrowUp' || event.code === 'ArrowDown')) { + event.preventDefault(); + event.stopPropagation(); + selectableRef.current.onFocus(); + setSelectableHasFocus(true); + } else if (event.code === 'ArrowUp') { + event.preventDefault(); + event.stopPropagation(); + selectableRef.current.incrementActiveOptionIndex(-1); + } else if (event.code === 'ArrowDown') { + event.preventDefault(); + event.stopPropagation(); + selectableRef.current.incrementActiveOptionIndex(1); + } else if (event.code === 'Escape') { + event.preventDefault(); + event.stopPropagation(); + closeList(); + } else if (event.code === 'Enter' || event.code === 'Space') { + const optionIndex = selectableRef.current.state.activeOptionIndex; + onOptionPick( + optionsToShow.map((ots, idx) => { + if (idx === optionIndex) { + return { + ...ots, + checked: 'on', + }; + } + return ots; + }) + ); + closeList(); + } + } else { + setSelectableHasFocus((prevValue) => { + if (prevValue) { + return false; + } + return prevValue; + }); + } + }, + [closeList, isListOpen, onOptionPick, optionsToShow, selectableHasFocus] + ); + + const clickOutSideTextArea = useCallback( + (event) => { + const box = document + .querySelector('.euiSelectableMsgAutoComplete') + ?.getBoundingClientRect() || { + left: 0, + right: 0, + top: 0, + bottom: 0, + }; + if ( + event.clientX > box.left && + event.clientX < box.right && + event.clientY > box.top && + event.clientY < box.bottom + ) { + return; + } + closeList(); + }, + [closeList] + ); + + const onSelectMessageVariable = useCallback( + (variable: ActionVariable) => { + if (!textAreaRef.current) return; + const { selectionStart: startPosition, selectionEnd: endPosition } = textAreaRef.current; + const templatedVar = templateActionVariable(variable); + + const newValue = + (inputTargetValue ?? '').substring(0, startPosition) + + templatedVar + + (inputTargetValue ?? '').substring(endPosition, (inputTargetValue ?? '').length); + + editAction(paramsProperty, newValue, index); + }, + [editAction, index, inputTargetValue, paramsProperty] + ); + + const renderSelectableOption = (option: any) => { + if (searchWord) { + return {option.label}; + } + return option.label; + }; + + const selectableStyle: Properties = useMemo( + () => ({ + position: 'absolute', + top: popupPosition.top, + width: popupPosition.width, + left: popupPosition.left, + border: `${euiTheme.border.width.thin} solid ${euiTheme.border.color}`, + background: backgroundColor, + zIndex: euiThemeVars.euiZLevel1, + }), + [ + backgroundColor, + euiTheme.border.color, + euiTheme.border.width.thin, + popupPosition.left, + popupPosition.top, + popupPosition.width, + ] + ); + + const onFocus = useCallback(() => setListOpen(true), []); + const onBlur = useCallback(() => { + if (!inputTargetValue && !isListOpen) { + editAction(paramsProperty, '', index); + } + }, [editAction, index, inputTargetValue, isListOpen, paramsProperty]); + const onClick = useCallback(() => closeList(), [closeList]); + + const onScroll = useCallback( + (evt) => { + // FUTURE ENGINEER -> we need to make sure to not close the autocomplete option list + if (selectableRef?.current?.listId !== evt.target?.firstElementChild?.id) { + closeList(true); + } + }, + [closeList] + ); + + useEffect(() => { + window.addEventListener('scroll', onScroll, { passive: true, capture: true }); + return () => { + window.removeEventListener('scroll', onScroll, { capture: true }); + }; + }, [onScroll]); + + return ( + 0 && inputTargetValue !== undefined} + label={label} + labelAppend={ + + } + > + <> + + 0 && inputTargetValue !== undefined} + name={paramsProperty} + value={inputTargetValue || ''} + data-test-subj={`${paramsProperty}TextArea`} + onChange={onChangeWithMessageVariable} + onFocus={onFocus} + onKeyDown={textareaOnKeyPress} + onBlur={onBlur} + onClick={onClick} + /> + + {matches.length > 0 && isListOpen && ( + + 5 ? 32 * 5.5 : matches.length * 32} + options={optionsToShow} + onChange={onOptionPick} + singleSelection + renderOption={renderSelectableOption} + listProps={selectableListProps} + > + {(list) => list} + + + )} + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { TextAreaWithAutocomplete as default }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx index a0a41a57a8331..76cc3b136455a 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.test.tsx @@ -7,10 +7,35 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { render, fireEvent, screen } from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; import EmailParamsFields from './email_params'; +import { getIsExperimentalFeatureEnabled } from '../../common/get_experimental_features'; + +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + useKibana: jest.fn(), +})); +jest.mock('../../common/get_experimental_features'); + +const useKibanaMock = useKibana as jest.Mock; +const mockKibana = () => { + useKibanaMock.mockReturnValue({ + services: { + triggersActionsUi: triggersActionsUiMock.createStart(), + }, + }); +}; describe('EmailParamsFields renders', () => { - test('all params fields is rendered', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockKibana(); + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + }); + + test('all params fields is rendered', async () => { const actionParams = { cc: [], bcc: [], @@ -19,21 +44,22 @@ describe('EmailParamsFields renders', () => { message: 'test message', }; - const wrapper = mountWithIntl( - {}} - index={0} - /> + render( + + {}} + defaultMessage={'Some default message'} + index={0} + /> + ); - expect(wrapper.find('[data-test-subj="toEmailAddressInput"]').length > 0).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="toEmailAddressInput"]').first().prop('selectedOptions') - ).toStrictEqual([{ label: 'test@test.com' }]); - expect(wrapper.find('[data-test-subj="subjectInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy(); + expect(screen.getByTestId('toEmailAddressInput')).toBeVisible(); + expect(screen.getByTestId('toEmailAddressInput').textContent).toStrictEqual('test@test.com'); + expect(screen.getByTestId('subjectInput')).toBeVisible(); + expect(await screen.findByTestId('messageTextArea')).toBeVisible(); }); test('message param field is rendered with default value if not set', () => { @@ -95,36 +121,57 @@ describe('EmailParamsFields renders', () => { }; const editAction = jest.fn(); - const wrapper = mountWithIntl( - + const { rerender } = render( + + + ); expect(editAction).toHaveBeenCalledWith('message', 'Some default message', 0); // simulate value being updated const valueToSimulate = 'some new value'; - wrapper - .find('[data-test-subj="messageTextArea"]') - .last() - .simulate('change', { target: { value: valueToSimulate } }); - expect(editAction).toHaveBeenCalledWith('message', valueToSimulate, 0); - wrapper.setProps({ - actionParams: { - ...actionParams, - message: valueToSimulate, - }, + fireEvent.change(screen.getByTestId('messageTextArea'), { + target: { value: valueToSimulate }, }); - // simulate default changing - wrapper.setProps({ - defaultMessage: 'Some different default message', - }); + expect(editAction).toHaveBeenCalledWith('message', valueToSimulate, 0); + + rerender( + + + + ); + + rerender( + + + + ); expect(editAction).not.toHaveBeenCalledWith('message', 'Some different default message', 0); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx index a8df45ba0e33f..2100e2b0d823c 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/email/email_params.tsx @@ -5,16 +5,18 @@ * 2.0. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiComboBox, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public'; import { - TextAreaWithMessageVariables, TextFieldWithMessageVariables, + TextAreaWithMessageVariables, } from '@kbn/triggers-actions-ui-plugin/public'; import { EmailActionParams } from '../types'; +import { getIsExperimentalFeatureEnabled } from '../../common/get_experimental_features'; +import { TextAreaWithAutocomplete } from '../../components/text_area_with_autocomplete'; const noop = () => {}; @@ -31,6 +33,7 @@ export const EmailParamsFields = ({ showEmailSubjectAndMessage = true, useDefaultMessage, }: ActionParamsProps) => { + const isMustacheAutocompleteOn = getIsExperimentalFeatureEnabled('isMustacheAutocompleteOn'); const { to, cc, bcc, subject, message } = actionParams; const toOptions = to ? to.map((label: string) => ({ label })) : []; const ccOptions = cc ? cc.map((label: string) => ({ label })) : []; @@ -60,6 +63,11 @@ export const EmailParamsFields = ({ const isCCInvalid: boolean = errors.cc !== undefined && errors.cc.length > 0 && cc !== undefined; const isBCCInvalid: boolean = errors.bcc !== undefined && errors.bcc.length > 0 && bcc !== undefined; + + const TextAreaComponent = useMemo(() => { + return isMustacheAutocompleteOn ? TextAreaWithAutocomplete : TextAreaWithMessageVariables; + }, [isMustacheAutocompleteOn]); + return ( <> )} {showEmailSubjectAndMessage && ( - new StackConnectorsPublicPlugin(); +export const plugin = (context: PluginInitializerContext) => + new StackConnectorsPublicPlugin(context); diff --git a/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.test.ts b/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.test.ts new file mode 100644 index 0000000000000..b51aa70475515 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { filterSuggestions } from './filter_suggestions_for_autocomplete'; + +const defaultActionVariablesList = [ + 'kibana.alert.id', + 'kibana.context.cloud.group', + 'context.container', + 'context.originalAlertState', + 'date', + 'rule.spaceId', + 'kibana.alertActionGroup', + 'tags', +]; +describe('Unit tests for filterSuggestions function', () => { + test('should return empty list if actionVariablesList argument is undefined', () => { + expect(filterSuggestions({ propertyPath: 'alert.id' })).toEqual([]); + }); + + test('should return full sorted list of suggestions if propertyPath is empty string', () => { + expect( + filterSuggestions({ actionVariablesList: defaultActionVariablesList, propertyPath: '' }) + ).toEqual([ + 'context', + 'context.container', + 'context.originalAlertState', + 'date', + 'kibana', + 'kibana.alert', + 'kibana.alert.id', + 'kibana.alertActionGroup', + 'kibana.context', + 'kibana.context.cloud', + 'kibana.context.cloud.group', + 'rule', + 'rule.spaceId', + 'tags', + ]); + }); + + test('should return sorted of filtered suggestions, v1', () => { + expect( + filterSuggestions({ actionVariablesList: defaultActionVariablesList, propertyPath: 'ki' }) + ).toEqual([ + 'kibana', + 'kibana.alert', + 'kibana.alert.id', + 'kibana.alertActionGroup', + 'kibana.context', + 'kibana.context.cloud', + 'kibana.context.cloud.group', + ]); + }); + + test('should return sorted of filtered suggestions, v2', () => { + expect( + filterSuggestions({ + actionVariablesList: defaultActionVariablesList, + propertyPath: 'kibana.al', + }) + ).toEqual(['kibana.alert', 'kibana.alert.id', 'kibana.alertActionGroup']); + }); + + test('should return sorted of filtered suggestions, v3', () => { + expect( + filterSuggestions({ + actionVariablesList: defaultActionVariablesList, + propertyPath: 'kibana.context.cloud.g', + }) + ).toEqual(['kibana.context.cloud.group']); + }); +}); diff --git a/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.ts b/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.ts new file mode 100644 index 0000000000000..80362d2b6770b --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/lib/filter_suggestions_for_autocomplete.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export const filterSuggestions = ({ + actionVariablesList, + propertyPath, +}: { + actionVariablesList?: string[]; + propertyPath: string; +}) => { + if (!actionVariablesList) return []; + const allSuggestions: string[] = []; + actionVariablesList.forEach((suggestion: string) => { + const splittedWords = suggestion.split('.'); + for (let i = 0; i < splittedWords.length; i++) { + const currentSuggestion = splittedWords.slice(0, i + 1).join('.'); + if (!allSuggestions.includes(currentSuggestion)) { + allSuggestions.push(currentSuggestion); + } + } + }); + return allSuggestions.sort().filter((suggestion) => suggestion.startsWith(propertyPath)); +}; diff --git a/x-pack/plugins/stack_connectors/public/lib/template_action_variable.test.ts b/x-pack/plugins/stack_connectors/public/lib/template_action_variable.test.ts new file mode 100644 index 0000000000000..3ac06967b641a --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/lib/template_action_variable.test.ts @@ -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 { templateActionVariable } from './template_action_variable'; + +describe('templateActionVariable', () => { + const actionVariable = { + name: 'myVar', + description: 'My variable description', + }; + + test('variable returns with double braces by default', () => { + expect(templateActionVariable(actionVariable)).toEqual('{{myVar}}'); + }); + + test('variable returns with triple braces when specified', () => { + expect( + templateActionVariable({ ...actionVariable, useWithTripleBracesInTemplates: true }) + ).toEqual('{{{myVar}}}'); + }); +}); diff --git a/x-pack/plugins/stack_connectors/public/lib/template_action_variable.ts b/x-pack/plugins/stack_connectors/public/lib/template_action_variable.ts new file mode 100644 index 0000000000000..887564a8213c4 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/lib/template_action_variable.ts @@ -0,0 +1,14 @@ +/* + * 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 { ActionVariable } from '@kbn/alerting-plugin/common'; + +export function templateActionVariable(variable: ActionVariable) { + return variable.useWithTripleBracesInTemplates + ? `{{{${variable.name}}}}` + : `{{${variable.name}}}`; +} diff --git a/x-pack/plugins/stack_connectors/public/plugin.ts b/x-pack/plugins/stack_connectors/public/plugin.ts index bc9d855a14303..3c153d0de2573 100644 --- a/x-pack/plugins/stack_connectors/public/plugin.ts +++ b/x-pack/plugins/stack_connectors/public/plugin.ts @@ -5,10 +5,16 @@ * 2.0. */ -import { CoreSetup, Plugin } from '@kbn/core/public'; +import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { TriggersAndActionsUIPublicPluginSetup } from '@kbn/triggers-actions-ui-plugin/public'; import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; import { registerConnectorTypes } from './connector_types'; +import { ExperimentalFeaturesService } from './common/experimental_features_service'; +import { + ExperimentalFeatures, + parseExperimentalConfigValue, +} from '../common/experimental_features'; +import { StackConnectorsConfigType } from '../common/types'; export type Setup = void; export type Start = void; @@ -21,6 +27,13 @@ export interface StackConnectorsPublicSetupDeps { export class StackConnectorsPublicPlugin implements Plugin { + private config: StackConnectorsConfigType; + readonly experimentalFeatures: ExperimentalFeatures; + + constructor(ctx: PluginInitializerContext) { + this.config = ctx.config.get(); + this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental || []); + } public setup(core: CoreSetup, { triggersActionsUi, actions }: StackConnectorsPublicSetupDeps) { registerConnectorTypes({ connectorTypeRegistry: triggersActionsUi.actionTypeRegistry, @@ -28,6 +41,7 @@ export class StackConnectorsPublicPlugin validateEmailAddresses: actions.validateEmailAddresses, }, }); + ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures }); } public start() {} diff --git a/x-pack/plugins/stack_connectors/server/config.ts b/x-pack/plugins/stack_connectors/server/config.ts new file mode 100644 index 0000000000000..d58e58b0e450c --- /dev/null +++ b/x-pack/plugins/stack_connectors/server/config.ts @@ -0,0 +1,49 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext } from '@kbn/core/server'; + +import { + ExperimentalFeatures, + getExperimentalAllowedValues, + isValidExperimentalValue, + parseExperimentalConfigValue, +} from '../common/experimental_features'; + +const allowedExperimentalValues = getExperimentalAllowedValues(); + +export const configSchema = schema.object({ + enableExperimental: schema.arrayOf(schema.string(), { + defaultValue: () => [], + validate(list) { + for (const key of list) { + if (!isValidExperimentalValue(key)) { + return `[${key}] is not allowed. Allowed values are: ${allowedExperimentalValues.join( + ', ' + )}`; + } + } + }, + }), +}); + +export type ConfigSchema = TypeOf; + +export type ConfigType = ConfigSchema & { + experimentalFeatures: ExperimentalFeatures; +}; + +export const createConfig = (context: PluginInitializerContext): ConfigType => { + const pluginConfig = context.config.get>(); + const experimentalFeatures = parseExperimentalConfigValue(pluginConfig.enableExperimental); + + return { + ...pluginConfig, + experimentalFeatures, + }; +}; diff --git a/x-pack/plugins/stack_connectors/server/index.ts b/x-pack/plugins/stack_connectors/server/index.ts index 2cc792da9f9a3..c41b13ee2b15c 100644 --- a/x-pack/plugins/stack_connectors/server/index.ts +++ b/x-pack/plugins/stack_connectors/server/index.ts @@ -4,8 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { PluginInitializerContext } from '@kbn/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server'; import { StackConnectorsPlugin } from './plugin'; +import { configSchema, ConfigSchema } from './config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + enableExperimental: true, + }, + schema: configSchema, +}; export const plugin = (initContext: PluginInitializerContext) => new StackConnectorsPlugin(initContext); diff --git a/x-pack/plugins/stack_connectors/tsconfig.json b/x-pack/plugins/stack_connectors/tsconfig.json index f18dfaea77cca..0009b86c8348c 100644 --- a/x-pack/plugins/stack_connectors/tsconfig.json +++ b/x-pack/plugins/stack_connectors/tsconfig.json @@ -33,7 +33,10 @@ "@kbn/core-saved-objects-common", "@kbn/core-http-browser-mocks", "@kbn/core-saved-objects-api-server-mocks", + "@kbn/alerts-ui-shared", + "@kbn/alerting-plugin", "@kbn/securitysolution-ecs", + "@kbn/ui-theme", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 178d5f003f81c..9ade8eaf3ee41 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -38430,15 +38430,6 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "AND", "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "quand", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "quand", - "xpack.triggersActionsUI.components.addMessageVariables.addRuleVariableTitle": "Ajouter une variable", - "xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton": "Ajouter une variable", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreHidden": "Les variables déclassées sont masquées", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreShown": "Les variables déclassées sont affichées", - "xpack.triggersActionsUI.components.addMessageVariables.hideDeprecatedVariables": "Masquer", - "xpack.triggersActionsUI.components.addMessageVariables.loadingMessage": "Chargement des variables", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesAvailable": "Aucune variable disponible", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesFound": "Aucune variable trouvée", - "xpack.triggersActionsUI.components.addMessageVariables.showAllDeprecatedVariables": "Afficher tout", "xpack.triggersActionsUI.components.alertTable.useFetchAlerts.errorMessageText": "Une erreur s'est produite lors de la recherche des alertes", "xpack.triggersActionsUI.components.alertTable.useFetchBrowserFieldsCapabilities.errorMessageText": "Une erreur s'est produite lors du chargement des champs du navigateur", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "Choisir…", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index aca7c6cf736d0..81c28867cb7bc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -38421,15 +38421,6 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "AND", "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "タイミング", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "タイミング", - "xpack.triggersActionsUI.components.addMessageVariables.addRuleVariableTitle": "変数を追加", - "xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton": "変数を追加", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreHidden": "廃止予定の変数は非表示です", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreShown": "廃止予定の変数は表示されます", - "xpack.triggersActionsUI.components.addMessageVariables.hideDeprecatedVariables": "非表示", - "xpack.triggersActionsUI.components.addMessageVariables.loadingMessage": "変数を読み込み中", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesAvailable": "変数がありません", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesFound": "変数が見つかりません", - "xpack.triggersActionsUI.components.addMessageVariables.showAllDeprecatedVariables": "すべて表示", "xpack.triggersActionsUI.components.alertTable.useFetchAlerts.errorMessageText": "アラート検索でエラーが発生しました", "xpack.triggersActionsUI.components.alertTable.useFetchBrowserFieldsCapabilities.errorMessageText": "ブラウザーフィールドの読み込み中にエラーが発生しました", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "選択…", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cf5593d5334e3..5a7410a88fcee 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -38415,15 +38415,6 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "且", "xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "当", "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "当", - "xpack.triggersActionsUI.components.addMessageVariables.addRuleVariableTitle": "添加变量", - "xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton": "添加变量", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreHidden": "将隐藏已弃用变量", - "xpack.triggersActionsUI.components.addMessageVariables.deprecatedVariablesAreShown": "将显示已弃用变量", - "xpack.triggersActionsUI.components.addMessageVariables.hideDeprecatedVariables": "隐藏", - "xpack.triggersActionsUI.components.addMessageVariables.loadingMessage": "正在加载变量", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesAvailable": "无变量可用", - "xpack.triggersActionsUI.components.addMessageVariables.noVariablesFound": "找不到变量", - "xpack.triggersActionsUI.components.addMessageVariables.showAllDeprecatedVariables": "全部显示", "xpack.triggersActionsUI.components.alertTable.useFetchAlerts.errorMessageText": "搜索告警时发生错误", "xpack.triggersActionsUI.components.alertTable.useFetchBrowserFieldsCapabilities.errorMessageText": "加载浏览器字段时出错", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "选择……", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/index.ts index 41ca1f51c735a..08ab85eece444 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/index.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - export { JsonEditorWithMessageVariables } from './json_editor_with_message_variables'; export { TextFieldWithMessageVariables } from './text_field_with_message_variables'; export { TextAreaWithMessageVariables } from './text_area_with_message_variables'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx index 1acd3e6392450..4459735acb927 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx @@ -11,12 +11,11 @@ import { EuiFormRow, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { monaco, XJsonLang } from '@kbn/monaco'; -import './add_message_variables.scss'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; import { CodeEditor } from '@kbn/kibana-react-plugin/public'; import { ActionVariable } from '@kbn/alerting-plugin/common'; -import { AddMessageVariables } from './add_message_variables'; +import { AddMessageVariables } from '@kbn/alerts-ui-shared'; import { templateActionVariable } from '../lib'; const NO_EDITOR_ERROR_TITLE = i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx index 346dce44af60b..c19ac416b4695 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -7,9 +7,8 @@ import React, { useState } from 'react'; import { EuiTextArea, EuiFormRow } from '@elastic/eui'; -import './add_message_variables.scss'; import { ActionVariable } from '@kbn/alerting-plugin/common'; -import { AddMessageVariables } from './add_message_variables'; +import { AddMessageVariables } from '@kbn/alerts-ui-shared'; import { templateActionVariable } from '../lib'; interface Props { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx index 0efe53603085e..e2dc816987d3d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx @@ -7,9 +7,8 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiFieldText, EuiFormRow } from '@elastic/eui'; -import './add_message_variables.scss'; import { ActionVariable } from '@kbn/alerting-plugin/common'; -import { AddMessageVariables } from './add_message_variables'; +import { AddMessageVariables } from '@kbn/alerts-ui-shared'; import { templateActionVariable } from '../lib'; interface Props { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/validate_params_for_warnings.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/validate_params_for_warnings.ts index e0f552f6bb0f7..5a5dc6f33e076 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/validate_params_for_warnings.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/validate_params_for_warnings.ts @@ -43,10 +43,10 @@ export function validateParamsForWarnings( return publicUrlWarning; } } catch (e) { - /* - * do nothing, we don't care if the mustache is invalid - */ + // Better to set the warning msg if you do not know if the mustache template is invalid + return publicUrlWarning; } } + return null; } diff --git a/yarn.lock b/yarn.lock index 52e67729e7891..b7b5e99534103 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9780,6 +9780,11 @@ dependencies: "@types/jest" "*" +"@types/textarea-caret@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/textarea-caret/-/textarea-caret-3.0.1.tgz#5afd4b1c1b3bacb001d76a1e6ef192c710709a86" + integrity sha512-JjrXYzk4t6dM/5nz1hHkZXmd3xSdJM6mOIDSBUrpg4xThwKNryiu4CqHx81LwUJHxEEoQWHTu4fMV4em+c5bXg== + "@types/through@*": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" @@ -28504,6 +28509,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +textarea-caret@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/textarea-caret/-/textarea-caret-3.1.0.tgz#5d5a35bb035fd06b2ff0e25d5359e97f2655087f" + integrity sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q== + throttle-debounce@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.1.0.tgz#257e648f0a56bd9e54fe0f132c4ab8611df4e1d5"