mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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.
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 <xavier.mouligneau@elastic.co>
This commit is contained in:
parent
a6aa865dee
commit
0af40a3066
37 changed files with 890 additions and 101 deletions
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -10,4 +10,5 @@ module.exports = {
|
|||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-alerts-ui-shared'],
|
||||
setupFilesAfterEnv: ['<rootDir>/packages/kbn-alerts-ui-shared/setup_tests.ts'],
|
||||
};
|
||||
|
|
10
packages/kbn-alerts-ui-shared/setup_tests.ts
Normal file
10
packages/kbn-alerts-ui-shared/setup_tests.ts
Normal file
|
@ -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';
|
|
@ -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 () => {
|
|
@ -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 {
|
|
@ -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',
|
||||
}
|
|
@ -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 }) => (
|
||||
<EuiText size="xs" color="subdued" css={styles.truncatedText}>
|
||||
{text}
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
TruncatedTextComponent.displayName = 'TruncatedText';
|
||||
|
||||
export const TruncatedText = React.memo(TruncatedTextComponent);
|
|
@ -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(<MaintenanceWindowCallout kibanaServices={kibanaServicesMock} />, {
|
||||
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(<MaintenanceWindowCallout kibanaServices={kibanaServicesMock} />, {
|
||||
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(<MaintenanceWindowCallout kibanaServices={servicesMock} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
// @ts-expect-error Jest types are incomplete in packages
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
|
@ -213,7 +212,7 @@ describe('MaintenanceWindowCallout', () => {
|
|||
const { findByText } = render(<MaintenanceWindowCallout kibanaServices={servicesMock} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
// @ts-expect-error Jest types are incomplete in packages
|
||||
|
||||
expect(await findByText('Maintenance window is running')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)',
|
||||
|
|
|
@ -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<keyof ExperimentalFeatures>;
|
||||
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
const InvalidExperimentalValue = class extends Error {};
|
||||
const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly<ExperimentalConfigKeys>;
|
||||
|
||||
/**
|
||||
* 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<Partial<ExperimentalFeatures>> = {};
|
||||
|
||||
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];
|
10
x-pack/plugins/stack_connectors/common/types.ts
Normal file
10
x-pack/plugins/stack_connectors/common/types.ts
Normal file
|
@ -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[];
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
"requiredPlugins": [
|
||||
"actions",
|
||||
"esUiShared",
|
||||
"kibanaReact",
|
||||
"triggersActionsUi"
|
||||
],
|
||||
"extraPublicDirs": [
|
||||
|
|
|
@ -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?'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
};
|
|
@ -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<TextAreaWithAutocompleteProps> = ({
|
||||
editAction,
|
||||
errors,
|
||||
index,
|
||||
inputTargetValue,
|
||||
isDisabled = false,
|
||||
label,
|
||||
messageVariables,
|
||||
paramsProperty,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const backgroundColor = useEuiBackgroundColor('plain');
|
||||
|
||||
const textAreaRef = React.useRef<HTMLTextAreaElement | null>(null);
|
||||
const selectableRef = React.useRef<EuiSelectable | null>(null);
|
||||
|
||||
const [matches, setMatches] = useState<string[]>([]);
|
||||
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<string>('');
|
||||
|
||||
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 <EuiHighlight search={searchWord}>{option.label}</EuiHighlight>;
|
||||
}
|
||||
return option.label;
|
||||
};
|
||||
|
||||
const selectableStyle: Properties<string | number> = 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 (
|
||||
<EuiFormRow
|
||||
error={errors}
|
||||
fullWidth
|
||||
isDisabled={isDisabled}
|
||||
isInvalid={errors && errors.length > 0 && inputTargetValue !== undefined}
|
||||
label={label}
|
||||
labelAppend={
|
||||
<AddMessageVariables
|
||||
messageVariables={messageVariables}
|
||||
onSelectEventHandler={onSelectMessageVariable}
|
||||
paramsProperty={paramsProperty}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<EuiOutsideClickDetector onOutsideClick={clickOutSideTextArea}>
|
||||
<EuiTextArea
|
||||
disabled={isDisabled}
|
||||
inputRef={textAreaRef}
|
||||
fullWidth
|
||||
isInvalid={errors && errors.length > 0 && inputTargetValue !== undefined}
|
||||
name={paramsProperty}
|
||||
value={inputTargetValue || ''}
|
||||
data-test-subj={`${paramsProperty}TextArea`}
|
||||
onChange={onChangeWithMessageVariable}
|
||||
onFocus={onFocus}
|
||||
onKeyDown={textareaOnKeyPress}
|
||||
onBlur={onBlur}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</EuiOutsideClickDetector>
|
||||
{matches.length > 0 && isListOpen && (
|
||||
<EuiPortal>
|
||||
<EuiSelectable
|
||||
ref={selectableRef}
|
||||
style={selectableStyle}
|
||||
height={matches.length > 5 ? 32 * 5.5 : matches.length * 32}
|
||||
options={optionsToShow}
|
||||
onChange={onOptionPick}
|
||||
singleSelection
|
||||
renderOption={renderSelectableOption}
|
||||
listProps={selectableListProps}
|
||||
>
|
||||
{(list) => list}
|
||||
</EuiSelectable>
|
||||
</EuiPortal>
|
||||
)}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { TextAreaWithAutocomplete as default };
|
|
@ -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<any, any>).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(
|
||||
<EmailParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ to: [], cc: [], bcc: [], subject: [], message: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
/>
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<EmailParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ to: [], cc: [], bcc: [], subject: [], message: [] }}
|
||||
editAction={() => {}}
|
||||
defaultMessage={'Some default message'}
|
||||
index={0}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<EmailParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ to: [], cc: [], bcc: [], subject: [], message: [] }}
|
||||
editAction={editAction}
|
||||
defaultMessage={'Some default message'}
|
||||
index={0}
|
||||
/>
|
||||
const { rerender } = render(
|
||||
<IntlProvider locale="en">
|
||||
<EmailParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ to: [], cc: [], bcc: [], subject: [], message: [] }}
|
||||
editAction={editAction}
|
||||
defaultMessage={'Some default message'}
|
||||
index={0}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<IntlProvider locale="en">
|
||||
<EmailParamsFields
|
||||
actionParams={{
|
||||
...actionParams,
|
||||
message: valueToSimulate,
|
||||
}}
|
||||
errors={{ to: [], cc: [], bcc: [], subject: [], message: [] }}
|
||||
editAction={editAction}
|
||||
defaultMessage={'Some default message'}
|
||||
index={0}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
rerender(
|
||||
<IntlProvider locale="en">
|
||||
<EmailParamsFields
|
||||
actionParams={{
|
||||
...actionParams,
|
||||
message: valueToSimulate,
|
||||
}}
|
||||
errors={{ to: [], cc: [], bcc: [], subject: [], message: [] }}
|
||||
editAction={editAction}
|
||||
defaultMessage={'Some different default message'}
|
||||
index={0}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(editAction).not.toHaveBeenCalledWith('message', 'Some different default message', 0);
|
||||
});
|
||||
|
|
|
@ -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<EmailActionParams>) => {
|
||||
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 (
|
||||
<>
|
||||
<EuiFormRow
|
||||
|
@ -231,7 +239,7 @@ export const EmailParamsFields = ({
|
|||
</EuiFormRow>
|
||||
)}
|
||||
{showEmailSubjectAndMessage && (
|
||||
<TextAreaWithMessageVariables
|
||||
<TextAreaComponent
|
||||
index={index}
|
||||
editAction={editAction}
|
||||
messageVariables={messageVariables}
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
import { PluginInitializerContext } from '@kbn/core/public';
|
||||
import { StackConnectorsPublicPlugin } from './plugin';
|
||||
|
||||
export const plugin = (ctx: PluginInitializerContext) => new StackConnectorsPublicPlugin();
|
||||
export const plugin = (context: PluginInitializerContext) =>
|
||||
new StackConnectorsPublicPlugin(context);
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
};
|
|
@ -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}}}');
|
||||
});
|
||||
});
|
|
@ -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}}}`;
|
||||
}
|
|
@ -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<Setup, Start, StackConnectorsPublicSetupDeps>
|
||||
{
|
||||
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() {}
|
||||
|
|
49
x-pack/plugins/stack_connectors/server/config.ts
Normal file
49
x-pack/plugins/stack_connectors/server/config.ts
Normal file
|
@ -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<typeof configSchema>;
|
||||
|
||||
export type ConfigType = ConfigSchema & {
|
||||
experimentalFeatures: ExperimentalFeatures;
|
||||
};
|
||||
|
||||
export const createConfig = (context: PluginInitializerContext): ConfigType => {
|
||||
const pluginConfig = context.config.get<TypeOf<typeof configSchema>>();
|
||||
const experimentalFeatures = parseExperimentalConfigValue(pluginConfig.enableExperimental);
|
||||
|
||||
return {
|
||||
...pluginConfig,
|
||||
experimentalFeatures,
|
||||
};
|
||||
};
|
|
@ -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<ConfigSchema> = {
|
||||
exposeToBrowser: {
|
||||
enableExperimental: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
};
|
||||
|
||||
export const plugin = (initContext: PluginInitializerContext) =>
|
||||
new StackConnectorsPlugin(initContext);
|
||||
|
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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": "選択…",
|
||||
|
|
|
@ -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": "选择……",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
10
yarn.lock
10
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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue