Skip to content

Commit

Permalink
[RAM] Autocomplete (#158454)
Browse files Browse the repository at this point in the history
## Summary

Solves this issue: #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 <xavier.mouligneau@elastic.co>
  • Loading branch information
3 people authored Aug 31, 2023
1 parent a6aa865 commit 0af40a3
Show file tree
Hide file tree
Showing 37 changed files with 890 additions and 101 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-alerts-ui-shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
1 change: 1 addition & 0 deletions packages/kbn-alerts-ui-shared/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 10 additions & 0 deletions packages/kbn-alerts-ui-shared/setup_tests.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand All @@ -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);
});
Expand All @@ -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);
});
Expand Down Expand Up @@ -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();
});

Expand All @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down
52 changes: 52 additions & 0 deletions x-pack/plugins/stack_connectors/common/experimental_features.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions x-pack/plugins/stack_connectors/common/types.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
1 change: 1 addition & 0 deletions x-pack/plugins/stack_connectors/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"requiredPlugins": [
"actions",
"esUiShared",
"kibanaReact",
"triggersActionsUi"
],
"extraPublicDirs": [
Expand Down
Original file line number Diff line number Diff line change
@@ -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?'
);
}
}
Original file line number Diff line number Diff line change
@@ -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];
};
Loading

0 comments on commit 0af40a3

Please sign in to comment.