= props => {
if (!serviceName) {
throw new Error('Service name is required');
}
-
const columns = useMemo(
() => [
{
- name: i18n.translate('xpack.apm.errorsTable.groupIdColumnLabel', {
- defaultMessage: 'Group ID'
- }),
+ name: (
+ <>
+ {i18n.translate('xpack.apm.errorsTable.groupIdColumnLabel', {
+ defaultMessage: 'Group ID'
+ })}{' '}
+
+ >
+ ),
field: 'groupId',
sortable: false,
width: px(unit * 6),
diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx
index cff190cd98a11..6aa7815ad688c 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx
@@ -134,9 +134,11 @@ export function MachineLearningFlyoutView({
+ ),
+ serviceMapAnnotationText: (
+
+ {i18n.translate(
+ 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.serviceMapAnnotationText',
+ {
+ defaultMessage: 'service maps'
}
)}
@@ -155,15 +167,15 @@ export function MachineLearningFlyoutView({
{i18n.translate(
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText',
{
- defaultMessage: 'Machine Learning jobs management page'
+ defaultMessage: 'Machine Learning Job Management page'
}
)}
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx
index e3b33f11d0805..f8dcec14630a5 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiIcon, EuiToolTip } from '@elastic/eui';
+import { EuiToolTip, EuiIconTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import styled from 'styled-components';
@@ -109,27 +109,26 @@ export function TransactionList({ items, isLoading }: Props) {
{
field: 'impact',
name: (
-
- <>
- {i18n.translate('xpack.apm.transactionsTable.impactColumnLabel', {
- defaultMessage: 'Impact'
- })}{' '}
-
- >
-
+ <>
+ {i18n.translate('xpack.apm.transactionsTable.impactColumnLabel', {
+ defaultMessage: 'Impact'
+ })}{' '}
+
+ >
),
sortable: true,
dataType: 'number',
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
index 4092e0148286e..6d9a917af659f 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
@@ -6,7 +6,8 @@
import { EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import React, { FunctionComponent, useMemo, useState } from 'react';
+import React, { FunctionComponent, useMemo, useState, MouseEvent } from 'react';
+import url from 'url';
import { Filter } from '../../../../common/custom_link/custom_link_types';
import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
import {
@@ -82,7 +83,39 @@ export const TransactionActionMenu: FunctionComponent = ({
basePath: core.http.basePath,
location,
urlParams
- });
+ }).map(sectionList =>
+ sectionList.map(section => ({
+ ...section,
+ actions: section.actions.map(action => {
+ const { href } = action;
+
+ // use navigateToApp as a temporary workaround for faster navigation between observability apps.
+ // see https://github.com/elastic/kibana/issues/65682
+
+ return {
+ ...action,
+ onClick: (event: MouseEvent) => {
+ const parsed = url.parse(href);
+
+ const appPathname = core.http.basePath.remove(
+ parsed.pathname ?? ''
+ );
+
+ const [, , app, ...rest] = appPathname.split('/');
+
+ if (app === 'uptime' || app === 'metrics' || app === 'logs') {
+ event.preventDefault();
+ core.application.navigateToApp(app, {
+ path: `${rest.join('/')}${
+ parsed.search ? `&${parsed.search}` : ''
+ }`
+ });
+ }
+ }
+ };
+ })
+ }))
+ );
const closePopover = () => {
setIsActionPopoverOpen(false);
@@ -151,6 +184,7 @@ export const TransactionActionMenu: FunctionComponent = ({
key={action.key}
label={action.label}
href={action.href}
+ onClick={action.onClick}
/>
))}
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
index 7d5f0a75d2208..8fb44b70bc081 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
@@ -9,16 +9,15 @@ import {
SERVICE_ENVIRONMENT,
SERVICE_NAME
} from '../../../common/elasticsearch_fieldnames';
+import { getMlIndex } from '../../../common/ml_job_constants';
import { getServicesProjection } from '../../../common/projections/services';
import { mergeProjection } from '../../../common/projections/util/merge_projection';
import { PromiseReturnType } from '../../../typings/common';
+import { rangeFilter } from '../helpers/range_filter';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
-import { dedupeConnections } from './dedupe_connections';
+import { transformServiceMapResponses } from './transform_service_map_responses';
import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids';
import { getTraceSampleIds } from './get_trace_sample_ids';
-import { addAnomaliesToServicesData } from './ml_helpers';
-import { getMlIndex } from '../../../common/ml_job_constants';
-import { rangeFilter } from '../helpers/range_filter';
export interface IEnvOptions {
setup: Setup & SetupTimeRange;
@@ -179,13 +178,9 @@ export async function getServiceMap(options: IEnvOptions) {
getAnomaliesData(options)
]);
- const servicesDataWithAnomalies = addAnomaliesToServicesData(
- servicesData,
- anomaliesData
- );
-
- return dedupeConnections({
+ return transformServiceMapResponses({
...connectionData,
- services: servicesDataWithAnomalies
+ anomalies: anomaliesData,
+ services: servicesData
});
}
diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts
index c80ba8dba01ea..908dbe6df4636 100644
--- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts
@@ -5,11 +5,11 @@
*/
import { AnomaliesResponse } from './get_service_map';
-import { addAnomaliesToServicesData } from './ml_helpers';
+import { addAnomaliesDataToNodes } from './ml_helpers';
-describe('addAnomaliesToServicesData', () => {
- it('adds anomalies to services data', () => {
- const servicesData = [
+describe('addAnomaliesDataToNodes', () => {
+ it('adds anomalies to nodes', () => {
+ const nodes = [
{
'service.name': 'opbeans-ruby',
'agent.name': 'ruby',
@@ -89,8 +89,8 @@ describe('addAnomaliesToServicesData', () => {
];
expect(
- addAnomaliesToServicesData(
- servicesData,
+ addAnomaliesDataToNodes(
+ nodes,
(anomaliesResponse as unknown) as AnomaliesResponse
)
).toEqual(result);
diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts
index 9789911660bd0..fae9e7d4cb1c6 100644
--- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts
@@ -9,10 +9,11 @@ import {
getMlJobServiceName,
getSeverity
} from '../../../common/ml_job_constants';
-import { AnomaliesResponse, ServicesResponse } from './get_service_map';
+import { ConnectionNode } from '../../../common/service_map';
+import { AnomaliesResponse } from './get_service_map';
-export function addAnomaliesToServicesData(
- servicesData: ServicesResponse,
+export function addAnomaliesDataToNodes(
+ nodes: ConnectionNode[],
anomaliesResponse: AnomaliesResponse
) {
const anomaliesMap = (
@@ -52,7 +53,7 @@ export function addAnomaliesToServicesData(
};
}, {});
- const servicesDataWithAnomalies = servicesData.map(service => {
+ const servicesDataWithAnomalies = nodes.map(service => {
const serviceAnomalies = anomaliesMap[service[SERVICE_NAME]];
if (serviceAnomalies) {
const maxScore = serviceAnomalies.max_score;
diff --git a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
similarity index 83%
rename from x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts
rename to x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
index 4af8a54139204..45b64c1ad03a4 100644
--- a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
@@ -4,16 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ServiceMapResponse } from './';
import {
- SPAN_DESTINATION_SERVICE_RESOURCE,
- SERVICE_NAME,
- SERVICE_ENVIRONMENT,
AGENT_NAME,
- SPAN_TYPE,
- SPAN_SUBTYPE
-} from '../../../../common/elasticsearch_fieldnames';
-import { dedupeConnections } from './';
+ SERVICE_ENVIRONMENT,
+ SERVICE_NAME,
+ SPAN_DESTINATION_SERVICE_RESOURCE,
+ SPAN_SUBTYPE,
+ SPAN_TYPE
+} from '../../../common/elasticsearch_fieldnames';
+import { AnomaliesResponse } from './get_service_map';
+import {
+ transformServiceMapResponses,
+ ServiceMapResponse
+} from './transform_service_map_responses';
const nodejsService = {
[SERVICE_NAME]: 'opbeans-node',
@@ -33,9 +36,14 @@ const javaService = {
[AGENT_NAME]: 'java'
};
-describe('dedupeConnections', () => {
+const anomalies = ({
+ aggregations: { jobs: { buckets: [] } }
+} as unknown) as AnomaliesResponse;
+
+describe('transformServiceMapResponses', () => {
it('maps external destinations to internal services', () => {
const response: ServiceMapResponse = {
+ anomalies,
services: [nodejsService, javaService],
discoveredServices: [
{
@@ -51,7 +59,7 @@ describe('dedupeConnections', () => {
]
};
- const { elements } = dedupeConnections(response);
+ const { elements } = transformServiceMapResponses(response);
const connection = elements.find(
element => 'source' in element.data && 'target' in element.data
@@ -67,6 +75,7 @@ describe('dedupeConnections', () => {
it('collapses external destinations based on span.destination.resource.name', () => {
const response: ServiceMapResponse = {
+ anomalies,
services: [nodejsService, javaService],
discoveredServices: [
{
@@ -89,7 +98,7 @@ describe('dedupeConnections', () => {
]
};
- const { elements } = dedupeConnections(response);
+ const { elements } = transformServiceMapResponses(response);
const connections = elements.filter(element => 'source' in element.data);
@@ -102,6 +111,7 @@ describe('dedupeConnections', () => {
it('picks the first span.type/subtype in an alphabetically sorted list', () => {
const response: ServiceMapResponse = {
+ anomalies,
services: [javaService],
discoveredServices: [],
connections: [
@@ -126,7 +136,7 @@ describe('dedupeConnections', () => {
]
};
- const { elements } = dedupeConnections(response);
+ const { elements } = transformServiceMapResponses(response);
const nodes = elements.filter(element => !('source' in element.data));
@@ -140,6 +150,7 @@ describe('dedupeConnections', () => {
it('processes connections without a matching "service" aggregation', () => {
const response: ServiceMapResponse = {
+ anomalies,
services: [javaService],
discoveredServices: [],
connections: [
@@ -150,7 +161,7 @@ describe('dedupeConnections', () => {
]
};
- const { elements } = dedupeConnections(response);
+ const { elements } = transformServiceMapResponses(response);
expect(elements.length).toBe(3);
});
diff --git a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
similarity index 76%
rename from x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts
rename to x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
index e5d7c0b2de10c..8b91bb98b5200 100644
--- a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
@@ -10,14 +10,19 @@ import {
SPAN_DESTINATION_SERVICE_RESOURCE,
SPAN_TYPE,
SPAN_SUBTYPE
-} from '../../../../common/elasticsearch_fieldnames';
+} from '../../../common/elasticsearch_fieldnames';
import {
Connection,
ConnectionNode,
ServiceConnectionNode,
ExternalConnectionNode
-} from '../../../../common/service_map';
-import { ConnectionsResponse, ServicesResponse } from '../get_service_map';
+} from '../../../common/service_map';
+import {
+ ConnectionsResponse,
+ ServicesResponse,
+ AnomaliesResponse
+} from './get_service_map';
+import { addAnomaliesDataToNodes } from './ml_helpers';
function getConnectionNodeId(node: ConnectionNode): string {
if ('span.destination.service.resource' in node) {
@@ -34,13 +39,16 @@ function getConnectionId(connection: Connection) {
}
export type ServiceMapResponse = ConnectionsResponse & {
+ anomalies: AnomaliesResponse;
services: ServicesResponse;
};
-export function dedupeConnections(response: ServiceMapResponse) {
- const { discoveredServices, services, connections } = response;
+export function transformServiceMapResponses(response: ServiceMapResponse) {
+ const { anomalies, discoveredServices, services, connections } = response;
- const allNodes = connections
+ // Derive the rest of the map nodes from the connections and add the services
+ // from the services data query
+ const allNodes: ConnectionNode[] = connections
.flatMap(connection => [connection.source, connection.destination])
.map(node => ({ ...node, id: getConnectionNodeId(node) }))
.concat(
@@ -50,25 +58,21 @@ export function dedupeConnections(response: ServiceMapResponse) {
}))
);
- const serviceNodes = allNodes.filter(node => SERVICE_NAME in node) as Array<
- ServiceConnectionNode & {
- id: string;
- }
- >;
+ // List of nodes that are services
+ const serviceNodes = allNodes.filter(
+ node => SERVICE_NAME in node
+ ) as ServiceConnectionNode[];
+ // List of nodes that are externals
const externalNodes = allNodes.filter(
node => SPAN_DESTINATION_SERVICE_RESOURCE in node
- ) as Array<
- ExternalConnectionNode & {
- id: string;
- }
- >;
+ ) as ExternalConnectionNode[];
- // 1. maps external nodes to internal services
- // 2. collapses external nodes into one node based on span.destination.service.resource
- // 3. picks the first available span.type/span.subtype in an alphabetically sorted list
+ // 1. Map external nodes to internal services
+ // 2. Collapse external nodes into one node based on span.destination.service.resource
+ // 3. Pick the first available span.type/span.subtype in an alphabetically sorted list
const nodeMap = allNodes.reduce((map, node) => {
- if (map[node.id]) {
+ if (!node.id || map[node.id]) {
return map;
}
@@ -119,14 +123,14 @@ export function dedupeConnections(response: ServiceMapResponse) {
.sort()[0]
}
};
- }, {} as Record);
+ }, {} as Record);
- // maps destination.address to service.name if possible
+ // Map destination.address to service.name if possible
function getConnectionNode(node: ConnectionNode) {
return nodeMap[getConnectionNodeId(node)];
}
- // build connections with mapped nodes
+ // Build connections with mapped nodes
const mappedConnections = connections
.map(connection => {
const sourceData = getConnectionNode(connection.source);
@@ -166,7 +170,7 @@ export function dedupeConnections(response: ServiceMapResponse) {
{} as Record
);
- // instead of adding connections in two directions,
+ // Instead of adding connections in two directions,
// we add a `bidirectional` flag to use in styling
const dedupedConnections = (sortBy(
Object.values(connectionsById),
@@ -192,10 +196,18 @@ export function dedupeConnections(response: ServiceMapResponse) {
return prev.concat(connection);
}, []);
+ // Add anomlies data
+ const dedupedNodesWithAnomliesData = addAnomaliesDataToNodes(
+ dedupedNodes,
+ anomalies
+ );
+
// Put everything together in elements, with everything in the "data" property
- const elements = [...dedupedConnections, ...dedupedNodes].map(element => ({
- data: element
- }));
+ const elements = [...dedupedConnections, ...dedupedNodesWithAnomliesData].map(
+ element => ({
+ data: element
+ })
+ );
return { elements };
}
diff --git a/x-pack/plugins/canvas/common/lib/constants.ts b/x-pack/plugins/canvas/common/lib/constants.ts
index a37dc3fd6a7b3..f2155d9202939 100644
--- a/x-pack/plugins/canvas/common/lib/constants.ts
+++ b/x-pack/plugins/canvas/common/lib/constants.ts
@@ -18,7 +18,7 @@ export const API_ROUTE_WORKPAD_STRUCTURES = `${API_ROUTE}/workpad-structures`;
export const API_ROUTE_CUSTOM_ELEMENT = `${API_ROUTE}/custom-element`;
export const LOCALSTORAGE_PREFIX = `kibana.canvas`;
export const LOCALSTORAGE_CLIPBOARD = `${LOCALSTORAGE_PREFIX}.clipboard`;
-export const LOCALSTORAGE_LASTPAGE = 'canvas:lastpage';
+export const SESSIONSTORAGE_LASTPATH = 'lastPath:canvas';
export const FETCH_TIMEOUT = 30000; // 30 seconds
export const CANVAS_USAGE_TYPE = 'canvas';
export const DEFAULT_WORKPAD_CSS = '.canvasPage {\n\n}';
diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx
index 284023e74d137..9c2aa821be2d5 100644
--- a/x-pack/plugins/canvas/public/application.tsx
+++ b/x-pack/plugins/canvas/public/application.tsx
@@ -10,8 +10,9 @@ import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { Provider } from 'react-redux';
+import { BehaviorSubject } from 'rxjs';
-import { AppMountParameters, CoreStart, CoreSetup } from 'kibana/public';
+import { AppMountParameters, CoreStart, CoreSetup, AppUpdater } from 'kibana/public';
import { CanvasStartDeps, CanvasSetupDeps } from './plugin';
// @ts-ignore Untyped local
@@ -88,9 +89,10 @@ export const initializeCanvas = async (
coreStart: CoreStart,
setupPlugins: CanvasSetupDeps,
startPlugins: CanvasStartDeps,
- registries: SetupRegistries
+ registries: SetupRegistries,
+ appUpdater: BehaviorSubject
) => {
- startServices(coreSetup, coreStart, setupPlugins, startPlugins);
+ startServices(coreSetup, coreStart, setupPlugins, startPlugins, appUpdater);
// Create Store
const canvasStore = await createStore(coreSetup, setupPlugins);
diff --git a/x-pack/plugins/canvas/public/components/app/index.js b/x-pack/plugins/canvas/public/components/app/index.js
index de0d4c190eae6..750132dadb97d 100644
--- a/x-pack/plugins/canvas/public/components/app/index.js
+++ b/x-pack/plugins/canvas/public/components/app/index.js
@@ -8,6 +8,7 @@ import { connect } from 'react-redux';
import { compose, withProps } from 'recompose';
import { getAppReady, getBasePath } from '../../state/selectors/app';
import { appReady, appError } from '../../state/actions/app';
+import { withKibana } from '../../../../../../src/plugins/kibana_react/public';
import { App as Component } from './app';
@@ -44,7 +45,8 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
export const App = compose(
connect(mapStateToProps, mapDispatchToProps, mergeProps),
- withProps(() => ({
- onRouteChange: () => undefined,
+ withKibana,
+ withProps(props => ({
+ onRouteChange: props.kibana.services.canvas.navLink.updatePath,
}))
)(Component);
diff --git a/x-pack/plugins/canvas/public/lib/clipboard.ts b/x-pack/plugins/canvas/public/lib/clipboard.ts
index 11755807aa533..cb940fd064a47 100644
--- a/x-pack/plugins/canvas/public/lib/clipboard.ts
+++ b/x-pack/plugins/canvas/public/lib/clipboard.ts
@@ -4,22 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { LOCALSTORAGE_CLIPBOARD } from '../../common/lib/constants';
-import { getWindow } from './get_window';
-
-let storage: Storage;
-
-const getStorage = (): Storage => {
- if (!storage) {
- storage = new Storage(getWindow().localStorage);
- }
-
- return storage;
-};
+import { getLocalStorage } from './storage';
export const setClipboardData = (data: any) => {
- getStorage().set(LOCALSTORAGE_CLIPBOARD, JSON.stringify(data));
+ getLocalStorage().set(LOCALSTORAGE_CLIPBOARD, JSON.stringify(data));
};
-export const getClipboardData = () => getStorage().get(LOCALSTORAGE_CLIPBOARD);
+export const getClipboardData = () => getLocalStorage().get(LOCALSTORAGE_CLIPBOARD);
diff --git a/x-pack/plugins/canvas/public/lib/get_window.ts b/x-pack/plugins/canvas/public/lib/get_window.ts
index 42c632f4a514f..c8fb035d4d33f 100644
--- a/x-pack/plugins/canvas/public/lib/get_window.ts
+++ b/x-pack/plugins/canvas/public/lib/get_window.ts
@@ -5,10 +5,18 @@
*/
// return window if it exists, otherwise just return an object literal
-const windowObj = { location: null, localStorage: {} as Window['localStorage'] };
+const windowObj = {
+ location: null,
+ localStorage: {} as Window['localStorage'],
+ sessionStorage: {} as Window['sessionStorage'],
+};
export const getWindow = ():
| Window
- | { location: Location | null; localStorage: Window['localStorage'] } => {
+ | {
+ location: Location | null;
+ localStorage: Window['localStorage'];
+ sessionStorage: Window['sessionStorage'];
+ } => {
return typeof window === 'undefined' ? windowObj : window;
};
diff --git a/x-pack/plugins/canvas/public/lib/storage.ts b/x-pack/plugins/canvas/public/lib/storage.ts
new file mode 100644
index 0000000000000..47c8cc741eaf3
--- /dev/null
+++ b/x-pack/plugins/canvas/public/lib/storage.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Storage } from '../../../../../src/plugins/kibana_utils/public';
+import { getWindow } from './get_window';
+
+export enum StorageType {
+ Local = 'localStorage',
+ Session = 'sessionStorage',
+}
+
+const storages: {
+ [x in StorageType]: Storage | null;
+} = {
+ [StorageType.Local]: null,
+ [StorageType.Session]: null,
+};
+
+const getStorage = (type: StorageType): Storage => {
+ const storage = storages[type] || new Storage(getWindow()[type]);
+ storages[type] = storage;
+
+ return storage;
+};
+
+export const getLocalStorage = (): Storage => {
+ return getStorage(StorageType.Local);
+};
+
+export const getSessionStorage = (): Storage => {
+ return getStorage(StorageType.Session);
+};
diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx
index ba57d1475bc4f..c2192818e528b 100644
--- a/x-pack/plugins/canvas/public/plugin.tsx
+++ b/x-pack/plugins/canvas/public/plugin.tsx
@@ -4,15 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { BehaviorSubject } from 'rxjs';
import {
CoreSetup,
CoreStart,
Plugin,
AppMountParameters,
+ AppUpdater,
DEFAULT_APP_CATEGORIES,
} from '../../../../src/core/public';
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import { initLoadingIndicator } from './lib/loading_indicator';
+import { getSessionStorage } from './lib/storage';
+import { SESSIONSTORAGE_LASTPATH } from '../common/lib/constants';
import { featureCatalogueEntry } from './feature_catalogue_entry';
import { ExpressionsSetup, ExpressionsStart } from '../../../../src/plugins/expressions/public';
import { DataPublicPluginSetup } from '../../../../src/plugins/data/public';
@@ -60,6 +64,7 @@ export type CanvasStart = void;
/** @internal */
export class CanvasPlugin
implements Plugin {
+ private appUpdater = new BehaviorSubject(() => ({}));
// TODO: Do we want to completely move canvas_plugin_src into it's own plugin?
private srcPlugin = new CanvasSrcPlugin();
@@ -68,12 +73,21 @@ export class CanvasPlugin
this.srcPlugin.setup(core, { canvas: canvasApi });
+ // Set the nav link to the last saved url if we have one in storage
+ const lastUrl = getSessionStorage().get(SESSIONSTORAGE_LASTPATH);
+ if (lastUrl) {
+ this.appUpdater.next(() => ({
+ defaultPath: `#${lastUrl}`,
+ }));
+ }
+
core.application.register({
category: DEFAULT_APP_CATEGORIES.kibana,
id: 'canvas',
title: 'Canvas',
euiIconType: 'canvasApp',
order: 3000,
+ updater$: this.appUpdater,
mount: async (params: AppMountParameters) => {
// Load application bundle
const { renderApp, initializeCanvas, teardownCanvas } = await import('./application');
@@ -81,7 +95,14 @@ export class CanvasPlugin
// Get start services
const [coreStart, depsStart] = await core.getStartServices();
- const canvasStore = await initializeCanvas(core, coreStart, plugins, depsStart, registries);
+ const canvasStore = await initializeCanvas(
+ core,
+ coreStart,
+ plugins,
+ depsStart,
+ registries,
+ this.appUpdater
+ );
const unmount = renderApp(coreStart, depsStart, params, canvasStore);
diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts
index abc46beaa3e64..42176f953c331 100644
--- a/x-pack/plugins/canvas/public/services/index.ts
+++ b/x-pack/plugins/canvas/public/services/index.ts
@@ -4,16 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { CoreSetup, CoreStart } from '../../../../../src/core/public';
+import { BehaviorSubject } from 'rxjs';
+import { CoreSetup, CoreStart, AppUpdater } from '../../../../../src/core/public';
import { CanvasSetupDeps, CanvasStartDeps } from '../plugin';
import { notifyServiceFactory } from './notify';
import { platformServiceFactory } from './platform';
+import { navLinkServiceFactory } from './nav_link';
export type CanvasServiceFactory = (
coreSetup: CoreSetup,
coreStart: CoreStart,
canvasSetupPlugins: CanvasSetupDeps,
- canvasStartPlugins: CanvasStartDeps
+ canvasStartPlugins: CanvasStartDeps,
+ appUpdater: BehaviorSubject
) => Service;
class CanvasServiceProvider {
@@ -28,9 +31,16 @@ class CanvasServiceProvider {
coreSetup: CoreSetup,
coreStart: CoreStart,
canvasSetupPlugins: CanvasSetupDeps,
- canvasStartPlugins: CanvasStartDeps
+ canvasStartPlugins: CanvasStartDeps,
+ appUpdater: BehaviorSubject
) {
- this.service = this.factory(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins);
+ this.service = this.factory(
+ coreSetup,
+ coreStart,
+ canvasSetupPlugins,
+ canvasStartPlugins,
+ appUpdater
+ );
}
getService(): Service {
@@ -51,20 +61,24 @@ export type ServiceFromProvider = P extends CanvasServiceProvider ?
export const services = {
notify: new CanvasServiceProvider(notifyServiceFactory),
platform: new CanvasServiceProvider(platformServiceFactory),
+ navLink: new CanvasServiceProvider(navLinkServiceFactory),
};
export interface CanvasServices {
notify: ServiceFromProvider;
+ platform: ServiceFromProvider;
+ navLink: ServiceFromProvider;
}
export const startServices = (
coreSetup: CoreSetup,
coreStart: CoreStart,
canvasSetupPlugins: CanvasSetupDeps,
- canvasStartPlugins: CanvasStartDeps
+ canvasStartPlugins: CanvasStartDeps,
+ appUpdater: BehaviorSubject
) => {
Object.entries(services).forEach(([key, provider]) =>
- provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins)
+ provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins, appUpdater)
);
};
@@ -72,4 +86,8 @@ export const stopServices = () => {
Object.entries(services).forEach(([key, provider]) => provider.stop());
};
-export const { notify: notifyService, platform: platformService } = services;
+export const {
+ notify: notifyService,
+ platform: platformService,
+ navLink: navLinkService,
+} = services;
diff --git a/x-pack/plugins/canvas/public/services/nav_link.ts b/x-pack/plugins/canvas/public/services/nav_link.ts
new file mode 100644
index 0000000000000..5061498458006
--- /dev/null
+++ b/x-pack/plugins/canvas/public/services/nav_link.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CanvasServiceFactory } from '.';
+import { SESSIONSTORAGE_LASTPATH } from '../../common/lib/constants';
+import { getSessionStorage } from '../lib/storage';
+
+interface NavLinkService {
+ updatePath: (path: string) => void;
+}
+
+export const navLinkServiceFactory: CanvasServiceFactory = (
+ coreSetup,
+ coreStart,
+ setupPlugins,
+ startPlugins,
+ appUpdater
+) => {
+ return {
+ updatePath: (path: string) => {
+ appUpdater.next(() => ({
+ defaultPath: `#${path}`,
+ }));
+
+ getSessionStorage().set(SESSIONSTORAGE_LASTPATH, path);
+ },
+ };
+};
diff --git a/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.ts b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.ts
index 839379387e094..158641cd97695 100644
--- a/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.ts
+++ b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.ts
@@ -47,6 +47,40 @@ describe('PanelNotificationsAction', () => {
});
});
+ describe('getDisplayNameTooltip', () => {
+ test('returns empty string if embeddable has no event', async () => {
+ const context = createContext();
+ const action = new PanelNotificationsAction();
+
+ const name = await action.getDisplayNameTooltip(context);
+ expect(name).toBe('');
+ });
+
+ test('returns "1 drilldown" if embeddable has one event', async () => {
+ const context = createContext([{}]);
+ const action = new PanelNotificationsAction();
+
+ const name = await action.getDisplayNameTooltip(context);
+ expect(name).toBe('Panel has 1 drilldown');
+ });
+
+ test('returns "2 drilldowns" if embeddable has two events', async () => {
+ const context = createContext([{}, {}]);
+ const action = new PanelNotificationsAction();
+
+ const name = await action.getDisplayNameTooltip(context);
+ expect(name).toBe('Panel has 2 drilldowns');
+ });
+
+ test('returns "3 drilldowns" if embeddable has three events', async () => {
+ const context = createContext([{}, {}, {}]);
+ const action = new PanelNotificationsAction();
+
+ const name = await action.getDisplayNameTooltip(context);
+ expect(name).toBe('Panel has 3 drilldowns');
+ });
+ });
+
describe('isCompatible', () => {
test('returns false if not in "edit" mode', async () => {
const context = createContext([{}]);
diff --git a/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts
index 19e0ac2a5a6d8..165ce24c13ea3 100644
--- a/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts
+++ b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts
@@ -4,10 +4,26 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { i18n } from '@kbn/i18n';
import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public';
import { ViewMode } from '../../../../../src/plugins/embeddable/public';
import { EnhancedEmbeddableContext, EnhancedEmbeddable } from '../types';
+export const txtOneDrilldown = i18n.translate(
+ 'xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown',
+ {
+ defaultMessage: 'Panel has 1 drilldown',
+ }
+);
+
+export const txtManyDrilldowns = (count: number) =>
+ i18n.translate('xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns', {
+ defaultMessage: 'Panel has {count} drilldowns',
+ values: {
+ count: String(count),
+ },
+ });
+
export const ACTION_PANEL_NOTIFICATIONS = 'ACTION_PANEL_NOTIFICATIONS';
/**
@@ -25,6 +41,11 @@ export class PanelNotificationsAction implements ActionDefinition {
+ const count = this.getEventCount(embeddable);
+ return !count ? '' : count === 1 ? txtOneDrilldown : txtManyDrilldowns(count);
+ };
+
public readonly isCompatible = async ({ embeddable }: EnhancedEmbeddableContext) => {
if (embeddable.getInput().viewMode !== ViewMode.EDIT) return false;
return this.getEventCount(embeddable) > 0;
diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts
index 9e7aedcc90bb5..ff8add42a5085 100644
--- a/x-pack/plugins/endpoint/common/generate_data.ts
+++ b/x-pack/plugins/endpoint/common/generate_data.ts
@@ -560,7 +560,7 @@ export class EndpointDocGenerator {
applied: {
actions: {
configure_elasticsearch_connection: {
- message: 'elasticsearch comes configured successfully',
+ message: 'elasticsearch communications configured successfully',
status: HostPolicyResponseActionStatus.success,
},
configure_kernel: {
diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts
index 181b0e7ab3884..b39b2e89ee150 100644
--- a/x-pack/plugins/endpoint/common/types.ts
+++ b/x-pack/plugins/endpoint/common/types.ts
@@ -644,6 +644,9 @@ export interface HostPolicyResponseActions {
read_malware_config: HostPolicyResponseActionDetails;
}
+/**
+ * policy configurations returned by the endpoint in response to a user applying a policy
+ */
export type HostPolicyResponseConfiguration = HostPolicyResponse['endpoint']['policy']['applied']['response']['configurations'];
interface HostPolicyResponseConfigurationStatus {
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx
index aa04f2fdff57f..8714141364e7d 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx
@@ -12,7 +12,6 @@ import {
HostPolicyResponseActions,
HostPolicyResponseConfiguration,
Immutable,
- ImmutableArray,
} from '../../../../../../common/types';
import { formatResponse } from './policy_response_friendly_names';
import { POLICY_STATUS_TO_HEALTH_COLOR } from './host_constants';
@@ -51,7 +50,7 @@ const ResponseActions = memo(
actions,
actionStatus,
}: {
- actions: ImmutableArray;
+ actions: Immutable>;
actionStatus: Partial;
}) => {
return (
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response_friendly_names.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response_friendly_names.ts
index 251b3e86bc3f9..502aa66b24421 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response_friendly_names.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response_friendly_names.ts
@@ -159,8 +159,7 @@ responseMap.set(
);
/**
- * Takes in the snake-cased response from the API and
- * removes the underscores and capitalizes the string.
+ * Maps a server provided value to corresponding i18n'd string.
*/
export function formatResponse(responseString: string) {
if (responseMap.has(responseString)) {
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx
index a600d59865ccc..77147d1b3b2b7 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx
@@ -118,10 +118,7 @@ export const ExpressionChart: React.FC = ({
const series = {
...firstSeries,
rows: firstSeries.rows.map(row => {
- const newRow: MetricsExplorerRow = {
- timestamp: row.timestamp,
- metric_0: row.metric_0 || null,
- };
+ const newRow: MetricsExplorerRow = { ...row };
thresholds.forEach((thresholdValue, index) => {
newRow[`metric_threshold_${index}`] = thresholdValue;
});
@@ -224,7 +221,7 @@ export const ExpressionChart: React.FC = ({
/>
>
) : null}
- {isAbove ? (
+ {isAbove && first(expression.threshold) != null ? (
`metric_${i}`).slice(id.length - 1, id.length)
- : [`metric_${id}`];
+ ? id.map(i => getMetricId(metric, i)).slice(id.length - 1, id.length)
+ : [getMetricId(metric, id)];
const y0Accessors =
- Array.isArray(id) && id.length > 1 ? id.map(i => `metric_${i}`).slice(0, 1) : undefined;
+ Array.isArray(id) && id.length > 1
+ ? id.map(i => getMetricId(metric, i)).slice(0, 1)
+ : undefined;
const chartId = `series-${series.id}-${yAccessors.join('-')}`;
const seriesAreaStyle: RecursivePartial = {
@@ -85,8 +88,10 @@ export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) =>
(metric.color && colorTransformer(metric.color)) ||
colorTransformer(MetricsExplorerColor.color0);
- const yAccessor = `metric_${id}`;
- const chartId = `series-${series.id}-${yAccessor}`;
+ const yAccessors = Array.isArray(id)
+ ? id.map(i => getMetricId(metric, i)).slice(id.length - 1, id.length)
+ : [getMetricId(metric, id)];
+ const chartId = `series-${series.id}-${yAccessors.join('-')}`;
const seriesBarStyle: RecursivePartial = {
rectBorder: {
@@ -100,13 +105,13 @@ export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) =>
};
return (
{
+ const value = type === Aggregators.P95 ? 95 : 99;
+ return {
+ aggregatedValue: {
+ percentiles: {
+ field,
+ percents: [value],
+ keyed: false,
+ },
+ },
+ };
+};
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
index 2531e939792af..ed5efc1473953 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
@@ -233,6 +233,58 @@ describe('The metric threshold alert type', () => {
expect(getState(instanceID).alertState).toBe(AlertStates.OK);
});
});
+ describe('querying with the p99 aggregator', () => {
+ const instanceID = 'test-*';
+ const execute = (comparator: Comparator, threshold: number[]) =>
+ executor({
+ services,
+ params: {
+ criteria: [
+ {
+ ...baseCriterion,
+ comparator,
+ threshold,
+ aggType: 'p99',
+ metric: 'test.metric.2',
+ },
+ ],
+ },
+ });
+ test('alerts based on the p99 values', async () => {
+ await execute(Comparator.GT, [1]);
+ expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id);
+ expect(getState(instanceID).alertState).toBe(AlertStates.ALERT);
+ await execute(Comparator.LT, [1]);
+ expect(mostRecentAction(instanceID)).toBe(undefined);
+ expect(getState(instanceID).alertState).toBe(AlertStates.OK);
+ });
+ });
+ describe('querying with the p95 aggregator', () => {
+ const instanceID = 'test-*';
+ const execute = (comparator: Comparator, threshold: number[]) =>
+ executor({
+ services,
+ params: {
+ criteria: [
+ {
+ ...baseCriterion,
+ comparator,
+ threshold,
+ aggType: 'p95',
+ metric: 'test.metric.1',
+ },
+ ],
+ },
+ });
+ test('alerts based on the p95 values', async () => {
+ await execute(Comparator.GT, [0.25]);
+ expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id);
+ expect(getState(instanceID).alertState).toBe(AlertStates.ALERT);
+ await execute(Comparator.LT, [0.95]);
+ expect(mostRecentAction(instanceID)).toBe(undefined);
+ expect(getState(instanceID).alertState).toBe(AlertStates.OK);
+ });
+ });
describe("querying a metric that hasn't reported data", () => {
const instanceID = 'test-*';
const execute = (alertOnNoData: boolean) =>
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
index ec9389537835b..71bee3209bf53 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { mapValues } from 'lodash';
+import { mapValues, first } from 'lodash';
import { i18n } from '@kbn/i18n';
import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_types';
import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler';
@@ -21,12 +21,16 @@ import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/ser
import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
import { getDateHistogramOffset } from '../../snapshot/query_helpers';
import { InfraBackendLibs } from '../../infra_types';
+import { createPercentileAggregation } from './create_percentile_aggregation';
const TOTAL_BUCKETS = 5;
interface Aggregation {
aggregatedIntervals: {
- buckets: Array<{ aggregatedValue: { value: number }; doc_count: number }>;
+ buckets: Array<{
+ aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> };
+ doc_count: number;
+ }>;
};
}
@@ -47,6 +51,12 @@ const getCurrentValueFromAggregations = (
if (aggType === Aggregators.COUNT) {
return mostRecentBucket.doc_count;
}
+ if (aggType === Aggregators.P95 || aggType === Aggregators.P99) {
+ const values = mostRecentBucket.aggregatedValue?.values || [];
+ const firstValue = first(values);
+ if (!firstValue) return null;
+ return firstValue.value;
+ }
const { value } = mostRecentBucket.aggregatedValue;
return value;
} catch (e) {
@@ -86,6 +96,8 @@ export const getElasticsearchMetricQuery = (
? {}
: aggType === Aggregators.RATE
? networkTraffic('aggregatedValue', metric)
+ : aggType === Aggregators.P95 || aggType === Aggregators.P99
+ ? createPercentileAggregation(aggType, metric)
: {
aggregatedValue: {
[aggType]: {
@@ -275,7 +287,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s
);
// Because each alert result has the same group definitions, just grap the groups from the first one.
- const groups = Object.keys(alertResults[0]);
+ const groups = Object.keys(first(alertResults));
for (const group of groups) {
const alertInstance = services.alertInstanceFactory(`${alertId}-${group}`);
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
index fa55f80e472de..25b709d6afc51 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
@@ -7,22 +7,22 @@
const bucketsA = [
{
doc_count: 2,
- aggregatedValue: { value: 0.5 },
+ aggregatedValue: { value: 0.5, values: [{ key: 95.0, value: 0.5 }] },
},
{
doc_count: 3,
- aggregatedValue: { value: 1.0 },
+ aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] },
},
];
const bucketsB = [
{
doc_count: 4,
- aggregatedValue: { value: 2.5 },
+ aggregatedValue: { value: 2.5, values: [{ key: 99.0, value: 2.5 }] },
},
{
doc_count: 5,
- aggregatedValue: { value: 3.5 },
+ aggregatedValue: { value: 3.5, values: [{ key: 99.0, value: 3.5 }] },
},
];
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts
index 18f5503fe2c9e..76ddd107bd728 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts
@@ -23,6 +23,8 @@ export enum Aggregators {
MAX = 'max',
RATE = 'rate',
CARDINALITY = 'cardinality',
+ P95 = 'p95',
+ P99 = 'p99',
}
export enum AlertStates {
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts
index f6db5dfe353ea..6cdcb8782f38e 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts
@@ -372,12 +372,11 @@ export const ensureDefaultIndices = async (callCluster: CallESAsCurrentUser) =>
Promise.all(
Object.keys(IndexPatternType).map(async indexPattern => {
const defaultIndexPatternName = indexPattern + INDEX_PATTERN_PLACEHOLDER_SUFFIX;
- const indexExists = await doesIndexExist(defaultIndexPatternName, callCluster);
+ const indexExists = await callCluster('indices.exists', { index: defaultIndexPatternName });
if (!indexExists) {
try {
- await callCluster('transport.request', {
- method: 'PUT',
- path: `/${defaultIndexPatternName}`,
+ await callCluster('indices.create', {
+ index: defaultIndexPatternName,
body: {
mappings: {
properties: {
@@ -387,20 +386,9 @@ export const ensureDefaultIndices = async (callCluster: CallESAsCurrentUser) =>
},
});
} catch (putErr) {
- throw new Error(`${defaultIndexPatternName} could not be created`);
+ // throw new Error(`${defaultIndexPatternName} could not be created`);
+ throw new Error(putErr);
}
}
})
);
-
-export const doesIndexExist = async (indexName: string, callCluster: CallESAsCurrentUser) => {
- try {
- await callCluster('transport.request', {
- method: 'HEAD',
- path: indexName,
- });
- return true;
- } catch (err) {
- return false;
- }
-};
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts
new file mode 100644
index 0000000000000..3a2ee7ef8b008
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import sinon, { SinonFakeServer } from 'sinon';
+
+import { API_BASE_PATH } from '../../../common/constants';
+
+// Register helpers to mock HTTP Requests
+const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
+ const setLoadPipelinesResponse = (response?: any[], error?: any) => {
+ const status = error ? error.status || 400 : 200;
+ const body = error ? error.body : response;
+
+ server.respondWith('GET', API_BASE_PATH, [
+ status,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(body),
+ ]);
+ };
+
+ const setLoadPipelineResponse = (response?: {}, error?: any) => {
+ const status = error ? error.status || 400 : 200;
+ const body = error ? error.body : response;
+
+ server.respondWith('GET', `${API_BASE_PATH}/:name`, [
+ status,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(body),
+ ]);
+ };
+
+ const setDeletePipelineResponse = (response?: object) => {
+ server.respondWith('DELETE', `${API_BASE_PATH}/:name`, [
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(response),
+ ]);
+ };
+
+ const setCreatePipelineResponse = (response?: object, error?: any) => {
+ const status = error ? error.status || 400 : 200;
+ const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
+
+ server.respondWith('POST', API_BASE_PATH, [
+ status,
+ { 'Content-Type': 'application/json' },
+ body,
+ ]);
+ };
+
+ return {
+ setLoadPipelinesResponse,
+ setLoadPipelineResponse,
+ setDeletePipelineResponse,
+ setCreatePipelineResponse,
+ };
+};
+
+export const init = () => {
+ const server = sinon.fakeServer.create();
+ server.respondImmediately = true;
+
+ // Define default response for unhandled requests.
+ // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry,
+ // and we can mock them all with a 200 instead of mocking each one individually.
+ server.respondWith([200, {}, 'DefaultMockedResponse']);
+
+ const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server);
+
+ return {
+ server,
+ httpRequestsMockHelpers,
+ };
+};
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts
new file mode 100644
index 0000000000000..6216119c5d1d1
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { setup as pipelinesListSetup } from './pipelines_list.helpers';
+import { setup as pipelinesCreateSetup } from './pipelines_create.helpers';
+import { setup as pipelinesCloneSetup } from './pipelines_clone.helpers';
+import { setup as pipelinesEditSetup } from './pipelines_edit.helpers';
+
+export { nextTick, getRandomString, findTestSubject } from '../../../../../test_utils';
+
+export { setupEnvironment } from './setup_environment';
+
+export const pageHelpers = {
+ pipelinesList: { setup: pipelinesListSetup },
+ pipelinesCreate: { setup: pipelinesCreateSetup },
+ pipelinesClone: { setup: pipelinesCloneSetup },
+ pipelinesEdit: { setup: pipelinesEditSetup },
+};
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts
new file mode 100644
index 0000000000000..d56e92a2419c4
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { TestBed } from '../../../../../test_utils';
+
+export const getFormActions = (testBed: TestBed) => {
+ const { find, form } = testBed;
+
+ // User actions
+ const clickSubmitButton = () => {
+ find('submitButton').simulate('click');
+ };
+
+ const clickTestPipelineButton = () => {
+ find('testPipelineButton').simulate('click');
+ };
+
+ const clickShowRequestLink = () => {
+ find('showRequestLink').simulate('click');
+ };
+
+ const toggleVersionSwitch = () => {
+ form.toggleEuiSwitch('versionToggle');
+ };
+
+ const toggleOnFailureSwitch = () => {
+ form.toggleEuiSwitch('onFailureToggle');
+ };
+
+ return {
+ clickSubmitButton,
+ clickShowRequestLink,
+ toggleVersionSwitch,
+ toggleOnFailureSwitch,
+ clickTestPipelineButton,
+ };
+};
+
+export type PipelineFormTestSubjects =
+ | 'submitButton'
+ | 'pageTitle'
+ | 'savePipelineError'
+ | 'pipelineForm'
+ | 'versionToggle'
+ | 'versionField'
+ | 'nameField.input'
+ | 'descriptionField.input'
+ | 'processorsField'
+ | 'onFailureToggle'
+ | 'onFailureEditor'
+ | 'testPipelineButton'
+ | 'showRequestLink'
+ | 'requestFlyout'
+ | 'requestFlyout.title'
+ | 'testPipelineFlyout'
+ | 'testPipelineFlyout.title'
+ | 'documentationLink';
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts
new file mode 100644
index 0000000000000..2791ffc32c858
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { registerTestBed, TestBedConfig, TestBed } from '../../../../../test_utils';
+import { BASE_PATH } from '../../../common/constants';
+import { PipelinesClone } from '../../../public/application/sections/pipelines_clone'; // eslint-disable-line @kbn/eslint/no-restricted-paths
+import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers';
+import { WithAppDependencies } from './setup_environment';
+
+export type PipelinesCloneTestBed = TestBed & {
+ actions: ReturnType;
+};
+
+export const PIPELINE_TO_CLONE = {
+ name: 'my_pipeline',
+ description: 'pipeline description',
+ processors: [
+ {
+ set: {
+ field: 'foo',
+ value: 'new',
+ },
+ },
+ ],
+};
+
+const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: [`${BASE_PATH}create/${PIPELINE_TO_CLONE.name}`],
+ componentRoutePath: `${BASE_PATH}create/:name`,
+ },
+ doMountAsync: true,
+};
+
+const initTestBed = registerTestBed(WithAppDependencies(PipelinesClone), testBedConfig);
+
+export const setup = async (): Promise => {
+ const testBed = await initTestBed();
+
+ return {
+ ...testBed,
+ actions: getFormActions(testBed),
+ };
+};
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts
new file mode 100644
index 0000000000000..54a62a8357e52
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { registerTestBed, TestBedConfig, TestBed } from '../../../../../test_utils';
+import { BASE_PATH } from '../../../common/constants';
+import { PipelinesCreate } from '../../../public/application/sections/pipelines_create'; // eslint-disable-line @kbn/eslint/no-restricted-paths
+import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers';
+import { WithAppDependencies } from './setup_environment';
+
+export type PipelinesCreateTestBed = TestBed & {
+ actions: ReturnType;
+};
+
+const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: [`${BASE_PATH}/create`],
+ componentRoutePath: `${BASE_PATH}/create`,
+ },
+ doMountAsync: true,
+};
+
+const initTestBed = registerTestBed(WithAppDependencies(PipelinesCreate), testBedConfig);
+
+export const setup = async (): Promise => {
+ const testBed = await initTestBed();
+
+ return {
+ ...testBed,
+ actions: getFormActions(testBed),
+ };
+};
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts
new file mode 100644
index 0000000000000..12320f034a819
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { registerTestBed, TestBedConfig, TestBed } from '../../../../../test_utils';
+import { BASE_PATH } from '../../../common/constants';
+import { PipelinesEdit } from '../../../public/application/sections/pipelines_edit'; // eslint-disable-line @kbn/eslint/no-restricted-paths
+import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers';
+import { WithAppDependencies } from './setup_environment';
+
+export type PipelinesEditTestBed = TestBed & {
+ actions: ReturnType;
+};
+
+export const PIPELINE_TO_EDIT = {
+ name: 'my_pipeline',
+ description: 'pipeline description',
+ processors: [
+ {
+ set: {
+ field: 'foo',
+ value: 'new',
+ },
+ },
+ ],
+};
+
+const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: [`${BASE_PATH}edit/${PIPELINE_TO_EDIT.name}`],
+ componentRoutePath: `${BASE_PATH}edit/:name`,
+ },
+ doMountAsync: true,
+};
+
+const initTestBed = registerTestBed(WithAppDependencies(PipelinesEdit), testBedConfig);
+
+export const setup = async (): Promise => {
+ const testBed = await initTestBed();
+
+ return {
+ ...testBed,
+ actions: getFormActions(testBed),
+ };
+};
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts
new file mode 100644
index 0000000000000..0f9745981c18b
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { act } from 'react-dom/test-utils';
+
+import { BASE_PATH } from '../../../common/constants';
+import {
+ registerTestBed,
+ TestBed,
+ TestBedConfig,
+ findTestSubject,
+ nextTick,
+} from '../../../../../test_utils';
+import { PipelinesList } from '../../../public/application/sections/pipelines_list';
+import { WithAppDependencies } from './setup_environment';
+
+const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: [BASE_PATH],
+ componentRoutePath: BASE_PATH,
+ },
+ doMountAsync: true,
+};
+
+const initTestBed = registerTestBed(WithAppDependencies(PipelinesList), testBedConfig);
+
+export type PipelineListTestBed = TestBed & {
+ actions: ReturnType;
+};
+
+const createActions = (testBed: TestBed) => {
+ const { find } = testBed;
+
+ /**
+ * User Actions
+ */
+ const clickReloadButton = () => {
+ find('reloadButton').simulate('click');
+ };
+
+ const clickPipelineAt = async (index: number) => {
+ const { component, table, router } = testBed;
+ const { rows } = table.getMetaData('pipelinesTable');
+ const pipelineLink = findTestSubject(rows[index].reactWrapper, 'pipelineDetailsLink');
+
+ await act(async () => {
+ const { href } = pipelineLink.props();
+ router.navigateTo(href!);
+ await nextTick();
+ component.update();
+ });
+ };
+
+ const clickActionMenu = (pipelineName: string) => {
+ const { component } = testBed;
+
+ // When a table has > 2 actions, EUI displays an overflow menu with an id "-actions"
+ component.find(`div[id="${pipelineName}-actions"] button`).simulate('click');
+ };
+
+ const clickPipelineAction = (pipelineName: string, action: 'edit' | 'clone' | 'delete') => {
+ const actions = ['edit', 'clone', 'delete'];
+ const { component } = testBed;
+
+ clickActionMenu(pipelineName);
+
+ component
+ .find('.euiContextMenuItem')
+ .at(actions.indexOf(action))
+ .simulate('click');
+ };
+
+ return {
+ clickReloadButton,
+ clickPipelineAt,
+ clickPipelineAction,
+ clickActionMenu,
+ };
+};
+
+export const setup = async (): Promise => {
+ const testBed = await initTestBed();
+
+ return {
+ ...testBed,
+ actions: createActions(testBed),
+ };
+};
+
+export type PipelineListTestSubjects =
+ | 'appTitle'
+ | 'documentationLink'
+ | 'createPipelineButton'
+ | 'pipelinesTable'
+ | 'pipelineDetails'
+ | 'pipelineDetails.title'
+ | 'deletePipelinesConfirmation'
+ | 'emptyList'
+ | 'emptyList.title'
+ | 'sectionLoading'
+ | 'pipelineLoadError'
+ | 'reloadButton';
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx
new file mode 100644
index 0000000000000..3243d665832f2
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+/* eslint-disable @kbn/eslint/no-restricted-paths */
+import React from 'react';
+
+import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
+import {
+ notificationServiceMock,
+ fatalErrorsServiceMock,
+ docLinksServiceMock,
+ injectedMetadataServiceMock,
+} from '../../../../../../src/core/public/mocks';
+
+import { usageCollectionPluginMock } from '../../../../../../src/plugins/usage_collection/public/mocks';
+
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { HttpService } from '../../../../../../src/core/public/http';
+
+import {
+ breadcrumbService,
+ documentationService,
+ uiMetricService,
+ apiService,
+} from '../../../public/application/services';
+
+import { init as initHttpRequests } from './http_requests';
+
+const httpServiceSetupMock = new HttpService().setup({
+ injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
+ fatalErrors: fatalErrorsServiceMock.createSetupContract(),
+});
+
+const appServices = {
+ breadcrumbs: breadcrumbService,
+ metric: uiMetricService,
+ documentation: documentationService,
+ api: apiService,
+ notifications: notificationServiceMock.createSetupContract(),
+};
+
+export const setupEnvironment = () => {
+ uiMetricService.setup(usageCollectionPluginMock.createSetupContract());
+ apiService.setup(httpServiceSetupMock, uiMetricService);
+ documentationService.setup(docLinksServiceMock.createStartContract());
+ breadcrumbService.setup(() => {});
+
+ const { server, httpRequestsMockHelpers } = initHttpRequests();
+
+ return {
+ server,
+ httpRequestsMockHelpers,
+ };
+};
+
+export const WithAppDependencies = (Comp: any) => (props: any) => (
+
+
+
+);
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_clone.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_clone.test.tsx
new file mode 100644
index 0000000000000..2901367892213
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_clone.test.tsx
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import { setupEnvironment, pageHelpers } from './helpers';
+import { PIPELINE_TO_CLONE, PipelinesCloneTestBed } from './helpers/pipelines_clone.helpers';
+
+const { setup } = pageHelpers.pipelinesClone;
+
+jest.mock('@elastic/eui', () => ({
+ ...jest.requireActual('@elastic/eui'),
+ // Mocking EuiCodeEditor, which uses React Ace under the hood
+ EuiCodeEditor: (props: any) => (
+ {
+ props.onChange(syntheticEvent.jsonString);
+ }}
+ />
+ ),
+}));
+
+describe('', () => {
+ let testBed: PipelinesCloneTestBed;
+
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPipelineResponse(PIPELINE_TO_CLONE);
+
+ await act(async () => {
+ testBed = await setup();
+ await testBed.waitFor('pipelineForm');
+ });
+ });
+
+ test('should render the correct page header', () => {
+ const { exists, find } = testBed;
+
+ // Verify page title
+ expect(exists('pageTitle')).toBe(true);
+ expect(find('pageTitle').text()).toEqual('Create pipeline');
+
+ // Verify documentation link
+ expect(exists('documentationLink')).toBe(true);
+ expect(find('documentationLink').text()).toBe('Create pipeline docs');
+ });
+
+ describe('form submission', () => {
+ it('should send the correct payload', async () => {
+ const { actions, waitFor } = testBed;
+
+ await act(async () => {
+ actions.clickSubmitButton();
+ await waitFor('pipelineForm', 0);
+ });
+
+ const latestRequest = server.requests[server.requests.length - 1];
+
+ const expected = {
+ ...PIPELINE_TO_CLONE,
+ name: `${PIPELINE_TO_CLONE.name}-copy`,
+ };
+
+ expect(JSON.parse(latestRequest.requestBody)).toEqual(expected);
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx
new file mode 100644
index 0000000000000..e0be8d2937729
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx
@@ -0,0 +1,208 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import { setupEnvironment, pageHelpers, nextTick } from './helpers';
+import { PipelinesCreateTestBed } from './helpers/pipelines_create.helpers';
+
+const { setup } = pageHelpers.pipelinesCreate;
+
+jest.mock('@elastic/eui', () => ({
+ ...jest.requireActual('@elastic/eui'),
+ // Mocking EuiCodeEditor, which uses React Ace under the hood
+ EuiCodeEditor: (props: any) => (
+ {
+ props.onChange(syntheticEvent.jsonString);
+ }}
+ />
+ ),
+}));
+
+describe('', () => {
+ let testBed: PipelinesCreateTestBed;
+
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ describe('on component mount', () => {
+ beforeEach(async () => {
+ await act(async () => {
+ testBed = await setup();
+ await testBed.waitFor('pipelineForm');
+ });
+ });
+
+ test('should render the correct page header', () => {
+ const { exists, find } = testBed;
+
+ // Verify page title
+ expect(exists('pageTitle')).toBe(true);
+ expect(find('pageTitle').text()).toEqual('Create pipeline');
+
+ // Verify documentation link
+ expect(exists('documentationLink')).toBe(true);
+ expect(find('documentationLink').text()).toBe('Create pipeline docs');
+ });
+
+ test('should toggle the version field', async () => {
+ const { actions, component, exists } = testBed;
+
+ // Version field should be hidden by default
+ expect(exists('versionField')).toBe(false);
+
+ await act(async () => {
+ actions.toggleVersionSwitch();
+ await nextTick();
+ component.update();
+ });
+
+ expect(exists('versionField')).toBe(true);
+ });
+
+ test('should toggle the on-failure processors editor', async () => {
+ const { actions, component, exists } = testBed;
+
+ // On-failure editor should be hidden by default
+ expect(exists('onFailureEditor')).toBe(false);
+
+ await act(async () => {
+ actions.toggleOnFailureSwitch();
+ await nextTick();
+ component.update();
+ });
+
+ expect(exists('onFailureEditor')).toBe(true);
+ });
+
+ test('should show the request flyout', async () => {
+ const { actions, component, find, exists } = testBed;
+
+ await act(async () => {
+ actions.clickShowRequestLink();
+ await nextTick();
+ component.update();
+ });
+
+ // Verify request flyout opens
+ expect(exists('requestFlyout')).toBe(true);
+ expect(find('requestFlyout.title').text()).toBe('Request');
+ });
+
+ describe('form validation', () => {
+ test('should prevent form submission if required fields are missing', async () => {
+ const { form, actions, component, find } = testBed;
+
+ await act(async () => {
+ actions.clickSubmitButton();
+ await nextTick();
+ component.update();
+ });
+
+ expect(form.getErrorsMessages()).toEqual([
+ 'Name is required.',
+ 'A description is required.',
+ ]);
+ expect(find('submitButton').props().disabled).toEqual(true);
+
+ // Add required fields and verify button is enabled again
+ form.setInputValue('nameField.input', 'my_pipeline');
+ form.setInputValue('descriptionField.input', 'pipeline description');
+
+ await act(async () => {
+ await nextTick();
+ component.update();
+ });
+
+ expect(find('submitButton').props().disabled).toEqual(false);
+ });
+ });
+
+ describe('form submission', () => {
+ beforeEach(async () => {
+ await act(async () => {
+ testBed = await setup();
+
+ const { waitFor, form } = testBed;
+
+ await waitFor('pipelineForm');
+
+ form.setInputValue('nameField.input', 'my_pipeline');
+ form.setInputValue('descriptionField.input', 'pipeline description');
+ });
+ });
+
+ test('should send the correct payload', async () => {
+ const { actions, waitFor } = testBed;
+
+ await act(async () => {
+ actions.clickSubmitButton();
+ await waitFor('pipelineForm', 0);
+ });
+
+ const latestRequest = server.requests[server.requests.length - 1];
+
+ const expected = {
+ name: 'my_pipeline',
+ description: 'pipeline description',
+ processors: [],
+ };
+
+ expect(JSON.parse(latestRequest.requestBody)).toEqual(expected);
+ });
+
+ test('should surface API errors from the request', async () => {
+ const { actions, find, exists, waitFor } = testBed;
+
+ const error = {
+ status: 409,
+ error: 'Conflict',
+ message: `There is already a pipeline with name 'my_pipeline'.`,
+ };
+
+ httpRequestsMockHelpers.setCreatePipelineResponse(undefined, { body: error });
+
+ await act(async () => {
+ actions.clickSubmitButton();
+ await waitFor('savePipelineError');
+ });
+
+ expect(exists('savePipelineError')).toBe(true);
+ expect(find('savePipelineError').text()).toContain(error.message);
+ });
+ });
+
+ describe('test pipeline', () => {
+ beforeEach(async () => {
+ await act(async () => {
+ testBed = await setup();
+
+ const { waitFor } = testBed;
+
+ await waitFor('pipelineForm');
+ });
+ });
+
+ test('should open the test pipeline flyout', async () => {
+ const { actions, exists, find, waitFor } = testBed;
+
+ await act(async () => {
+ actions.clickTestPipelineButton();
+ await waitFor('testPipelineFlyout');
+ });
+
+ // Verify test pipeline flyout opens
+ expect(exists('testPipelineFlyout')).toBe(true);
+ expect(find('testPipelineFlyout.title').text()).toBe('Test pipeline');
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_edit.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_edit.test.tsx
new file mode 100644
index 0000000000000..477eec83f876d
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_edit.test.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import { setupEnvironment, pageHelpers } from './helpers';
+import { PIPELINE_TO_EDIT, PipelinesEditTestBed } from './helpers/pipelines_edit.helpers';
+
+const { setup } = pageHelpers.pipelinesEdit;
+
+jest.mock('@elastic/eui', () => ({
+ ...jest.requireActual('@elastic/eui'),
+ // Mocking EuiCodeEditor, which uses React Ace under the hood
+ EuiCodeEditor: (props: any) => (
+ {
+ props.onChange(syntheticEvent.jsonString);
+ }}
+ />
+ ),
+}));
+
+describe('', () => {
+ let testBed: PipelinesEditTestBed;
+
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPipelineResponse(PIPELINE_TO_EDIT);
+
+ await act(async () => {
+ testBed = await setup();
+ await testBed.waitFor('pipelineForm');
+ });
+ });
+
+ test('should render the correct page header', () => {
+ const { exists, find } = testBed;
+
+ // Verify page title
+ expect(exists('pageTitle')).toBe(true);
+ expect(find('pageTitle').text()).toEqual(`Edit pipeline '${PIPELINE_TO_EDIT.name}'`);
+
+ // Verify documentation link
+ expect(exists('documentationLink')).toBe(true);
+ expect(find('documentationLink').text()).toBe('Edit pipeline docs');
+ });
+
+ it('should disable the name field', () => {
+ const { find } = testBed;
+
+ const nameInput = find('nameField.input');
+ expect(nameInput.props().disabled).toEqual(true);
+ });
+
+ describe('form submission', () => {
+ it('should send the correct payload with changed values', async () => {
+ const UPDATED_DESCRIPTION = 'updated pipeline description';
+ const { actions, form, waitFor } = testBed;
+
+ // Make change to description field
+ form.setInputValue('descriptionField.input', UPDATED_DESCRIPTION);
+
+ await act(async () => {
+ actions.clickSubmitButton();
+ await waitFor('pipelineForm', 0);
+ });
+
+ const latestRequest = server.requests[server.requests.length - 1];
+
+ const { name, ...pipelineDefinition } = PIPELINE_TO_EDIT;
+
+ const expected = {
+ ...pipelineDefinition,
+ description: UPDATED_DESCRIPTION,
+ };
+
+ expect(JSON.parse(latestRequest.requestBody)).toEqual(expected);
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
new file mode 100644
index 0000000000000..3e0b78d4f2e9d
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
@@ -0,0 +1,186 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { act } from 'react-dom/test-utils';
+
+import { API_BASE_PATH } from '../../common/constants';
+
+import { setupEnvironment, pageHelpers, nextTick } from './helpers';
+import { PipelineListTestBed } from './helpers/pipelines_list.helpers';
+
+const { setup } = pageHelpers.pipelinesList;
+
+jest.mock('ui/i18n', () => {
+ const I18nContext = ({ children }: any) => children;
+ return { I18nContext };
+});
+
+describe('', () => {
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+ let testBed: PipelineListTestBed;
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ describe('With pipelines', () => {
+ const pipeline1 = {
+ name: 'test_pipeline1',
+ description: 'test_pipeline1 description',
+ processors: [],
+ };
+
+ const pipeline2 = {
+ name: 'test_pipeline2',
+ description: 'test_pipeline2 description',
+ processors: [],
+ };
+
+ const pipelines = [pipeline1, pipeline2];
+
+ httpRequestsMockHelpers.setLoadPipelinesResponse(pipelines);
+
+ beforeEach(async () => {
+ testBed = await setup();
+
+ await act(async () => {
+ const { waitFor } = testBed;
+
+ await waitFor('pipelinesTable');
+ });
+ });
+
+ test('should render the list view', async () => {
+ const { exists, find, table } = testBed;
+
+ // Verify app title
+ expect(exists('appTitle')).toBe(true);
+ expect(find('appTitle').text()).toEqual('Ingest Node Pipelines');
+
+ // Verify documentation link
+ expect(exists('documentationLink')).toBe(true);
+ expect(find('documentationLink').text()).toBe('Ingest Node Pipelines docs');
+
+ // Verify create button exists
+ expect(exists('createPipelineButton')).toBe(true);
+
+ // Verify table content
+ const { tableCellsValues } = table.getMetaData('pipelinesTable');
+ tableCellsValues.forEach((row, i) => {
+ const pipeline = pipelines[i];
+
+ expect(row).toEqual(['', pipeline.name, '']);
+ });
+ });
+
+ test('should reload the pipeline data', async () => {
+ const { component, actions } = testBed;
+ const totalRequests = server.requests.length;
+
+ await act(async () => {
+ actions.clickReloadButton();
+ await nextTick(100);
+ component.update();
+ });
+
+ expect(server.requests.length).toBe(totalRequests + 1);
+ expect(server.requests[server.requests.length - 1].url).toBe(API_BASE_PATH);
+ });
+
+ test('should show the details of a pipeline', async () => {
+ const { find, exists, actions } = testBed;
+
+ await actions.clickPipelineAt(0);
+
+ expect(exists('pipelinesTable')).toBe(true);
+ expect(exists('pipelineDetails')).toBe(true);
+ expect(find('pipelineDetails.title').text()).toBe(pipeline1.name);
+ });
+
+ test('should delete a pipeline', async () => {
+ const { actions, component } = testBed;
+ const { name: pipelineName } = pipeline1;
+
+ httpRequestsMockHelpers.setDeletePipelineResponse({
+ itemsDeleted: [pipelineName],
+ errors: [],
+ });
+
+ actions.clickPipelineAction(pipelineName, 'delete');
+
+ // We need to read the document "body" as the modal is added there and not inside
+ // the component DOM tree.
+ const modal = document.body.querySelector('[data-test-subj="deletePipelinesConfirmation"]');
+ const confirmButton: HTMLButtonElement | null = modal!.querySelector(
+ '[data-test-subj="confirmModalConfirmButton"]'
+ );
+
+ expect(modal).not.toBe(null);
+ expect(modal!.textContent).toContain('Delete pipeline');
+
+ await act(async () => {
+ confirmButton!.click();
+ await nextTick();
+ component.update();
+ });
+
+ const latestRequest = server.requests[server.requests.length - 1];
+
+ expect(latestRequest.method).toBe('DELETE');
+ expect(latestRequest.url).toBe(`${API_BASE_PATH}/${pipelineName}`);
+ expect(latestRequest.status).toEqual(200);
+ });
+ });
+
+ describe('No pipelines', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPipelinesResponse([]);
+
+ testBed = await setup();
+
+ await act(async () => {
+ const { waitFor } = testBed;
+
+ await waitFor('emptyList');
+ });
+ });
+
+ test('should display an empty prompt', async () => {
+ const { exists, find } = testBed;
+
+ expect(exists('sectionLoading')).toBe(false);
+ expect(exists('emptyList')).toBe(true);
+ expect(find('emptyList.title').text()).toEqual('Start by creating a pipeline');
+ });
+ });
+
+ describe('Error handling', () => {
+ beforeEach(async () => {
+ const error = {
+ status: 500,
+ error: 'Internal server error',
+ message: 'Internal server error',
+ };
+
+ httpRequestsMockHelpers.setLoadPipelinesResponse(undefined, { body: error });
+
+ testBed = await setup();
+
+ await act(async () => {
+ const { waitFor } = testBed;
+
+ await waitFor('pipelineLoadError');
+ });
+ });
+
+ test('should render an error message if error fetching pipelines', async () => {
+ const { exists, find } = testBed;
+
+ expect(exists('pipelineLoadError')).toBe(true);
+ expect(find('pipelineLoadError').text()).toContain('Unable to load pipelines.');
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx
index 9082196a48b39..55523bfa7d116 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx
@@ -126,6 +126,7 @@ export const PipelineForm: React.FunctionComponent = ({
setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)}
>
{isRequestVisible ? (
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx
index b90683426887f..8144228b1e9d5 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx
@@ -140,7 +140,12 @@ export const PipelineFormFields: React.FunctionComponent = ({
-
+
= ({
path="processors"
component={JsonEditorField}
componentProps={{
- ['data-test-subj']: 'processorsField',
euiCodeEditorProps: {
+ ['data-test-subj']: 'processorsField',
height: '300px',
'aria-label': i18n.translate('xpack.ingestPipelines.form.processorsFieldAriaLabel', {
defaultMessage: 'Processors JSON editor',
@@ -211,8 +216,8 @@ export const PipelineFormFields: React.FunctionComponent = ({
path="on_failure"
component={JsonEditorField}
componentProps={{
- ['data-test-subj']: 'onFailureEditor',
euiCodeEditorProps: {
+ ['data-test-subj']: 'onFailureEditor',
height: '300px',
'aria-label': i18n.translate('xpack.ingestPipelines.form.onFailureFieldAriaLabel', {
defaultMessage: 'Failure processors JSON editor',
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx
index 7cfe887d68d52..2ab7e84b3bb2b 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx
@@ -40,10 +40,10 @@ export const PipelineRequestFlyout: React.FunctionComponent = ({
uuid.current++;
return (
-
+
-
+
{name ? (
+
-
+
{pipeline.name ? (
-
-
+
+
-
-
+
+
= ({
-
+
{pipeline.name}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
index 318a9219b2010..f6fe2f0cf65fa 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
@@ -18,8 +18,9 @@ export const EmptyList: FunctionComponent = () => {
+
{i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', {
defaultMessage: 'Start by creating a pipeline',
})}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
index 23d105c807c8b..948290b169134 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
@@ -80,11 +80,15 @@ export const PipelinesList: React.FunctionComponent = ({
history.push(BASE_PATH);
};
+ if (data && data.length === 0) {
+ return ;
+ }
+
let content: React.ReactNode;
if (isLoading) {
content = (
-
+
= ({
pipelines={data}
/>
);
- } else {
- return ;
}
const renderFlyout = (): React.ReactNode => {
@@ -148,6 +150,7 @@ export const PipelinesList: React.FunctionComponent = ({
href={services.documentation.getIngestNodeUrl()}
target="_blank"
iconType="help"
+ data-test-subj="documentationLink"
>
= ({
= ({
const tableProps: EuiInMemoryTableProps = {
itemId: 'name',
isSelectable: true,
+ 'data-test-subj': 'pipelinesTable',
sorting: { sort: { field: 'name', direction: 'asc' } },
selection: {
onSelectionChange: setSelection,
@@ -91,7 +92,11 @@ export const PipelineTable: FunctionComponent = ({
defaultMessage: 'Name',
}),
sortable: true,
- render: (name: string) => {name},
+ render: (name: string) => (
+
+ {name}
+
+ ),
},
{
name: (
diff --git a/x-pack/plugins/lens/public/debounced_component/debounced_component.tsx b/x-pack/plugins/lens/public/debounced_component/debounced_component.tsx
index be6830c115836..08f55850b119e 100644
--- a/x-pack/plugins/lens/public/debounced_component/debounced_component.tsx
+++ b/x-pack/plugins/lens/public/debounced_component/debounced_component.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState, useMemo, memo, FunctionComponent } from 'react';
+import React, { useState, useMemo, useEffect, memo, FunctionComponent } from 'react';
import { debounce } from 'lodash';
/**
@@ -17,7 +17,11 @@ export function debouncedComponent(component: FunctionComponent,
return (props: TProps) => {
const [cachedProps, setCachedProps] = useState(props);
- const delayRender = useMemo(() => debounce(setCachedProps, delay), []);
+ const debouncePropsChange = debounce(setCachedProps, delay);
+ const delayRender = useMemo(() => debouncePropsChange, []);
+
+ // cancel debounced prop change if component has been unmounted in the meantime
+ useEffect(() => () => debouncePropsChange.cancel(), []);
delayRender(props);
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
index f7be82dd34ba3..81476e8fa3708 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
@@ -43,6 +43,12 @@ export function LayerPanel(
}
) {
const dragDropContext = useContext(DragContext);
+ const [popoverState, setPopoverState] = useState({
+ isOpen: false,
+ openId: null,
+ addingToGroupId: null,
+ });
+
const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props;
const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId];
if (!datasourcePublicAPI) {
@@ -74,12 +80,6 @@ export function LayerPanel(
dateRange: props.framePublicAPI.dateRange,
};
- const [popoverState, setPopoverState] = useState({
- isOpen: false,
- openId: null,
- addingToGroupId: null,
- });
-
const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps);
const isEmptyLayer = !groups.some(d => d.accessors.length > 0);
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
index 5cd803e7cebbc..6da9a94711081 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
@@ -61,6 +61,8 @@ export function EditorFrame(props: EditorFrameProps) {
// Initialize current datasource and all active datasources
useEffect(() => {
+ // prevents executing dispatch on unmounted component
+ let isUnmounted = false;
if (!allLoaded) {
Object.entries(props.datasourceMap).forEach(([datasourceId, datasource]) => {
if (
@@ -70,16 +72,21 @@ export function EditorFrame(props: EditorFrameProps) {
datasource
.initialize(state.datasourceStates[datasourceId].state || undefined)
.then(datasourceState => {
- dispatch({
- type: 'UPDATE_DATASOURCE_STATE',
- updater: datasourceState,
- datasourceId,
- });
+ if (!isUnmounted) {
+ dispatch({
+ type: 'UPDATE_DATASOURCE_STATE',
+ updater: datasourceState,
+ datasourceId,
+ });
+ }
})
.catch(onError);
}
});
}
+ return () => {
+ isUnmounted = true;
+ };
}, [allLoaded]);
const datasourceLayers: Record = {};
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx
index 1f741ca37934f..e246d8e27a708 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx
@@ -122,6 +122,16 @@ export function InnerWorkspacePanel({
framePublicAPI.filters,
]);
+ useEffect(() => {
+ // reset expression error if component attempts to run it again
+ if (expression && localState.expressionBuildError) {
+ setLocalState(s => ({
+ ...s,
+ expressionBuildError: undefined,
+ }));
+ }
+ }, [expression]);
+
function onDrop() {
if (suggestionForDraggedField) {
trackUiEvent('drop_onto_workspace');
@@ -174,16 +184,6 @@ export function InnerWorkspacePanel({
}
function renderVisualization() {
- useEffect(() => {
- // reset expression error if component attempts to run it again
- if (expression && localState.expressionBuildError) {
- setLocalState(s => ({
- ...s,
- expressionBuildError: undefined,
- }));
- }
- }, [expression]);
-
if (expression === null) {
return renderEmptyWorkspace();
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
index c396f0efee42e..5e3b32f6961e6 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
@@ -258,7 +258,17 @@ describe('IndexPattern Data Panel', () => {
it('should render a warning if there are no index patterns', () => {
const wrapper = shallowWithIntl(
-
+ {} }}
+ changeIndexPattern={jest.fn()}
+ />
);
expect(wrapper.find('[data-test-subj="indexPattern-no-indexpatterns"]')).toHaveLength(1);
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
index 79dcdafd916b4..b013f2b9d22a6 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
@@ -144,21 +144,49 @@ export function IndexPatternDataPanel({
indexPatternList.map(x => `${x.title}:${x.timeFieldName}`).join(','),
]}
/>
-
+
+ {Object.keys(indexPatterns).length === 0 ? (
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
>
);
}
@@ -194,35 +222,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
onChangeIndexPattern: (newId: string) => void;
existingFields: IndexPatternPrivateState['existingFields'];
}) {
- if (Object.keys(indexPatterns).length === 0) {
- return (
-
-
-
-
-
-
-
-
-
- );
- }
-
const [localState, setLocalState] = useState({
nameFilter: '',
typeFilter: [],
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
index 04e13fead6fca..7e2af6a19b041 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
@@ -127,7 +127,7 @@ export function BucketNestingEditor({
defaultMessage: 'Entire data set',
}),
},
- ...aggColumns,
+ ...aggColumns.map(({ value, text }) => ({ value, text })),
]}
value={prevColumn}
onChange={e => setColumns(nestColumn(layer.columnOrder, e.target.value, columnId))}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
index c4d2a6f8780c6..5f0fa95ad0022 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
@@ -251,22 +251,6 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
const IS_DARK_THEME = core.uiSettings.get('theme:darkMode');
const chartTheme = IS_DARK_THEME ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme;
-
- if (props.isLoading) {
- return ;
- } else if (
- (!props.histogram || props.histogram.buckets.length === 0) &&
- (!props.topValues || props.topValues.buckets.length === 0)
- ) {
- return (
-
- {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', {
- defaultMessage: 'No data to display.',
- })}
-
- );
- }
-
let histogramDefault = !!props.histogram;
const totalValuesCount =
@@ -309,6 +293,21 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
let title = <>>;
+ if (props.isLoading) {
+ return ;
+ } else if (
+ (!props.histogram || props.histogram.buckets.length === 0) &&
+ (!props.topValues || props.topValues.buckets.length === 0)
+ ) {
+ return (
+
+ {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', {
+ defaultMessage: 'No data to display.',
+ })}
+
+ );
+ }
+
if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) {
title = (
{
+ id?: string;
+ type?: string;
+ visualizationType: string | null;
+ title: string;
+ expression: string | null;
+ state: {
+ datasourceMetaData: {
+ filterableIndexPatterns: Array<{ id: string; title: string }>;
+ };
+ datasourceStates: {
+ // This is hardcoded as our only datasource
+ indexpattern: {
+ layers: Record<
+ string,
+ {
+ columnOrder: string[];
+ columns: Record;
+ }
+ >;
+ };
+ };
+ visualization: VisualizationState;
+ query: unknown;
+ filters: unknown[];
+ };
+}
interface XYLayerPre77 {
layerId: string;
@@ -15,13 +43,23 @@ interface XYLayerPre77 {
accessors: string[];
}
+interface XYStatePre77 {
+ layers: XYLayerPre77[];
+}
+
+interface XYStatePost77 {
+ layers: Array>;
+}
+
/**
* Removes the `lens_auto_date` subexpression from a stored expression
* string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"}
*/
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => {
- const expression: string = doc.attributes?.expression;
+const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => {
+ const expression = doc.attributes.expression;
+ if (!expression) {
+ return doc;
+ }
try {
const ast = fromExpression(expression);
const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => {
@@ -74,9 +112,11 @@ const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => {
/**
* Adds missing timeField arguments to esaggs in the Lens expression
*/
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => {
- const expression: string = doc.attributes?.expression;
+const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => {
+ const expression = doc.attributes.expression;
+ if (!expression) {
+ return doc;
+ }
try {
const ast = fromExpression(expression);
@@ -133,27 +173,32 @@ const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) =>
}
};
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const migrations: Record> = {
- '7.7.0': doc => {
- const newDoc = cloneDeep(doc);
- if (newDoc.attributes?.visualizationType === 'lnsXY') {
- const datasourceState = newDoc.attributes.state?.datasourceStates?.indexpattern;
- const datasourceLayers = datasourceState?.layers ?? {};
- const xyState = newDoc.attributes.state?.visualization;
- newDoc.attributes.state.visualization.layers = xyState.layers.map((layer: XYLayerPre77) => {
- const layerId = layer.layerId;
- const datasource = datasourceLayers[layerId];
- return {
- ...layer,
- xAccessor: datasource?.columns[layer.xAccessor] ? layer.xAccessor : undefined,
- splitAccessor: datasource?.columns[layer.splitAccessor] ? layer.splitAccessor : undefined,
- accessors: layer.accessors.filter(accessor => !!datasource?.columns[accessor]),
- };
- }) as typeof xyState.layers;
- }
- return newDoc;
- },
+const removeInvalidAccessors: SavedObjectMigrationFn<
+ LensDocShape,
+ LensDocShape
+> = doc => {
+ const newDoc = cloneDeep(doc);
+ if (newDoc.attributes.visualizationType === 'lnsXY') {
+ const datasourceLayers = newDoc.attributes.state.datasourceStates.indexpattern.layers || {};
+ const xyState = newDoc.attributes.state.visualization;
+ (newDoc.attributes as LensDocShape<
+ XYStatePost77
+ >).state.visualization.layers = xyState.layers.map((layer: XYLayerPre77) => {
+ const layerId = layer.layerId;
+ const datasource = datasourceLayers[layerId];
+ return {
+ ...layer,
+ xAccessor: datasource?.columns[layer.xAccessor] ? layer.xAccessor : undefined,
+ splitAccessor: datasource?.columns[layer.splitAccessor] ? layer.splitAccessor : undefined,
+ accessors: layer.accessors.filter(accessor => !!datasource?.columns[accessor]),
+ };
+ });
+ }
+ return newDoc;
+};
+
+export const migrations: SavedObjectMigrationMap = {
+ '7.7.0': removeInvalidAccessors,
// The order of these migrations matter, since the timefield migration relies on the aggConfigs
// sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was).
'7.8.0': (doc, context) => addTimeFieldToEsaggs(removeLensAutoDate(doc, context), context),
diff --git a/x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.ts b/x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.ts
index a59122d7d6309..e2833d5abd0c2 100644
--- a/x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.ts
+++ b/x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.ts
@@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n';
import {
AggDescriptor,
ColorDynamicOptions,
- LabelDynamicOptions,
LayerDescriptor,
SizeDynamicOptions,
StylePropertyField,
@@ -80,10 +79,6 @@ function createLayerLabel(
metricName = i18n.translate('xpack.maps.observability.durationMetricName', {
defaultMessage: 'Duration',
});
- } else if (metric === OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE) {
- metricName = i18n.translate('xpack.maps.observability.slaPercentageMetricName', {
- defaultMessage: '% Duration of SLA',
- });
} else if (metric === OBSERVABILITY_METRIC_TYPE.COUNT) {
metricName = i18n.translate('xpack.maps.observability.countMetricName', {
defaultMessage: 'Total',
@@ -103,11 +98,6 @@ function createAggDescriptor(metric: OBSERVABILITY_METRIC_TYPE): AggDescriptor {
type: AGG_TYPE.AVG,
field: 'transaction.duration.us',
};
- } else if (metric === OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE) {
- return {
- type: AGG_TYPE.AVG,
- field: 'duration_sla_pct',
- };
} else if (metric === OBSERVABILITY_METRIC_TYPE.UNIQUE_COUNT) {
return {
type: AGG_TYPE.UNIQUE_COUNT,
@@ -251,16 +241,6 @@ export function createLayerDescriptor({
},
};
- if (metric === OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE) {
- styleProperties[VECTOR_STYLES.LABEL_TEXT] = {
- type: STYLE_TYPE.DYNAMIC,
- options: {
- ...(defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT]!.options as LabelDynamicOptions),
- field: metricStyleField,
- },
- };
- }
-
return VectorLayer.createDescriptor({
label,
query: apmSourceQuery,
diff --git a/x-pack/plugins/maps/public/layers/solution_layers/observability/metric_select.tsx b/x-pack/plugins/maps/public/layers/solution_layers/observability/metric_select.tsx
index 8750034f74696..4a40b257cb517 100644
--- a/x-pack/plugins/maps/public/layers/solution_layers/observability/metric_select.tsx
+++ b/x-pack/plugins/maps/public/layers/solution_layers/observability/metric_select.tsx
@@ -11,7 +11,6 @@ import { OBSERVABILITY_LAYER_TYPE } from './layer_select';
export enum OBSERVABILITY_METRIC_TYPE {
TRANSACTION_DURATION = 'TRANSACTION_DURATION',
- SLA_PERCENTAGE = 'SLA_PERCENTAGE',
COUNT = 'COUNT',
UNIQUE_COUNT = 'UNIQUE_COUNT',
}
@@ -23,12 +22,6 @@ const APM_RUM_PERFORMANCE_METRIC_OPTIONS = [
defaultMessage: 'Transaction duraction',
}),
},
- {
- value: OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE,
- text: i18n.translate('xpack.maps.observability.slaPercentageLabel', {
- defaultMessage: 'SLA percentage',
- }),
- },
];
const APM_RUM_TRAFFIC_METRIC_OPTIONS = [
diff --git a/x-pack/plugins/ml/common/types/ml_server_info.ts b/x-pack/plugins/ml/common/types/ml_server_info.ts
index 26dd1758827b4..66142f53add3a 100644
--- a/x-pack/plugins/ml/common/types/ml_server_info.ts
+++ b/x-pack/plugins/ml/common/types/ml_server_info.ts
@@ -18,6 +18,7 @@ export interface MlServerDefaults {
export interface MlServerLimits {
max_model_memory_limit?: string;
+ effective_max_model_memory_limit?: string;
}
export interface MlInfoResponse {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
index fb3b2b3519947..7501fe3d82fc6 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
@@ -91,7 +91,7 @@ export interface FieldSelectionItem {
}
export interface DfAnalyticsExplainResponse {
- field_selection: FieldSelectionItem[];
+ field_selection?: FieldSelectionItem[];
memory_estimation: {
expected_memory_without_disk: string;
expected_memory_with_disk: string;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts
index 6f9dc694d8172..e664a1ddbdbcc 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts
@@ -51,6 +51,10 @@ export const useExplorationResults = (
d => !d.includes(`.${FEATURE_IMPORTANCE}.`) && d !== ML__ID_COPY
);
+ useEffect(() => {
+ dataGrid.resetPagination();
+ }, [JSON.stringify(searchQuery)]);
+
useEffect(() => {
getIndexData(jobConfig, dataGrid, searchQuery);
// custom comparison
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts
index 0d06bc0d43307..75b2f6aa867df 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts
@@ -58,6 +58,10 @@ export const useOutlierData = (
d => !d.includes(`.${FEATURE_INFLUENCE}.`) && d !== ML__ID_COPY
);
+ useEffect(() => {
+ dataGrid.resetPagination();
+ }, [JSON.stringify(searchQuery)]);
+
// initialize sorting: reverse sort on outlier score column
useEffect(() => {
if (jobConfig !== undefined) {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx
index 92de5ad7be21e..85cd70912b41f 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx
@@ -53,7 +53,7 @@ describe('Data Frame Analytics: ', () => {
);
const euiFormRows = wrapper.find('EuiFormRow');
- expect(euiFormRows.length).toBe(9);
+ expect(euiFormRows.length).toBe(10);
const row1 = euiFormRows.at(0);
expect(row1.find('label').text()).toBe('Job type');
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx
index 199100d8b5ab0..11052b171845d 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx
@@ -48,6 +48,13 @@ import {
} from '../../../../common/analytics';
import { shouldAddAsDepVarOption, OMIT_FIELDS } from './form_options_validation';
+const requiredFieldsErrorText = i18n.translate(
+ 'xpack.ml.dataframe.analytics.create.requiredFieldsErrorMessage',
+ {
+ defaultMessage: 'At least one field must be included in the analysis.',
+ }
+);
+
export const CreateAnalyticsForm: FC = ({ actions, state }) => {
const {
services: { docLinks },
@@ -96,6 +103,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta
numTopFeatureImportanceValuesValid,
previousJobType,
previousSourceIndex,
+ requiredFieldsError,
sourceIndex,
sourceIndexNameEmpty,
sourceIndexNameValid,
@@ -158,6 +166,8 @@ export const CreateAnalyticsForm: FC = ({ actions, sta
};
const debouncedGetExplainData = debounce(async () => {
+ const jobTypeOrIndexChanged =
+ previousSourceIndex !== sourceIndex || previousJobType !== jobType;
const shouldUpdateModelMemoryLimit = !firstUpdate.current || !modelMemoryLimit;
const shouldUpdateEstimatedMml =
!firstUpdate.current || !modelMemoryLimit || estimatedModelMemoryLimit === '';
@@ -167,7 +177,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta
}
// Reset if sourceIndex or jobType changes (jobType requires dependent_variable to be set -
// which won't be the case if switching from outlier detection)
- if (previousSourceIndex !== sourceIndex || previousJobType !== jobType) {
+ if (jobTypeOrIndexChanged) {
setFormState({
loadingFieldOptions: true,
});
@@ -186,8 +196,21 @@ export const CreateAnalyticsForm: FC = ({ actions, sta
setEstimatedModelMemoryLimit(expectedMemoryWithoutDisk);
}
+ const fieldSelection: FieldSelectionItem[] | undefined = resp.field_selection;
+
+ let hasRequiredFields = false;
+ if (fieldSelection) {
+ for (let i = 0; i < fieldSelection.length; i++) {
+ const field = fieldSelection[i];
+ if (field.is_included === true && field.is_required === false) {
+ hasRequiredFields = true;
+ break;
+ }
+ }
+ }
+
// If sourceIndex has changed load analysis field options again
- if (previousSourceIndex !== sourceIndex || previousJobType !== jobType) {
+ if (jobTypeOrIndexChanged) {
const analyzedFieldsOptions: EuiComboBoxOptionOption[] = [];
if (resp.field_selection) {
@@ -204,21 +227,24 @@ export const CreateAnalyticsForm: FC = ({ actions, sta
loadingFieldOptions: false,
fieldOptionsFetchFail: false,
maxDistinctValuesError: undefined,
+ requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined,
});
} else {
setFormState({
...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}),
+ requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined,
});
}
} catch (e) {
let errorMessage;
if (
jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION &&
- e.message !== undefined &&
- e.message.includes('status_exception') &&
- e.message.includes('must have at most')
+ e.body &&
+ e.body.message !== undefined &&
+ e.body.message.includes('status_exception') &&
+ e.body.message.includes('must have at most')
) {
- errorMessage = e.message;
+ errorMessage = e.body.message;
}
const fallbackModelMemoryLimit =
jobType !== undefined
@@ -321,6 +347,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta
excludesOptions: [],
previousSourceIndex: sourceIndex,
sourceIndex: selectedOptions[0].label || '',
+ requiredFieldsError: undefined,
});
};
@@ -368,6 +395,9 @@ export const CreateAnalyticsForm: FC = ({ actions, sta
forceInput.current.dispatchEvent(evt);
}, []);
+ const noSupportetdAnalysisFields =
+ excludesOptions.length === 0 && fieldOptionsFetchFail === false && !sourceIndexNameEmpty;
+
return (
@@ -715,18 +745,31 @@ export const CreateAnalyticsForm: FC = ({ actions, sta
)}
+
+
+
= ({ type, setFormState }) => {
previousJobType: type,
jobType: value,
excludes: [],
+ requiredFieldsError: undefined,
});
}}
data-test-subj="mlAnalyticsCreateJobFlyoutJobTypeSelect"
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
index d55eb14a20e29..1cab42d8ee12d 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
@@ -124,6 +124,7 @@ export const validateAdvancedEditor = (state: State): State => {
createIndexPattern,
excludes,
maxDistinctValuesError,
+ requiredFieldsError,
} = state.form;
const { jobConfig } = state;
@@ -330,6 +331,7 @@ export const validateAdvancedEditor = (state: State): State => {
state.isValid =
maxDistinctValuesError === undefined &&
+ requiredFieldsError === undefined &&
excludesValid &&
trainingPercentValid &&
state.form.modelMemoryLimitUnitValid &&
@@ -397,6 +399,7 @@ const validateForm = (state: State): State => {
maxDistinctValuesError,
modelMemoryLimit,
numTopFeatureImportanceValuesValid,
+ requiredFieldsError,
} = state.form;
const { estimatedModelMemoryLimit } = state;
@@ -412,6 +415,7 @@ const validateForm = (state: State): State => {
state.isValid =
maxDistinctValuesError === undefined &&
+ requiredFieldsError === undefined &&
!jobTypeEmpty &&
!mmlValidationResult &&
!jobIdEmpty &&
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
index 70840a442f6f6..8ca985a537b6e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
@@ -76,6 +76,7 @@ export interface State {
numTopFeatureImportanceValuesValid: boolean;
previousJobType: null | AnalyticsJobType;
previousSourceIndex: EsIndexName | undefined;
+ requiredFieldsError: string | undefined;
sourceIndex: EsIndexName;
sourceIndexNameEmpty: boolean;
sourceIndexNameValid: boolean;
@@ -133,6 +134,7 @@ export const getInitialState = (): State => ({
numTopFeatureImportanceValuesValid: true,
previousJobType: null,
previousSourceIndex: undefined,
+ requiredFieldsError: undefined,
sourceIndex: '',
sourceIndexNameEmpty: true,
sourceIndexNameValid: false,
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js
index f7b0e726ecc53..fa36a0626d632 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js
@@ -165,6 +165,15 @@ export function extractJobDetails(job) {
items: filterObjects(job.model_size_stats).map(formatValues),
};
+ const jobTimingStats = {
+ id: 'jobTimingStats',
+ title: i18n.translate('xpack.ml.jobsList.jobDetails.jobTimingStatsTitle', {
+ defaultMessage: 'Job timing stats',
+ }),
+ position: 'left',
+ items: filterObjects(job.timing_stats).map(formatValues),
+ };
+
const datafeedTimingStats = {
id: 'datafeedTimingStats',
title: i18n.translate('xpack.ml.jobsList.jobDetails.datafeedTimingStatsTitle', {
@@ -192,6 +201,7 @@ export function extractJobDetails(job) {
datafeed,
counts,
modelSizeStats,
+ jobTimingStats,
datafeedTimingStats,
};
}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js
index 9984f3be299ae..246a476517ace 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js
@@ -63,6 +63,12 @@ export function formatValues([key, value]) {
// numbers rounded to 3 decimal places
case 'average_search_time_per_bucket_ms':
case 'exponential_average_search_time_per_hour_ms':
+ case 'total_bucket_processing_time_ms':
+ case 'minimum_bucket_processing_time_ms':
+ case 'maximum_bucket_processing_time_ms':
+ case 'average_bucket_processing_time_ms':
+ case 'exponential_average_bucket_processing_time_ms':
+ case 'exponential_average_bucket_processing_time_per_hour_ms':
value = typeof value === 'number' ? roundToDecimalPlace(value, 3).toLocaleString() : value;
break;
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
index e3f348ad32b0c..0375997b86bb8 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
@@ -60,6 +60,7 @@ export class JobDetails extends Component {
datafeed,
counts,
modelSizeStats,
+ jobTimingStats,
datafeedTimingStats,
} = extractJobDetails(job);
@@ -102,7 +103,7 @@ export class JobDetails extends Component {
content: (
),
},
diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js
index bbfec49ac1388..fb75476c48fa3 100644
--- a/x-pack/plugins/ml/public/application/services/job_service.js
+++ b/x-pack/plugins/ml/public/application/services/job_service.js
@@ -369,6 +369,8 @@ class JobService {
delete tempJob.open_time;
delete tempJob.established_model_memory;
delete tempJob.calendars;
+ delete tempJob.timing_stats;
+ delete tempJob.forecasts_stats;
delete tempJob.analysis_config.use_per_partition_normalization;
diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap
index e5026778fec1c..df2e119f511e1 100644
--- a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap
+++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap
@@ -88,6 +88,7 @@ exports[`CalendarForm Renders calendar form 1`] = `
size="xl"
/>
{isGlobalCalendar === false && (
diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts
similarity index 56%
rename from x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js
rename to x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts
index a0dacc38e5835..f5daadfe86be0 100644
--- a/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js
+++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts
@@ -4,8 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import expect from '@kbn/expect';
-import { estimateBucketSpanFactory } from '../bucket_span_estimator';
+import { APICaller } from 'kibana/server';
+
+import { ES_AGGREGATION } from '../../../common/constants/aggregation_types';
+
+import { estimateBucketSpanFactory, BucketSpanEstimatorData } from './bucket_span_estimator';
// Mock callWithRequest with the ability to simulate returning different
// permission settings. On each call using `ml.privilegeCheck` we retrieve
@@ -14,7 +17,7 @@ import { estimateBucketSpanFactory } from '../bucket_span_estimator';
// sufficient permissions should be returned, the second time insufficient
// permissions.
const permissions = [false, true];
-const callWithRequest = method => {
+const callWithRequest: APICaller = (method: string) => {
return new Promise(resolve => {
if (method === 'ml.privilegeCheck') {
resolve({
@@ -28,34 +31,19 @@ const callWithRequest = method => {
return;
}
resolve({});
- });
+ }) as Promise;
};
-const callWithInternalUser = () => {
+const callWithInternalUser: APICaller = () => {
return new Promise(resolve => {
resolve({});
- });
+ }) as Promise;
};
-// mock xpack_main plugin
-function mockXpackMainPluginFactory(isEnabled = false, licenseType = 'platinum') {
- return {
- info: {
- isAvailable: () => true,
- feature: () => ({
- isEnabled: () => isEnabled,
- }),
- license: {
- getType: () => licenseType,
- },
- },
- };
-}
-
// mock configuration to be passed to the estimator
-const formConfig = {
- aggTypes: ['count'],
- duration: {},
+const formConfig: BucketSpanEstimatorData = {
+ aggTypes: [ES_AGGREGATION.COUNT],
+ duration: { start: 0, end: 1 },
fields: [null],
index: '',
query: {
@@ -64,13 +52,15 @@ const formConfig = {
must_not: [],
},
},
+ splitField: undefined,
+ timeField: undefined,
};
describe('ML - BucketSpanEstimator', () => {
it('call factory', () => {
expect(function() {
- estimateBucketSpanFactory(callWithRequest, callWithInternalUser);
- }).to.not.throwError('Not initialized.');
+ estimateBucketSpanFactory(callWithRequest, callWithInternalUser, false);
+ }).not.toThrow('Not initialized.');
});
it('call factory and estimator with security disabled', done => {
@@ -78,44 +68,29 @@ describe('ML - BucketSpanEstimator', () => {
const estimateBucketSpan = estimateBucketSpanFactory(
callWithRequest,
callWithInternalUser,
- mockXpackMainPluginFactory()
+ true
);
estimateBucketSpan(formConfig).catch(catchData => {
- expect(catchData).to.be('Unable to retrieve cluster setting search.max_buckets');
+ expect(catchData).toBe('Unable to retrieve cluster setting search.max_buckets');
done();
});
- }).to.not.throwError('Not initialized.');
+ }).not.toThrow('Not initialized.');
});
- it('call factory and estimator with security enabled and sufficient permissions.', done => {
+ it('call factory and estimator with security enabled.', done => {
expect(function() {
const estimateBucketSpan = estimateBucketSpanFactory(
callWithRequest,
callWithInternalUser,
- mockXpackMainPluginFactory(true)
+ false
);
estimateBucketSpan(formConfig).catch(catchData => {
- expect(catchData).to.be('Unable to retrieve cluster setting search.max_buckets');
+ expect(catchData).toBe('Unable to retrieve cluster setting search.max_buckets');
done();
});
- }).to.not.throwError('Not initialized.');
- });
-
- it('call factory and estimator with security enabled and insufficient permissions.', done => {
- expect(function() {
- const estimateBucketSpan = estimateBucketSpanFactory(
- callWithRequest,
- callWithInternalUser,
- mockXpackMainPluginFactory(true)
- );
-
- estimateBucketSpan(formConfig).catch(catchData => {
- expect(catchData).to.be('Insufficient permissions to call bucket span estimation.');
- done();
- });
- }).to.not.throwError('Not initialized.');
+ }).not.toThrow('Not initialized.');
});
});
diff --git a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts
index cd61dd9eddcdd..1cc2a07ddbc88 100644
--- a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts
+++ b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts
@@ -9,6 +9,7 @@ import { APICaller } from 'kibana/server';
import { MLCATEGORY } from '../../../common/constants/field_types';
import { AnalysisConfig } from '../../../common/types/anomaly_detection_jobs';
import { fieldsServiceProvider } from '../fields_service';
+import { MlInfoResponse } from '../../../common/types/ml_server_info';
interface ModelMemoryEstimationResult {
/**
@@ -139,15 +140,9 @@ export function calculateModelMemoryLimitProvider(callAsCurrentUser: APICaller)
latestMs: number,
allowMMLGreaterThanMax = false
): Promise {
- let maxModelMemoryLimit;
- try {
- const resp = await callAsCurrentUser('ml.info');
- if (resp?.limits?.max_model_memory_limit !== undefined) {
- maxModelMemoryLimit = resp.limits.max_model_memory_limit.toUpperCase();
- }
- } catch (e) {
- throw new Error('Unable to retrieve max model memory limit');
- }
+ const info = await callAsCurrentUser('ml.info');
+ const maxModelMemoryLimit = info.limits.max_model_memory_limit?.toUpperCase();
+ const effectiveMaxModelMemoryLimit = info.limits.effective_max_model_memory_limit?.toUpperCase();
const { overallCardinality, maxBucketCardinality } = await getCardinalities(
analysisConfig,
@@ -168,17 +163,32 @@ export function calculateModelMemoryLimitProvider(callAsCurrentUser: APICaller)
})
).model_memory_estimate.toUpperCase();
- let modelMemoryLimit: string = estimatedModelMemoryLimit;
+ let modelMemoryLimit = estimatedModelMemoryLimit;
+ let mmlCappedAtMax = false;
// if max_model_memory_limit has been set,
// make sure the estimated value is not greater than it.
- if (!allowMMLGreaterThanMax && maxModelMemoryLimit !== undefined) {
- // @ts-ignore
- const maxBytes = numeral(maxModelMemoryLimit).value();
+ if (allowMMLGreaterThanMax === false) {
// @ts-ignore
const mmlBytes = numeral(estimatedModelMemoryLimit).value();
- if (mmlBytes > maxBytes) {
+ if (maxModelMemoryLimit !== undefined) {
+ // @ts-ignore
+ const maxBytes = numeral(maxModelMemoryLimit).value();
+ if (mmlBytes > maxBytes) {
+ // @ts-ignore
+ modelMemoryLimit = `${Math.floor(maxBytes / numeral('1MB').value())}MB`;
+ mmlCappedAtMax = true;
+ }
+ }
+
+ // if we've not already capped the estimated mml at the hard max server setting
+ // ensure that the estimated mml isn't greater than the effective max mml
+ if (mmlCappedAtMax === false && effectiveMaxModelMemoryLimit !== undefined) {
// @ts-ignore
- modelMemoryLimit = `${Math.floor(maxBytes / numeral('1MB').value())}MB`;
+ const effectiveMaxMmlBytes = numeral(effectiveMaxModelMemoryLimit).value();
+ if (mmlBytes > effectiveMaxMmlBytes) {
+ // @ts-ignore
+ modelMemoryLimit = `${Math.floor(effectiveMaxMmlBytes / numeral('1MB').value())}MB`;
+ }
}
}
@@ -186,6 +196,7 @@ export function calculateModelMemoryLimitProvider(callAsCurrentUser: APICaller)
estimatedModelMemoryLimit,
modelMemoryLimit,
...(maxModelMemoryLimit ? { maxModelMemoryLimit } : {}),
+ ...(effectiveMaxModelMemoryLimit ? { effectiveMaxModelMemoryLimit } : {}),
};
};
}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json
index d8c970e179416..c792b981df30a 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json
@@ -30,7 +30,7 @@
{
"url_name": "Process rate",
"time_range": "1h",
- "url_value": "kibana#/dashboard/ml_auditbeat_docker_process_event_rate_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:container.runtime,negate:!f,params:(query:docker),type:phrase,value:docker),query:(match:(container.runtime:(query:docker,type:phrase)))),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022\u0027))"
+ "url_value": "kibana#/dashboard/ml_auditbeat_docker_process_event_rate_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:container.runtime,negate:!f,params:(query:docker),type:phrase,value:docker),query:(match:(container.runtime:(query:docker,type:phrase)))),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022\u0027))"
},
{
"url_name": "Raw data",
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json
index 76e3c8026c631..b3f02ae5a6bf8 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json
@@ -30,7 +30,7 @@
{
"url_name": "Process explorer",
"time_range": "1h",
- "url_value": "kibana#/dashboard/ml_auditbeat_docker_process_explorer_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:container.runtime,negate:!f,params:(query:docker),type:phrase,value:docker),query:(match:(container.runtime:(query:docker,type:phrase)))),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022\u0027))"
+ "url_value": "kibana#/dashboard/ml_auditbeat_docker_process_explorer_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:container.runtime,negate:!f,params:(query:docker),type:phrase,value:docker),query:(match:(container.runtime:(query:docker,type:phrase)))),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027container.name:\u0022$container.name$\u0022\u0027))"
},
{
"url_name": "Raw data",
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json
index 487bee5311878..0e9336507b465 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json
@@ -29,7 +29,7 @@
{
"url_name": "Process rate",
"time_range": "1h",
- "url_value": "kibana#/dashboard/ml_auditbeat_hosts_process_event_rate_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),exists:(field:container.runtime),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:container.runtime,negate:!t,type:exists,value:exists)),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022\u0027))"
+ "url_value": "kibana#/dashboard/ml_auditbeat_hosts_process_event_rate_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),exists:(field:container.runtime),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:container.runtime,negate:!t,type:exists,value:exists)),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022\u0027))"
},
{
"url_name": "Raw data",
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json
index 9ba6859bfa166..4dd1409b71c79 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json
@@ -30,7 +30,7 @@
{
"url_name": "Process explorer",
"time_range": "1h",
- "url_value": "kibana#/dashboard/ml_auditbeat_hosts_process_explorer_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),exists:(field:container.runtime),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:container.runtime,negate:!t,type:exists,value:exists)),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:INDEX_PATTERN_ID,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022\u0027))"
+ "url_value": "kibana#/dashboard/ml_auditbeat_hosts_process_explorer_ecs?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:event.module,negate:!f,params:(query:auditd),type:phrase,value:auditd),query:(match:(event.module:(query:auditd,type:phrase)))),('$state':(store:appState),exists:(field:container.runtime),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:container.runtime,negate:!t,type:exists,value:exists)),('$state':(store:appState),exists:(field:auditd.data.syscall),meta:(alias:!n,disabled:!f,index:\u0027INDEX_PATTERN_ID\u0027,key:auditd.data.syscall,negate:!f,type:exists,value:exists))),query:(language:kuery,query:\u0027host.name:\u0022$host.name$\u0022\u0027))"
},
{
"url_name": "Raw data",
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json
index e0230e2a06373..c3d401085f7ae 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json
@@ -27,7 +27,7 @@
"custom_urls": [
{
"url_name": "Raw data",
- "url_value": "kibana#/discover?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:ff959d40-b880-11e8-a6d9-e546fe2bba5f,query:(language:kuery,query:\u0027customer_full_name.keyword:\u0022$customer_full_name.keyword$\u0022\u0027),sort:!('@timestamp',desc))"
+ "url_value": "kibana#/discover?_g=(time:(from:\u0027$earliest$\u0027,mode:absolute,to:\u0027$latest$\u0027))&_a=(index:\u0027ff959d40-b880-11e8-a6d9-e546fe2bba5f\u0027,query:(language:kuery,query:\u0027customer_full_name.keyword:\u0022$customer_full_name.keyword$\u0022\u0027),sort:!('@timestamp',desc))"
},
{
"url_name": "Data dashboard",
diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts
index 6024ecf4925e6..225cd43e411a4 100644
--- a/x-pack/plugins/ml/server/models/job_service/jobs.ts
+++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts
@@ -328,7 +328,7 @@ export function jobsProvider(callAsCurrentUser: APICaller) {
// create jobs objects containing job stats, datafeeds, datafeed stats and calendars
if (jobResults && jobResults.jobs) {
jobResults.jobs.forEach(job => {
- const tempJob = job as CombinedJobWithStats;
+ let tempJob = job as CombinedJobWithStats;
const calendars: string[] = [
...(calendarsByJobId[tempJob.job_id] || []),
@@ -341,9 +341,7 @@ export function jobsProvider(callAsCurrentUser: APICaller) {
if (jobStatsResults && jobStatsResults.jobs) {
const jobStats = jobStatsResults.jobs.find(js => js.job_id === tempJob.job_id);
if (jobStats !== undefined) {
- tempJob.state = jobStats.state;
- tempJob.data_counts = jobStats.data_counts;
- tempJob.model_size_stats = jobStats.model_size_stats;
+ tempJob = { ...tempJob, ...jobStats };
if (jobStats.node) {
tempJob.node = jobStats.node;
}
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_cardinality.json
similarity index 100%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json
rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_cardinality.json
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_search_response.json
similarity index 100%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json
rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_farequote_search_response.json
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_field_caps.json
similarity index 100%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json
rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_field_caps.json
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_it_search_response.json
similarity index 100%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json
rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_it_search_response.json
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field.json
similarity index 100%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json
rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field.json
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field_nested.json
similarity index 100%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json
rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_field_nested.json
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json b/x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_range.json
similarity index 100%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json
rename to x-pack/plugins/ml/server/models/job_validation/__mocks__/mock_time_range.json
diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts
index 33f5d5ec95fad..6a9a7a0c13395 100644
--- a/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts
+++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts
@@ -6,14 +6,19 @@
import { APICaller } from 'kibana/server';
import { TypeOf } from '@kbn/config-schema';
+
+import { DeepPartial } from '../../../common/types/common';
+
import { validateJobSchema } from '../../routes/schemas/job_validation_schema';
-type ValidateJobPayload = TypeOf;
+import { ValidationMessage } from './messages';
+
+export type ValidateJobPayload = TypeOf;
export function validateJob(
callAsCurrentUser: APICaller,
- payload: ValidateJobPayload,
- kbnVersion: string,
- callAsInternalUser: APICaller,
- isSecurityDisabled: boolean
-): string[];
+ payload?: DeepPartial,
+ kbnVersion?: string,
+ callAsInternalUser?: APICaller,
+ isSecurityDisabled?: boolean
+): Promise;
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts
similarity index 77%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js
rename to x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts
index 726a8e8d8db85..ca127f43d08af 100644
--- a/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js
+++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts
@@ -4,16 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import expect from '@kbn/expect';
-import { validateJob } from '../job_validation';
+import { APICaller } from 'kibana/server';
+
+import { validateJob } from './job_validation';
// mock callWithRequest
-const callWithRequest = () => {
+const callWithRequest: APICaller = (method: string) => {
return new Promise(resolve => {
+ if (method === 'fieldCaps') {
+ resolve({ fields: [] });
+ return;
+ }
resolve({});
- });
+ }) as Promise;
};
+// Note: The tests cast `payload` as any
+// so we can simulate possible runtime payloads
+// that don't satisfy the TypeScript specs.
describe('ML - validateJob', () => {
it('calling factory without payload throws an error', done => {
validateJob(callWithRequest).then(
@@ -61,7 +69,7 @@ describe('ML - validateJob', () => {
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql([
+ expect(ids).toStrictEqual([
'job_id_empty',
'detectors_empty',
'bucket_span_empty',
@@ -70,10 +78,14 @@ describe('ML - validateJob', () => {
});
});
- const jobIdTests = (testIds, messageId) => {
+ const jobIdTests = (testIds: string[], messageId: string) => {
const promises = testIds.map(id => {
- const payload = { job: { analysis_config: { detectors: [] } } };
- payload.job.job_id = id;
+ const payload = {
+ job: {
+ analysis_config: { detectors: [] },
+ job_id: id,
+ },
+ };
return validateJob(callWithRequest, payload).catch(() => {
new Error('Promise should not fail for jobIdTests.');
});
@@ -81,19 +93,21 @@ describe('ML - validateJob', () => {
return Promise.all(promises).then(testResults => {
testResults.forEach(messages => {
- const ids = messages.map(m => m.id);
- expect(ids.includes(messageId)).to.equal(true);
+ expect(Array.isArray(messages)).toBe(true);
+ if (Array.isArray(messages)) {
+ const ids = messages.map(m => m.id);
+ expect(ids.includes(messageId)).toBe(true);
+ }
});
});
};
- const jobGroupIdTest = (testIds, messageId) => {
- const payload = { job: { analysis_config: { detectors: [] } } };
- payload.job.groups = testIds;
+ const jobGroupIdTest = (testIds: string[], messageId: string) => {
+ const payload = { job: { analysis_config: { detectors: [] }, groups: testIds } };
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids.includes(messageId)).to.equal(true);
+ expect(ids.includes(messageId)).toBe(true);
});
};
@@ -126,10 +140,9 @@ describe('ML - validateJob', () => {
return jobGroupIdTest(validTestIds, 'job_group_id_valid');
});
- const bucketSpanFormatTests = (testFormats, messageId) => {
+ const bucketSpanFormatTests = (testFormats: string[], messageId: string) => {
const promises = testFormats.map(format => {
- const payload = { job: { analysis_config: { detectors: [] } } };
- payload.job.analysis_config.bucket_span = format;
+ const payload = { job: { analysis_config: { bucket_span: format, detectors: [] } } };
return validateJob(callWithRequest, payload).catch(() => {
new Error('Promise should not fail for bucketSpanFormatTests.');
});
@@ -137,8 +150,11 @@ describe('ML - validateJob', () => {
return Promise.all(promises).then(testResults => {
testResults.forEach(messages => {
- const ids = messages.map(m => m.id);
- expect(ids.includes(messageId)).to.equal(true);
+ expect(Array.isArray(messages)).toBe(true);
+ if (Array.isArray(messages)) {
+ const ids = messages.map(m => m.id);
+ expect(ids.includes(messageId)).toBe(true);
+ }
});
});
};
@@ -152,7 +168,7 @@ describe('ML - validateJob', () => {
});
it('at least one detector function is empty', () => {
- const payload = { job: { analysis_config: { detectors: [] } } };
+ const payload = { job: { analysis_config: { detectors: [] as Array<{ function?: string }> } } };
payload.job.analysis_config.detectors.push({
function: 'count',
});
@@ -165,19 +181,19 @@ describe('ML - validateJob', () => {
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids.includes('detectors_function_empty')).to.equal(true);
+ expect(ids.includes('detectors_function_empty')).toBe(true);
});
});
it('detector function is not empty', () => {
- const payload = { job: { analysis_config: { detectors: [] } } };
+ const payload = { job: { analysis_config: { detectors: [] as Array<{ function?: string }> } } };
payload.job.analysis_config.detectors.push({
function: 'count',
});
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids.includes('detectors_function_not_empty')).to.equal(true);
+ expect(ids.includes('detectors_function_not_empty')).toBe(true);
});
});
@@ -189,7 +205,7 @@ describe('ML - validateJob', () => {
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids.includes('index_fields_invalid')).to.equal(true);
+ expect(ids.includes('index_fields_invalid')).toBe(true);
});
});
@@ -201,11 +217,11 @@ describe('ML - validateJob', () => {
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids.includes('index_fields_valid')).to.equal(true);
+ expect(ids.includes('index_fields_valid')).toBe(true);
});
});
- const getBasicPayload = () => ({
+ const getBasicPayload = (): any => ({
job: {
job_id: 'test',
analysis_config: {
@@ -214,7 +230,7 @@ describe('ML - validateJob', () => {
{
function: 'count',
},
- ],
+ ] as Array<{ function: string; by_field_name?: string; partition_field_name?: string }>,
influencers: [],
},
data_description: { time_field: '@timestamp' },
@@ -224,7 +240,7 @@ describe('ML - validateJob', () => {
});
it('throws an error because job.analysis_config.influencers is not an Array', done => {
- const payload = getBasicPayload();
+ const payload = getBasicPayload() as any;
delete payload.job.analysis_config.influencers;
validateJob(callWithRequest, payload).then(
@@ -237,11 +253,11 @@ describe('ML - validateJob', () => {
});
it('detect duplicate detectors', () => {
- const payload = getBasicPayload();
+ const payload = getBasicPayload() as any;
payload.job.analysis_config.detectors.push({ function: 'count' });
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql([
+ expect(ids).toStrictEqual([
'job_id_valid',
'detectors_function_not_empty',
'detectors_duplicates',
@@ -253,7 +269,7 @@ describe('ML - validateJob', () => {
});
it('dedupe duplicate messages', () => {
- const payload = getBasicPayload();
+ const payload = getBasicPayload() as any;
// in this test setup, the following configuration passes
// the duplicate detectors check, but would return the same
// 'field_not_aggregatable' message for both detectors.
@@ -264,7 +280,7 @@ describe('ML - validateJob', () => {
];
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql([
+ expect(ids).toStrictEqual([
'job_id_valid',
'detectors_function_not_empty',
'index_fields_valid',
@@ -274,11 +290,12 @@ describe('ML - validateJob', () => {
});
});
- it('basic validation passes, extended checks return some messages', () => {
+ // Failing https://github.com/elastic/kibana/issues/65865
+ it.skip('basic validation passes, extended checks return some messages', () => {
const payload = getBasicPayload();
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql([
+ expect(ids).toStrictEqual([
'job_id_valid',
'detectors_function_not_empty',
'index_fields_valid',
@@ -287,8 +304,9 @@ describe('ML - validateJob', () => {
});
});
- it('categorization job using mlcategory passes aggregatable field check', () => {
- const payload = {
+ // Failing https://github.com/elastic/kibana/issues/65866
+ it.skip('categorization job using mlcategory passes aggregatable field check', () => {
+ const payload: any = {
job: {
job_id: 'categorization_test',
analysis_config: {
@@ -310,7 +328,7 @@ describe('ML - validateJob', () => {
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql([
+ expect(ids).toStrictEqual([
'job_id_valid',
'detectors_function_not_empty',
'index_fields_valid',
@@ -322,7 +340,7 @@ describe('ML - validateJob', () => {
});
it('non-existent field reported as non aggregatable', () => {
- const payload = {
+ const payload: any = {
job: {
job_id: 'categorization_test',
analysis_config: {
@@ -343,7 +361,7 @@ describe('ML - validateJob', () => {
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql([
+ expect(ids).toStrictEqual([
'job_id_valid',
'detectors_function_not_empty',
'index_fields_valid',
@@ -353,8 +371,9 @@ describe('ML - validateJob', () => {
});
});
- it('script field not reported as non aggregatable', () => {
- const payload = {
+ // Failing https://github.com/elastic/kibana/issues/65867
+ it.skip('script field not reported as non aggregatable', () => {
+ const payload: any = {
job: {
job_id: 'categorization_test',
analysis_config: {
@@ -385,7 +404,7 @@ describe('ML - validateJob', () => {
return validateJob(callWithRequest, payload).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql([
+ expect(ids).toStrictEqual([
'job_id_valid',
'detectors_function_not_empty',
'index_fields_valid',
@@ -399,19 +418,19 @@ describe('ML - validateJob', () => {
// the following two tests validate the correct template rendering of
// urls in messages with {{version}} in them to be replaced with the
// specified version. (defaulting to 'current')
- const docsTestPayload = getBasicPayload();
+ const docsTestPayload = getBasicPayload() as any;
docsTestPayload.job.analysis_config.detectors = [{ function: 'count', by_field_name: 'airline' }];
it('creates a docs url pointing to the current docs version', () => {
return validateJob(callWithRequest, docsTestPayload).then(messages => {
const message = messages[messages.findIndex(m => m.id === 'field_not_aggregatable')];
- expect(message.url.search('/current/')).not.to.be(-1);
+ expect(message.url.search('/current/')).not.toBe(-1);
});
});
it('creates a docs url pointing to the master docs version', () => {
return validateJob(callWithRequest, docsTestPayload, 'master').then(messages => {
const message = messages[messages.findIndex(m => m.id === 'field_not_aggregatable')];
- expect(message.url.search('/master/')).not.to.be(-1);
+ expect(message.url.search('/master/')).not.toBe(-1);
});
});
});
diff --git a/x-pack/plugins/ml/server/models/job_validation/messages.d.ts b/x-pack/plugins/ml/server/models/job_validation/messages.d.ts
new file mode 100644
index 0000000000000..772d78b4187dd
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/job_validation/messages.d.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface ValidationMessage {
+ id: string;
+ url: string;
+}
diff --git a/x-pack/plugins/ml/server/models/job_validation/messages.js b/x-pack/plugins/ml/server/models/job_validation/messages.js
index 3fd90d0a356a1..6cdbc457e6ade 100644
--- a/x-pack/plugins/ml/server/models/job_validation/messages.js
+++ b/x-pack/plugins/ml/server/models/job_validation/messages.js
@@ -433,6 +433,17 @@ export const getMessages = () => {
}
),
},
+ mml_greater_than_effective_max_mml: {
+ status: 'WARNING',
+ text: i18n.translate(
+ 'xpack.ml.models.jobValidation.messages.mmlGreaterThanEffectiveMaxMmlMessage',
+ {
+ defaultMessage:
+ 'Job will not be able to run in the current cluster because model memory limit is higher than {effectiveMaxModelMemoryLimit}.',
+ values: { effectiveMaxModelMemoryLimit: '{{effectiveMaxModelMemoryLimit}}' },
+ }
+ ),
+ },
mml_greater_than_max_mml: {
status: 'ERROR',
text: i18n.translate('xpack.ml.models.jobValidation.messages.mmlGreaterThanMaxMmlMessage', {
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts
similarity index 81%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js
rename to x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts
index 3dc2bee1e8705..4001697d74320 100644
--- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js
+++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts
@@ -4,22 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import expect from '@kbn/expect';
-import { validateBucketSpan } from '../validate_bucket_span';
-import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../../common/constants/validation';
+import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../common/constants/validation';
+
+import { ValidationMessage } from './messages';
+// @ts-ignore
+import { validateBucketSpan } from './validate_bucket_span';
// farequote2017 snapshot snapshot mock search response
// it returns a mock for the response of PolledDataChecker's search request
// to get an aggregation of non_empty_buckets with an interval of 1m.
// this allows us to test bucket span estimation.
-import mockFareQuoteSearchResponse from './mock_farequote_search_response';
+import mockFareQuoteSearchResponse from './__mocks__/mock_farequote_search_response.json';
// it_ops_app_logs 2017 snapshot mock search response
// sparse data with a low number of buckets
-import mockItSearchResponse from './mock_it_search_response';
+import mockItSearchResponse from './__mocks__/mock_it_search_response.json';
// mock callWithRequestFactory
-const callWithRequestFactory = mockSearchResponse => {
+const callWithRequestFactory = (mockSearchResponse: any) => {
return () => {
return new Promise(resolve => {
resolve(mockSearchResponse);
@@ -86,17 +88,17 @@ describe('ML - validateBucketSpan', () => {
};
return validateBucketSpan(callWithRequestFactory(mockFareQuoteSearchResponse), job).then(
- messages => {
+ (messages: ValidationMessage[]) => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql([]);
+ expect(ids).toStrictEqual([]);
}
);
});
- const getJobConfig = bucketSpan => ({
+ const getJobConfig = (bucketSpan: string) => ({
analysis_config: {
bucket_span: bucketSpan,
- detectors: [],
+ detectors: [] as Array<{ function?: string }>,
influencers: [],
},
data_description: { time_field: '@timestamp' },
@@ -111,9 +113,9 @@ describe('ML - validateBucketSpan', () => {
callWithRequestFactory(mockFareQuoteSearchResponse),
job,
duration
- ).then(messages => {
+ ).then((messages: ValidationMessage[]) => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['success_bucket_span']);
+ expect(ids).toStrictEqual(['success_bucket_span']);
});
});
@@ -125,9 +127,9 @@ describe('ML - validateBucketSpan', () => {
callWithRequestFactory(mockFareQuoteSearchResponse),
job,
duration
- ).then(messages => {
+ ).then((messages: ValidationMessage[]) => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['bucket_span_high']);
+ expect(ids).toStrictEqual(['bucket_span_high']);
});
});
@@ -135,14 +137,18 @@ describe('ML - validateBucketSpan', () => {
return;
}
- const testBucketSpan = (bucketSpan, mockSearchResponse, test) => {
+ const testBucketSpan = (
+ bucketSpan: string,
+ mockSearchResponse: any,
+ test: (ids: string[]) => void
+ ) => {
const job = getJobConfig(bucketSpan);
job.analysis_config.detectors.push({
function: 'count',
});
return validateBucketSpan(callWithRequestFactory(mockSearchResponse), job, {}).then(
- messages => {
+ (messages: ValidationMessage[]) => {
const ids = messages.map(m => m.id);
test(ids);
}
@@ -151,13 +157,13 @@ describe('ML - validateBucketSpan', () => {
it('farequote count detector, bucket span estimation matches 15m', () => {
return testBucketSpan('15m', mockFareQuoteSearchResponse, ids => {
- expect(ids).to.eql(['success_bucket_span']);
+ expect(ids).toStrictEqual(['success_bucket_span']);
});
});
it('farequote count detector, bucket span estimation does not match 1m', () => {
return testBucketSpan('1m', mockFareQuoteSearchResponse, ids => {
- expect(ids).to.eql(['bucket_span_estimation_mismatch']);
+ expect(ids).toStrictEqual(['bucket_span_estimation_mismatch']);
});
});
@@ -167,7 +173,7 @@ describe('ML - validateBucketSpan', () => {
// should result in a lower bucket span estimation.
it('it_ops_app_logs count detector, bucket span estimation matches 6h', () => {
return testBucketSpan('6h', mockItSearchResponse, ids => {
- expect(ids).to.eql(['success_bucket_span']);
+ expect(ids).toStrictEqual(['success_bucket_span']);
});
});
});
diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts
index 22d2fec0beddc..2fad1252e6446 100644
--- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts
+++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts
@@ -7,4 +7,7 @@
import { APICaller } from 'kibana/server';
import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
-export function validateCardinality(callAsCurrentUser: APICaller, job: CombinedJob): any[];
+export function validateCardinality(
+ callAsCurrentUser: APICaller,
+ job?: CombinedJob
+): Promise;
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts
similarity index 69%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js
rename to x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts
index 9617982a66b0e..e5111629f1182 100644
--- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js
+++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts
@@ -5,11 +5,15 @@
*/
import _ from 'lodash';
-import expect from '@kbn/expect';
-import { validateCardinality } from '../validate_cardinality';
-import mockFareQuoteCardinality from './mock_farequote_cardinality';
-import mockFieldCaps from './mock_field_caps';
+import { APICaller } from 'kibana/server';
+
+import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
+
+import mockFareQuoteCardinality from './__mocks__/mock_farequote_cardinality.json';
+import mockFieldCaps from './__mocks__/mock_field_caps.json';
+
+import { validateCardinality } from './validate_cardinality';
const mockResponses = {
search: mockFareQuoteCardinality,
@@ -17,8 +21,8 @@ const mockResponses = {
};
// mock callWithRequestFactory
-const callWithRequestFactory = (responses, fail = false) => {
- return requestName => {
+const callWithRequestFactory = (responses: Record, fail = false): APICaller => {
+ return (requestName: string) => {
return new Promise((resolve, reject) => {
const response = responses[requestName];
if (fail) {
@@ -26,7 +30,7 @@ const callWithRequestFactory = (responses, fail = false) => {
} else {
resolve(response);
}
- });
+ }) as Promise;
};
};
@@ -39,21 +43,23 @@ describe('ML - validateCardinality', () => {
});
it('called with non-valid job argument #1, missing analysis_config', done => {
- validateCardinality(callWithRequestFactory(mockResponses), {}).then(
+ validateCardinality(callWithRequestFactory(mockResponses), {} as CombinedJob).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
);
});
it('called with non-valid job argument #2, missing datafeed_config', done => {
- validateCardinality(callWithRequestFactory(mockResponses), { analysis_config: {} }).then(
+ validateCardinality(callWithRequestFactory(mockResponses), {
+ analysis_config: {},
+ } as CombinedJob).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
);
});
it('called with non-valid job argument #3, missing datafeed_config.indices', done => {
- const job = { analysis_config: {}, datafeed_config: {} };
+ const job = { analysis_config: {}, datafeed_config: {} } as CombinedJob;
validateCardinality(callWithRequestFactory(mockResponses), job).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
@@ -61,7 +67,10 @@ describe('ML - validateCardinality', () => {
});
it('called with non-valid job argument #4, missing data_description', done => {
- const job = { analysis_config: {}, datafeed_config: { indices: [] } };
+ const job = ({
+ analysis_config: {},
+ datafeed_config: { indices: [] },
+ } as unknown) as CombinedJob;
validateCardinality(callWithRequestFactory(mockResponses), job).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
@@ -69,7 +78,11 @@ describe('ML - validateCardinality', () => {
});
it('called with non-valid job argument #5, missing data_description.time_field', done => {
- const job = { analysis_config: {}, data_description: {}, datafeed_config: { indices: [] } };
+ const job = ({
+ analysis_config: {},
+ data_description: {},
+ datafeed_config: { indices: [] },
+ } as unknown) as CombinedJob;
validateCardinality(callWithRequestFactory(mockResponses), job).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
@@ -77,11 +90,11 @@ describe('ML - validateCardinality', () => {
});
it('called with non-valid job argument #6, missing analysis_config.influencers', done => {
- const job = {
+ const job = ({
analysis_config: {},
datafeed_config: { indices: [] },
data_description: { time_field: '@timestamp' },
- };
+ } as unknown) as CombinedJob;
validateCardinality(callWithRequestFactory(mockResponses), job).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
@@ -89,21 +102,21 @@ describe('ML - validateCardinality', () => {
});
it('minimum job configuration to pass cardinality check code', () => {
- const job = {
+ const job = ({
analysis_config: { detectors: [], influencers: [] },
data_description: { time_field: '@timestamp' },
datafeed_config: {
indices: [],
},
- };
+ } as unknown) as CombinedJob;
return validateCardinality(callWithRequestFactory(mockResponses), job).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql([]);
+ expect(ids).toStrictEqual([]);
});
});
- const getJobConfig = fieldName => ({
+ const getJobConfig = (fieldName: string) => ({
analysis_config: {
detectors: [
{
@@ -119,11 +132,18 @@ describe('ML - validateCardinality', () => {
},
});
- const testCardinality = (fieldName, cardinality, test) => {
+ const testCardinality = (
+ fieldName: string,
+ cardinality: number,
+ test: (ids: string[]) => void
+ ) => {
const job = getJobConfig(fieldName);
const mockCardinality = _.cloneDeep(mockResponses);
mockCardinality.search.aggregations.airline_cardinality.value = cardinality;
- return validateCardinality(callWithRequestFactory(mockCardinality), job, {}).then(messages => {
+ return validateCardinality(
+ callWithRequestFactory(mockCardinality),
+ (job as unknown) as CombinedJob
+ ).then(messages => {
const ids = messages.map(m => m.id);
test(ids);
});
@@ -132,26 +152,34 @@ describe('ML - validateCardinality', () => {
it(`field '_source' not aggregatable`, () => {
const job = getJobConfig('partition_field_name');
job.analysis_config.detectors[0].partition_field_name = '_source';
- return validateCardinality(callWithRequestFactory(mockResponses), job).then(messages => {
+ return validateCardinality(
+ callWithRequestFactory(mockResponses),
+ (job as unknown) as CombinedJob
+ ).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['field_not_aggregatable']);
+ expect(ids).toStrictEqual(['field_not_aggregatable']);
});
});
it(`field 'airline' aggregatable`, () => {
const job = getJobConfig('partition_field_name');
- return validateCardinality(callWithRequestFactory(mockResponses), job).then(messages => {
+ return validateCardinality(
+ callWithRequestFactory(mockResponses),
+ (job as unknown) as CombinedJob
+ ).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['success_cardinality']);
+ expect(ids).toStrictEqual(['success_cardinality']);
});
});
it('field not aggregatable', () => {
const job = getJobConfig('partition_field_name');
- return validateCardinality(callWithRequestFactory({}), job).then(messages => {
- const ids = messages.map(m => m.id);
- expect(ids).to.eql(['field_not_aggregatable']);
- });
+ return validateCardinality(callWithRequestFactory({}), (job as unknown) as CombinedJob).then(
+ messages => {
+ const ids = messages.map(m => m.id);
+ expect(ids).toStrictEqual(['field_not_aggregatable']);
+ }
+ );
});
it('fields not aggregatable', () => {
@@ -160,107 +188,110 @@ describe('ML - validateCardinality', () => {
function: 'count',
partition_field_name: 'airline',
});
- return validateCardinality(callWithRequestFactory({}, true), job).then(messages => {
+ return validateCardinality(
+ callWithRequestFactory({}, true),
+ (job as unknown) as CombinedJob
+ ).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['fields_not_aggregatable']);
+ expect(ids).toStrictEqual(['fields_not_aggregatable']);
});
});
it('valid partition field cardinality', () => {
return testCardinality('partition_field_name', 50, ids => {
- expect(ids).to.eql(['success_cardinality']);
+ expect(ids).toStrictEqual(['success_cardinality']);
});
});
it('too high partition field cardinality', () => {
return testCardinality('partition_field_name', 1001, ids => {
- expect(ids).to.eql(['cardinality_partition_field']);
+ expect(ids).toStrictEqual(['cardinality_partition_field']);
});
});
it('valid by field cardinality', () => {
return testCardinality('by_field_name', 50, ids => {
- expect(ids).to.eql(['success_cardinality']);
+ expect(ids).toStrictEqual(['success_cardinality']);
});
});
it('too high by field cardinality', () => {
return testCardinality('by_field_name', 1001, ids => {
- expect(ids).to.eql(['cardinality_by_field']);
+ expect(ids).toStrictEqual(['cardinality_by_field']);
});
});
it('valid over field cardinality', () => {
return testCardinality('over_field_name', 50, ids => {
- expect(ids).to.eql(['success_cardinality']);
+ expect(ids).toStrictEqual(['success_cardinality']);
});
});
it('too low over field cardinality', () => {
return testCardinality('over_field_name', 9, ids => {
- expect(ids).to.eql(['cardinality_over_field_low']);
+ expect(ids).toStrictEqual(['cardinality_over_field_low']);
});
});
it('too high over field cardinality', () => {
return testCardinality('over_field_name', 1000001, ids => {
- expect(ids).to.eql(['cardinality_over_field_high']);
+ expect(ids).toStrictEqual(['cardinality_over_field_high']);
});
});
const cardinality = 10000;
it(`disabled model_plot, over field cardinality of ${cardinality} doesn't trigger a warning`, () => {
- const job = getJobConfig('over_field_name');
+ const job = (getJobConfig('over_field_name') as unknown) as CombinedJob;
job.model_plot_config = { enabled: false };
const mockCardinality = _.cloneDeep(mockResponses);
mockCardinality.search.aggregations.airline_cardinality.value = cardinality;
return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['success_cardinality']);
+ expect(ids).toStrictEqual(['success_cardinality']);
});
});
it(`enabled model_plot, over field cardinality of ${cardinality} triggers a model plot warning`, () => {
- const job = getJobConfig('over_field_name');
+ const job = (getJobConfig('over_field_name') as unknown) as CombinedJob;
job.model_plot_config = { enabled: true };
const mockCardinality = _.cloneDeep(mockResponses);
mockCardinality.search.aggregations.airline_cardinality.value = cardinality;
return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['cardinality_model_plot_high']);
+ expect(ids).toStrictEqual(['cardinality_model_plot_high']);
});
});
it(`disabled model_plot, by field cardinality of ${cardinality} triggers a field cardinality warning`, () => {
- const job = getJobConfig('by_field_name');
+ const job = (getJobConfig('by_field_name') as unknown) as CombinedJob;
job.model_plot_config = { enabled: false };
const mockCardinality = _.cloneDeep(mockResponses);
mockCardinality.search.aggregations.airline_cardinality.value = cardinality;
return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['cardinality_by_field']);
+ expect(ids).toStrictEqual(['cardinality_by_field']);
});
});
it(`enabled model_plot, by field cardinality of ${cardinality} triggers a model plot warning and field cardinality warning`, () => {
- const job = getJobConfig('by_field_name');
+ const job = (getJobConfig('by_field_name') as unknown) as CombinedJob;
job.model_plot_config = { enabled: true };
const mockCardinality = _.cloneDeep(mockResponses);
mockCardinality.search.aggregations.airline_cardinality.value = cardinality;
return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['cardinality_model_plot_high', 'cardinality_by_field']);
+ expect(ids).toStrictEqual(['cardinality_model_plot_high', 'cardinality_by_field']);
});
});
it(`enabled model_plot with terms, by field cardinality of ${cardinality} triggers just field cardinality warning`, () => {
- const job = getJobConfig('by_field_name');
+ const job = (getJobConfig('by_field_name') as unknown) as CombinedJob;
job.model_plot_config = { enabled: true, terms: 'AAL,AAB' };
const mockCardinality = _.cloneDeep(mockResponses);
mockCardinality.search.aggregations.airline_cardinality.value = cardinality;
return validateCardinality(callWithRequestFactory(mockCardinality), job).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['cardinality_by_field']);
+ expect(ids).toStrictEqual(['cardinality_by_field']);
});
});
});
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.test.ts
similarity index 63%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js
rename to x-pack/plugins/ml/server/models/job_validation/validate_influencers.test.ts
index 06b2e5205fdbd..df3310ad9f5e8 100644
--- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js
+++ b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.test.ts
@@ -4,19 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import expect from '@kbn/expect';
-import { validateInfluencers } from '../validate_influencers';
+import { APICaller } from 'kibana/server';
+
+import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
+
+import { validateInfluencers } from './validate_influencers';
describe('ML - validateInfluencers', () => {
it('called without arguments throws an error', done => {
- validateInfluencers().then(
+ validateInfluencers(
+ (undefined as unknown) as APICaller,
+ (undefined as unknown) as CombinedJob
+ ).then(
() => done(new Error('Promise should not resolve for this test without job argument.')),
() => done()
);
});
it('called with non-valid job argument #1, missing analysis_config', done => {
- validateInfluencers(undefined, {}).then(
+ validateInfluencers((undefined as unknown) as APICaller, ({} as unknown) as CombinedJob).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
);
@@ -28,7 +34,7 @@ describe('ML - validateInfluencers', () => {
datafeed_config: { indices: [] },
data_description: { time_field: '@timestamp' },
};
- validateInfluencers(undefined, job).then(
+ validateInfluencers((undefined as unknown) as APICaller, (job as unknown) as CombinedJob).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
);
@@ -40,25 +46,29 @@ describe('ML - validateInfluencers', () => {
datafeed_config: { indices: [] },
data_description: { time_field: '@timestamp' },
};
- validateInfluencers(undefined, job).then(
+ validateInfluencers((undefined as unknown) as APICaller, (job as unknown) as CombinedJob).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
);
});
- const getJobConfig = (influencers = [], detectors = []) => ({
- analysis_config: { detectors, influencers },
- data_description: { time_field: '@timestamp' },
- datafeed_config: {
- indices: [],
- },
- });
+ const getJobConfig: (
+ influencers?: string[],
+ detectors?: CombinedJob['analysis_config']['detectors']
+ ) => CombinedJob = (influencers = [], detectors = []) =>
+ (({
+ analysis_config: { detectors, influencers },
+ data_description: { time_field: '@timestamp' },
+ datafeed_config: {
+ indices: [],
+ },
+ } as unknown) as CombinedJob);
it('success_influencer', () => {
const job = getJobConfig(['airline']);
- return validateInfluencers(undefined, job).then(messages => {
+ return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['success_influencers']);
+ expect(ids).toStrictEqual(['success_influencers']);
});
});
@@ -69,31 +79,30 @@ describe('ML - validateInfluencers', () => {
{
detector_description: 'count',
function: 'count',
- rules: [],
detector_index: 0,
},
]
);
- return validateInfluencers(undefined, job).then(messages => {
+ return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql([]);
+ expect(ids).toStrictEqual([]);
});
});
it('influencer_low', () => {
const job = getJobConfig();
- return validateInfluencers(undefined, job).then(messages => {
+ return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['influencer_low']);
+ expect(ids).toStrictEqual(['influencer_low']);
});
});
it('influencer_high', () => {
const job = getJobConfig(['i1', 'i2', 'i3', 'i4']);
- return validateInfluencers(undefined, job).then(messages => {
+ return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['influencer_high']);
+ expect(ids).toStrictEqual(['influencer_high']);
});
});
@@ -105,14 +114,13 @@ describe('ML - validateInfluencers', () => {
detector_description: 'count',
function: 'count',
partition_field_name: 'airline',
- rules: [],
detector_index: 0,
},
]
);
- return validateInfluencers(undefined, job).then(messages => {
+ return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['influencer_low_suggestion']);
+ expect(ids).toStrictEqual(['influencer_low_suggestion']);
});
});
@@ -124,27 +132,24 @@ describe('ML - validateInfluencers', () => {
detector_description: 'count',
function: 'count',
partition_field_name: 'partition_field',
- rules: [],
detector_index: 0,
},
{
detector_description: 'count',
function: 'count',
by_field_name: 'by_field',
- rules: [],
detector_index: 0,
},
{
detector_description: 'count',
function: 'count',
over_field_name: 'over_field',
- rules: [],
detector_index: 0,
},
]
);
- return validateInfluencers(undefined, job).then(messages => {
- expect(messages).to.eql([
+ return validateInfluencers((undefined as unknown) as APICaller, job).then(messages => {
+ expect(messages).toStrictEqual([
{
id: 'influencer_low_suggestions',
influencerSuggestion: '["partition_field","by_field","over_field"]',
diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_influencers.js b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.ts
similarity index 89%
rename from x-pack/plugins/ml/server/models/job_validation/validate_influencers.js
rename to x-pack/plugins/ml/server/models/job_validation/validate_influencers.ts
index 60fd5c37b9958..e54ffc4586a8e 100644
--- a/x-pack/plugins/ml/server/models/job_validation/validate_influencers.js
+++ b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.ts
@@ -4,19 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { APICaller } from 'kibana/server';
+
+import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
+
import { validateJobObject } from './validate_job_object';
const INFLUENCER_LOW_THRESHOLD = 0;
const INFLUENCER_HIGH_THRESHOLD = 4;
const DETECTOR_FIELD_NAMES_THRESHOLD = 1;
-export async function validateInfluencers(callWithRequest, job) {
+export async function validateInfluencers(callWithRequest: APICaller, job: CombinedJob) {
validateJobObject(job);
const messages = [];
const influencers = job.analysis_config.influencers;
- const detectorFieldNames = [];
+ const detectorFieldNames: string[] = [];
job.analysis_config.detectors.forEach(d => {
if (d.by_field_name) {
detectorFieldNames.push(d.by_field_name);
diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts
index 6b5d5614325bf..bf88716181bb3 100644
--- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts
+++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts
@@ -24,6 +24,7 @@ describe('ML - validateModelMemoryLimit', () => {
},
limits: {
max_model_memory_limit: '30mb',
+ effective_max_model_memory_limit: '40mb',
},
};
@@ -211,6 +212,30 @@ describe('ML - validateModelMemoryLimit', () => {
});
});
+ it('Called with no duration or split and mml above limit, no max setting', () => {
+ const job = getJobConfig();
+ const duration = undefined;
+ // @ts-ignore
+ job.analysis_limits.model_memory_limit = '31mb';
+
+ return validateModelMemoryLimit(getMockCallWithRequest(), job, duration).then(messages => {
+ const ids = messages.map(m => m.id);
+ expect(ids).toEqual([]);
+ });
+ });
+
+ it('Called with no duration or split and mml above limit, no max setting, above effective max mml', () => {
+ const job = getJobConfig();
+ const duration = undefined;
+ // @ts-ignore
+ job.analysis_limits.model_memory_limit = '41mb';
+
+ return validateModelMemoryLimit(getMockCallWithRequest(), job, duration).then(messages => {
+ const ids = messages.map(m => m.id);
+ expect(ids).toEqual(['mml_greater_than_effective_max_mml']);
+ });
+ });
+
it('Called with small number of detectors, so estimated mml is under specified mml, no max setting', () => {
const dtrs = createDetectors(1);
const job = getJobConfig(['instance'], dtrs);
diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts
index 16a48addfeaf4..5c3250af6ef46 100644
--- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts
+++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts
@@ -10,6 +10,7 @@ import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
import { validateJobObject } from './validate_job_object';
import { calculateModelMemoryLimitProvider } from '../calculate_model_memory_limit';
import { ALLOWED_DATA_UNITS } from '../../../common/constants/validation';
+import { MlInfoResponse } from '../../../common/types/ml_server_info';
// The minimum value the backend expects is 1MByte
const MODEL_MEMORY_LIMIT_MINIMUM_BYTES = 1048576;
@@ -50,9 +51,9 @@ export async function validateModelMemoryLimit(
// retrieve the max_model_memory_limit value from the server
// this will be unset unless the user has set this on their cluster
- const maxModelMemoryLimit: string | undefined = (
- await callWithRequest('ml.info')
- )?.limits?.max_model_memory_limit?.toUpperCase();
+ const info = await callWithRequest('ml.info');
+ const maxModelMemoryLimit = info.limits.max_model_memory_limit?.toUpperCase();
+ const effectiveMaxModelMemoryLimit = info.limits.effective_max_model_memory_limit?.toUpperCase();
if (runCalcModelMemoryTest) {
const { modelMemoryLimit } = await calculateModelMemoryLimitProvider(callWithRequest)(
@@ -113,17 +114,35 @@ export async function validateModelMemoryLimit(
// if max_model_memory_limit has been set,
// make sure the user defined MML is not greater than it
- if (maxModelMemoryLimit !== undefined && mml !== null) {
- // @ts-ignore
- const maxMmlBytes = numeral(maxModelMemoryLimit).value();
+ if (mml !== null) {
+ let maxMmlExceeded = false;
// @ts-ignore
const mmlBytes = numeral(mml).value();
- if (mmlBytes > maxMmlBytes) {
- messages.push({
- id: 'mml_greater_than_max_mml',
- maxModelMemoryLimit,
- mml,
- });
+
+ if (maxModelMemoryLimit !== undefined) {
+ // @ts-ignore
+ const maxMmlBytes = numeral(maxModelMemoryLimit).value();
+ if (mmlBytes > maxMmlBytes) {
+ maxMmlExceeded = true;
+ messages.push({
+ id: 'mml_greater_than_max_mml',
+ maxModelMemoryLimit,
+ mml,
+ });
+ }
+ }
+
+ if (effectiveMaxModelMemoryLimit !== undefined && maxMmlExceeded === false) {
+ // @ts-ignore
+ const effectiveMaxMmlBytes = numeral(effectiveMaxModelMemoryLimit).value();
+ if (mmlBytes > effectiveMaxMmlBytes) {
+ messages.push({
+ id: 'mml_greater_than_effective_max_mml',
+ maxModelMemoryLimit,
+ mml,
+ effectiveMaxModelMemoryLimit,
+ });
+ }
}
}
diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts
similarity index 76%
rename from x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js
rename to x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts
index e3ef62e507485..2c3b2dd4dc6ae 100644
--- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js
+++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts
@@ -5,28 +5,32 @@
*/
import _ from 'lodash';
-import expect from '@kbn/expect';
-import { isValidTimeField, validateTimeRange } from '../validate_time_range';
-import mockTimeField from './mock_time_field';
-import mockTimeFieldNested from './mock_time_field_nested';
-import mockTimeRange from './mock_time_range';
+import { APICaller } from 'kibana/server';
+
+import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
+
+import { isValidTimeField, validateTimeRange } from './validate_time_range';
+
+import mockTimeField from './__mocks__/mock_time_field.json';
+import mockTimeFieldNested from './__mocks__/mock_time_field_nested.json';
+import mockTimeRange from './__mocks__/mock_time_range.json';
const mockSearchResponse = {
fieldCaps: mockTimeField,
search: mockTimeRange,
};
-const callWithRequestFactory = resp => {
- return path => {
+const callWithRequestFactory = (resp: any): APICaller => {
+ return (path: string) => {
return new Promise(resolve => {
resolve(resp[path]);
- });
+ }) as Promise;
};
};
function getMinimalValidJob() {
- return {
+ return ({
analysis_config: {
bucket_span: '15m',
detectors: [],
@@ -36,12 +40,15 @@ function getMinimalValidJob() {
datafeed_config: {
indices: [],
},
- };
+ } as unknown) as CombinedJob;
}
describe('ML - isValidTimeField', () => {
it('called without job config argument triggers Promise rejection', done => {
- isValidTimeField(callWithRequestFactory(mockSearchResponse)).then(
+ isValidTimeField(
+ callWithRequestFactory(mockSearchResponse),
+ (undefined as unknown) as CombinedJob
+ ).then(
() => done(new Error('Promise should not resolve for this test without job argument.')),
() => done()
);
@@ -50,7 +57,7 @@ describe('ML - isValidTimeField', () => {
it('time_field `@timestamp`', done => {
isValidTimeField(callWithRequestFactory(mockSearchResponse), getMinimalValidJob()).then(
valid => {
- expect(valid).to.be(true);
+ expect(valid).toBe(true);
done();
},
() => done(new Error('isValidTimeField Promise failed for time_field `@timestamp`.'))
@@ -71,7 +78,7 @@ describe('ML - isValidTimeField', () => {
mockJobConfigNestedDate
).then(
valid => {
- expect(valid).to.be(true);
+ expect(valid).toBe(true);
done();
},
() => done(new Error('isValidTimeField Promise failed for time_field `metadata.timestamp`.'))
@@ -81,14 +88,19 @@ describe('ML - isValidTimeField', () => {
describe('ML - validateTimeRange', () => {
it('called without arguments', done => {
- validateTimeRange(callWithRequestFactory(mockSearchResponse)).then(
+ validateTimeRange(
+ callWithRequestFactory(mockSearchResponse),
+ (undefined as unknown) as CombinedJob
+ ).then(
() => done(new Error('Promise should not resolve for this test without job argument.')),
() => done()
);
});
it('called with non-valid job argument #2, missing datafeed_config', done => {
- validateTimeRange(callWithRequestFactory(mockSearchResponse), { analysis_config: {} }).then(
+ validateTimeRange(callWithRequestFactory(mockSearchResponse), ({
+ analysis_config: {},
+ } as unknown) as CombinedJob).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
);
@@ -96,7 +108,10 @@ describe('ML - validateTimeRange', () => {
it('called with non-valid job argument #3, missing datafeed_config.indices', done => {
const job = { analysis_config: {}, datafeed_config: {} };
- validateTimeRange(callWithRequestFactory(mockSearchResponse), job).then(
+ validateTimeRange(
+ callWithRequestFactory(mockSearchResponse),
+ (job as unknown) as CombinedJob
+ ).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
);
@@ -104,7 +119,10 @@ describe('ML - validateTimeRange', () => {
it('called with non-valid job argument #4, missing data_description', done => {
const job = { analysis_config: {}, datafeed_config: { indices: [] } };
- validateTimeRange(callWithRequestFactory(mockSearchResponse), job).then(
+ validateTimeRange(
+ callWithRequestFactory(mockSearchResponse),
+ (job as unknown) as CombinedJob
+ ).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
);
@@ -112,7 +130,10 @@ describe('ML - validateTimeRange', () => {
it('called with non-valid job argument #5, missing data_description.time_field', done => {
const job = { analysis_config: {}, data_description: {}, datafeed_config: { indices: [] } };
- validateTimeRange(callWithRequestFactory(mockSearchResponse), job).then(
+ validateTimeRange(
+ callWithRequestFactory(mockSearchResponse),
+ (job as unknown) as CombinedJob
+ ).then(
() => done(new Error('Promise should not resolve for this test without valid job argument.')),
() => done()
);
@@ -128,7 +149,7 @@ describe('ML - validateTimeRange', () => {
duration
).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['time_field_invalid']);
+ expect(ids).toStrictEqual(['time_field_invalid']);
});
});
@@ -142,7 +163,7 @@ describe('ML - validateTimeRange', () => {
duration
).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['time_range_short']);
+ expect(ids).toStrictEqual(['time_range_short']);
});
});
@@ -154,7 +175,7 @@ describe('ML - validateTimeRange', () => {
duration
).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['time_range_short']);
+ expect(ids).toStrictEqual(['time_range_short']);
});
});
@@ -166,7 +187,7 @@ describe('ML - validateTimeRange', () => {
duration
).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['time_range_short']);
+ expect(ids).toStrictEqual(['time_range_short']);
});
});
@@ -178,7 +199,7 @@ describe('ML - validateTimeRange', () => {
duration
).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['success_time_range']);
+ expect(ids).toStrictEqual(['success_time_range']);
});
});
@@ -190,7 +211,7 @@ describe('ML - validateTimeRange', () => {
duration
).then(messages => {
const ids = messages.map(m => m.id);
- expect(ids).to.eql(['time_range_before_epoch']);
+ expect(ids).toStrictEqual(['time_range_before_epoch']);
});
});
});
diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts
index 5f73438769851..4fb09af94dcc6 100644
--- a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts
+++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts
@@ -37,9 +37,9 @@ export async function isValidTimeField(callAsCurrentUser: APICaller, job: Combin
fields: [timeField],
});
- let fieldType = fieldCaps.fields[timeField]?.date?.type;
+ let fieldType = fieldCaps?.fields[timeField]?.date?.type;
if (fieldType === undefined) {
- fieldType = fieldCaps.fields[timeField]?.date_nanos?.type;
+ fieldType = fieldCaps?.fields[timeField]?.date_nanos?.type;
}
return fieldType === ES_FIELD_TYPES.DATE || fieldType === ES_FIELD_TYPES.DATE_NANOS;
}
@@ -47,7 +47,7 @@ export async function isValidTimeField(callAsCurrentUser: APICaller, job: Combin
export async function validateTimeRange(
callAsCurrentUser: APICaller,
job: CombinedJob,
- timeRange: TimeRange | undefined
+ timeRange?: TimeRange
) {
const messages: ValidateTimeRangeMessage[] = [];
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js
index b90a9aa7d139a..0722a80dc2c11 100644
--- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js
@@ -140,7 +140,7 @@ export class BulkUploader {
async _fetchAndUpload(usageCollection) {
const collectorsReady = await usageCollection.areAllCollectorsReady();
const hasUsageCollectors = usageCollection.some(usageCollection.isUsageCollector);
- if (!collectorsReady) {
+ if (!collectorsReady || typeof this.kibanaStatusGetter !== 'function') {
this._log.debug('Skipping bulk uploading because not all collectors are ready');
if (hasUsageCollectors) {
this._lastFetchUsageTime = null;
@@ -151,7 +151,7 @@ export class BulkUploader {
const data = await usageCollection.bulkFetch(this._cluster.callAsInternalUser);
const payload = this.toBulkUploadFormat(compact(data), usageCollection);
- if (payload) {
+ if (payload && payload.length > 0) {
try {
this._log.debug(`Uploading bulk stats payload to the local cluster`);
const result = await this._onPayload(payload);
@@ -244,7 +244,7 @@ export class BulkUploader {
*/
toBulkUploadFormat(rawData, usageCollection) {
if (rawData.length === 0) {
- return;
+ return [];
}
// convert the raw data to a nested object by taking each payload through
diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details.tsx
index ac2a2997515d5..6579d18556cc0 100644
--- a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details.tsx
+++ b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details.tsx
@@ -44,6 +44,7 @@ export const ShardDetails = ({ index, shard, operations }: Props) => {
setShardVisibility(!shardVisibility)}
+ data-test-subj="openCloseShardDetails"
>
[{shard.id[0]}][
{shard.id[2]}]
diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details_tree_node.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details_tree_node.tsx
index 1d8f915d3d47d..d89046090a961 100644
--- a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details_tree_node.tsx
+++ b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/shard_details/shard_details_tree_node.tsx
@@ -94,6 +94,7 @@ export const ShardDetailsTreeNode = ({ operation, index, shard }: Props) => {
highlight({ indexName: index.name, operation, shard })}
>
{i18n.translate('xpack.searchProfiler.profileTree.body.viewDetailsLabel', {
diff --git a/x-pack/plugins/searchprofiler/public/application/components/searchprofiler_tabs.tsx b/x-pack/plugins/searchprofiler/public/application/components/searchprofiler_tabs.tsx
index 19224e7099fd6..7e6dad7df5528 100644
--- a/x-pack/plugins/searchprofiler/public/application/components/searchprofiler_tabs.tsx
+++ b/x-pack/plugins/searchprofiler/public/application/components/searchprofiler_tabs.tsx
@@ -24,6 +24,7 @@ export const SearchProfilerTabs = ({ activeTab, activateTab, has }: Props) => {
return (
activateTab('searches')}
@@ -33,6 +34,7 @@ export const SearchProfilerTabs = ({ activeTab, activateTab, has }: Props) => {
})}
activateTab('aggregations')}
diff --git a/x-pack/plugins/searchprofiler/public/application/containers/profile_query_editor.tsx b/x-pack/plugins/searchprofiler/public/application/containers/profile_query_editor.tsx
index 5348c55ad5213..f6377d2b4f906 100644
--- a/x-pack/plugins/searchprofiler/public/application/containers/profile_query_editor.tsx
+++ b/x-pack/plugins/searchprofiler/public/application/containers/profile_query_editor.tsx
@@ -120,7 +120,12 @@ export const ProfileQueryEditor = memo(() => {
- handleProfileClick()}>
+ handleProfileClick()}
+ >
{i18n.translate('xpack.searchProfiler.formProfileButtonLabel', {
defaultMessage: 'Profile',
diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx
index b48cc546fe78c..7c9accd4cef49 100644
--- a/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx
+++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx
@@ -7,6 +7,7 @@
import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiToolTip } from '@elastic/eui';
import { isString, isEmpty } from 'lodash/fp';
import React from 'react';
+import styled from 'styled-components';
import { DefaultDraggable } from '../../../draggables';
import { getEmptyTagValue } from '../../../empty_value';
@@ -18,6 +19,10 @@ import endPointSvg from '../../../../utils/logo_endpoint/64_color.svg';
import * as i18n from './translations';
+const EventModuleFlexItem = styled(EuiFlexItem)`
+ width: 100%;
+`;
+
export const renderRuleName = ({
contextId,
eventId,
@@ -87,7 +92,7 @@ export const renderEventModule = ({
endpointRefUrl != null && !isEmpty(endpointRefUrl) ? 'flexStart' : 'spaceBetween'
}
>
-
+
{content}
-
+
{endpointRefUrl != null && canYouAddEndpointLogo(moduleName, endpointRefUrl) && (
(
const popover = useMemo(() => {
return (
- (
panelPaddingSize={!alwaysShow ? 's' : 'none'}
>
{isOpen ? hoverContent : null}
-
+
);
}, [content, onMouseLeave, isOpen, alwaysShow, hoverContent]);
diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx
index 9c3d1c90e67d7..337ca2e3c918e 100644
--- a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx
+++ b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx
@@ -109,3 +109,6 @@ export const JiraConnectorFlyout = withConnectorFlyout({
configKeys: ['projectKey'],
connectorActionTypeId: '.jira',
});
+
+// eslint-disable-next-line import/no-default-export
+export { JiraConnectorFlyout as default };
diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx
index ada9608e37c98..049ccb7cf17b7 100644
--- a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx
+++ b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { lazy } from 'react';
import {
ValidationResult,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
@@ -13,7 +14,6 @@ import { connector } from './config';
import { createActionType } from '../utils';
import logo from './logo.svg';
import { JiraActionConnector } from './types';
-import { JiraConnectorFlyout } from './flyout';
import * as i18n from './translations';
interface Errors {
@@ -50,5 +50,5 @@ export const getActionType = createActionType({
selectMessage: i18n.JIRA_DESC,
actionTypeTitle: connector.name,
validateConnector,
- actionConnectorFields: JiraConnectorFlyout,
+ actionConnectorFields: lazy(() => import('./flyout')),
});
diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx
index 5d5d08dacf90c..2783e988a6405 100644
--- a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx
+++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx
@@ -82,3 +82,6 @@ export const ServiceNowConnectorFlyout = withConnectorFlyout import('./flyout')),
});
diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts
index ffb013c347e59..3d3692c9806e4 100644
--- a/x-pack/plugins/siem/public/lib/connectors/types.ts
+++ b/x-pack/plugins/siem/public/lib/connectors/types.ts
@@ -8,6 +8,7 @@
/* eslint-disable @kbn/eslint/no-restricted-paths */
import { ActionType } from '../../../../triggers_actions_ui/public';
+import { IErrorObject } from '../../../../triggers_actions_ui/public/types';
import { ExternalIncidentServiceConfiguration } from '../../../../actions/server/builtin_action_types/case/types';
import { ActionType as ThirdPartySupportedActions, CaseField } from '../../../../case/common/api';
@@ -42,7 +43,7 @@ export interface ActionConnectorValidationErrors {
export type Optional = Omit & Partial;
export interface ConnectorFlyoutFormProps {
- errors: { [key: string]: string[] };
+ errors: IErrorObject;
action: T;
onChangeSecret: (key: string, value: string) => void;
onBlurSecret: (key: string) => void;
diff --git a/x-pack/plugins/siem/public/lib/connectors/utils.ts b/x-pack/plugins/siem/public/lib/connectors/utils.ts
index 169b4758876e8..cc1608a05e2ce 100644
--- a/x-pack/plugins/siem/public/lib/connectors/utils.ts
+++ b/x-pack/plugins/siem/public/lib/connectors/utils.ts
@@ -7,7 +7,6 @@
import {
ActionTypeModel,
ValidationResult,
- ActionParamsProps,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../triggers_actions_ui/public/types';
@@ -31,7 +30,7 @@ export const createActionType = ({
validateConnector,
validateParams = connectorParamsValidator,
actionConnectorFields,
- actionParamsFields = ConnectorParamsFields,
+ actionParamsFields = null,
}: Optional) => (): ActionTypeModel => {
return {
id,
@@ -59,15 +58,6 @@ export const createActionType = ({
};
};
-const ConnectorParamsFields: React.FunctionComponent> = ({
- actionParams,
- editAction,
- index,
- errors,
-}) => {
- return null;
-};
-
const connectorParamsValidator = (actionParams: ActionConnectorParams): ValidationResult => {
return { errors: {} };
};
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts
index 80594ca74a353..30362392898d1 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts
@@ -7,10 +7,18 @@
import { getSignalsTemplate } from './get_signals_template';
describe('get_signals_template', () => {
- test('it should set the lifecycle name and the rollover alias to be the name of the index passed in', () => {
+ test('it should set the lifecycle "name" and "rollover_alias" to be the name of the index passed in', () => {
const template = getSignalsTemplate('test-index');
expect(template.settings).toEqual({
- index: { lifecycle: { name: 'test-index', rollover_alias: 'test-index' } },
+ index: {
+ lifecycle: {
+ name: 'test-index',
+ rollover_alias: 'test-index',
+ },
+ },
+ mapping: {
+ total_fields: { limit: 10000 },
+ },
});
});
@@ -28,4 +36,9 @@ describe('get_signals_template', () => {
const template = getSignalsTemplate('test-index');
expect(typeof template.mappings.properties.signal).toEqual('object');
});
+
+ test('it should have a "total_fields" section that is at least 10k in size', () => {
+ const template = getSignalsTemplate('test-index');
+ expect(template.settings.mapping.total_fields.limit).toBeGreaterThanOrEqual(10000);
+ });
});
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts
index c6580f0bdda42..01d7182e253ce 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts
@@ -17,6 +17,11 @@ export const getSignalsTemplate = (index: string) => {
rollover_alias: index,
},
},
+ mapping: {
+ total_fields: {
+ limit: 10000,
+ },
+ },
},
index_patterns: [`${index}-*`],
mappings: ecsMapping.mappings,
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
index e7db228225880..91685a68a60ae 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
@@ -122,20 +122,11 @@ describe('import_rules_route', () => {
clients.siemClient.getSignalsIndex.mockReturnValue('mockSignalsIndex');
clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex());
const response = await server.inject(request, context);
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(400);
expect(response.body).toEqual({
- errors: [
- {
- error: {
- message:
- 'To create a rule, the index must exist first. Index mockSignalsIndex does not exist',
- status_code: 409,
- },
- rule_id: 'rule-1',
- },
- ],
- success: false,
- success_count: 0,
+ message:
+ 'To create a rule, the index must exist first. Index mockSignalsIndex does not exist',
+ status_code: 400,
});
});
@@ -145,19 +136,10 @@ describe('import_rules_route', () => {
});
const response = await server.inject(request, context);
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(500);
expect(response.body).toEqual({
- errors: [
- {
- error: {
- message: 'Test error',
- status_code: 400,
- },
- rule_id: 'rule-1',
- },
- ],
- success: false,
- success_count: 0,
+ message: 'Test error',
+ status_code: 500,
});
});
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
index 4d86f0bec6502..9ba083ae48086 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
@@ -75,6 +75,14 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => {
body: `Invalid file extension ${fileExtension}`,
});
}
+ const signalsIndex = siemClient.getSignalsIndex();
+ const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, signalsIndex);
+ if (!indexExists) {
+ return siemResponse.error({
+ statusCode: 400,
+ body: `To create a rule, the index must exist first. Index ${signalsIndex} does not exist`,
+ });
+ }
const objectLimit = config.maxRuleImportExportSize;
const readStream = createRulesStreamFromNdJson(objectLimit);
@@ -94,166 +102,150 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => {
const batchParseObjects = chunkParseObjects.shift() ?? [];
const newImportRuleResponse = await Promise.all(
batchParseObjects.reduce>>((accum, parsedRule) => {
- const importsWorkerPromise = new Promise(
- async (resolve, reject) => {
- if (parsedRule instanceof Error) {
- // If the JSON object had a validation or parse error then we return
- // early with the error and an (unknown) for the ruleId
- resolve(
- createBulkErrorObject({
- statusCode: 400,
- message: parsedRule.message,
- })
- );
- return null;
- }
- const {
- anomaly_threshold: anomalyThreshold,
- description,
- enabled,
- false_positives: falsePositives,
- from,
- immutable,
- query,
- language,
- machine_learning_job_id: machineLearningJobId,
- output_index: outputIndex,
- saved_id: savedId,
- meta,
- filters,
- rule_id: ruleId,
- index,
- interval,
- max_signals: maxSignals,
- risk_score: riskScore,
- name,
- severity,
- tags,
- threat,
- to,
- type,
- references,
- note,
- timeline_id: timelineId,
- timeline_title: timelineTitle,
- version,
- exceptions_list,
- } = parsedRule;
+ const importsWorkerPromise = new Promise(async resolve => {
+ if (parsedRule instanceof Error) {
+ // If the JSON object had a validation or parse error then we return
+ // early with the error and an (unknown) for the ruleId
+ resolve(
+ createBulkErrorObject({
+ statusCode: 400,
+ message: parsedRule.message,
+ })
+ );
+ return null;
+ }
+ const {
+ anomaly_threshold: anomalyThreshold,
+ description,
+ enabled,
+ false_positives: falsePositives,
+ from,
+ immutable,
+ query,
+ language,
+ machine_learning_job_id: machineLearningJobId,
+ output_index: outputIndex,
+ saved_id: savedId,
+ meta,
+ filters,
+ rule_id: ruleId,
+ index,
+ interval,
+ max_signals: maxSignals,
+ risk_score: riskScore,
+ name,
+ severity,
+ tags,
+ threat,
+ to,
+ type,
+ references,
+ note,
+ timeline_id: timelineId,
+ timeline_title: timelineTitle,
+ version,
+ exceptions_list,
+ } = parsedRule;
- try {
- validateLicenseForRuleType({
- license: context.licensing.license,
- ruleType: type,
- });
+ try {
+ validateLicenseForRuleType({
+ license: context.licensing.license,
+ ruleType: type,
+ });
- const signalsIndex = siemClient.getSignalsIndex();
- const indexExists = await getIndexExists(
- clusterClient.callAsCurrentUser,
- signalsIndex
- );
- if (!indexExists) {
- resolve(
- createBulkErrorObject({
- ruleId,
- statusCode: 409,
- message: `To create a rule, the index must exist first. Index ${signalsIndex} does not exist`,
- })
- );
- }
- const rule = await readRules({ alertsClient, ruleId });
- if (rule == null) {
- await createRules({
- alertsClient,
- anomalyThreshold,
- description,
- enabled,
- falsePositives,
- from,
- immutable,
- query,
- language,
- machineLearningJobId,
- outputIndex: signalsIndex,
- savedId,
- timelineId,
- timelineTitle,
- meta,
- filters,
- ruleId,
- index,
- interval,
- maxSignals,
- riskScore,
- name,
- severity,
- tags,
- to,
- type,
- threat,
- references,
- note,
- version,
- exceptions_list,
- actions: [], // Actions are not imported nor exported at this time
- });
- resolve({ rule_id: ruleId, status_code: 200 });
- } else if (rule != null && request.query.overwrite) {
- await patchRules({
- alertsClient,
- savedObjectsClient,
- description,
- enabled,
- falsePositives,
- from,
- immutable,
- query,
- language,
- outputIndex,
- savedId,
- timelineId,
- timelineTitle,
- meta,
- filters,
- id: undefined,
- ruleId,
- index,
- interval,
- maxSignals,
- riskScore,
- name,
- severity,
- tags,
- to,
- type,
- threat,
- references,
- note,
- version,
- exceptions_list,
- anomalyThreshold,
- machineLearningJobId,
- });
- resolve({ rule_id: ruleId, status_code: 200 });
- } else if (rule != null) {
- resolve(
- createBulkErrorObject({
- ruleId,
- statusCode: 409,
- message: `rule_id: "${ruleId}" already exists`,
- })
- );
- }
- } catch (err) {
+ const rule = await readRules({ alertsClient, ruleId });
+ if (rule == null) {
+ await createRules({
+ alertsClient,
+ anomalyThreshold,
+ description,
+ enabled,
+ falsePositives,
+ from,
+ immutable,
+ query,
+ language,
+ machineLearningJobId,
+ outputIndex: signalsIndex,
+ savedId,
+ timelineId,
+ timelineTitle,
+ meta,
+ filters,
+ ruleId,
+ index,
+ interval,
+ maxSignals,
+ riskScore,
+ name,
+ severity,
+ tags,
+ to,
+ type,
+ threat,
+ references,
+ note,
+ version,
+ exceptions_list,
+ actions: [], // Actions are not imported nor exported at this time
+ });
+ resolve({ rule_id: ruleId, status_code: 200 });
+ } else if (rule != null && request.query.overwrite) {
+ await patchRules({
+ alertsClient,
+ savedObjectsClient,
+ description,
+ enabled,
+ falsePositives,
+ from,
+ immutable,
+ query,
+ language,
+ outputIndex,
+ savedId,
+ timelineId,
+ timelineTitle,
+ meta,
+ filters,
+ id: undefined,
+ ruleId,
+ index,
+ interval,
+ maxSignals,
+ riskScore,
+ name,
+ severity,
+ tags,
+ to,
+ type,
+ threat,
+ references,
+ note,
+ version,
+ exceptions_list,
+ anomalyThreshold,
+ machineLearningJobId,
+ });
+ resolve({ rule_id: ruleId, status_code: 200 });
+ } else if (rule != null) {
resolve(
createBulkErrorObject({
ruleId,
- statusCode: 400,
- message: err.message,
+ statusCode: 409,
+ message: `rule_id: "${ruleId}" already exists`,
})
);
}
+ } catch (err) {
+ resolve(
+ createBulkErrorObject({
+ ruleId,
+ statusCode: 400,
+ message: err.message,
+ })
+ );
}
- );
+ });
return [...accum, importsWorkerPromise];
}, [])
);
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts
index e50f82bb482a7..a7556d975da40 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts
@@ -8,7 +8,7 @@ import { SavedObjectsType } from '../../../../../../../src/core/server';
export const ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions';
-export const ruleActionsSavedObjectMappings = {
+export const ruleActionsSavedObjectMappings: SavedObjectsType['mappings'] = {
properties: {
alertThrottle: {
type: 'keyword',
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts
index 2dcc90240ad40..c01bc2497d677 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts
@@ -8,7 +8,7 @@ import { SavedObjectsType } from '../../../../../../../src/core/server';
export const ruleStatusSavedObjectType = 'siem-detection-engine-rule-status';
-export const ruleStatusSavedObjectMappings = {
+export const ruleStatusSavedObjectMappings: SavedObjectsType['mappings'] = {
properties: {
alertId: {
type: 'keyword',
diff --git a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts
index 0f079571b868b..de0bb3468e524 100644
--- a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts
+++ b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts
@@ -8,7 +8,7 @@ import { SavedObjectsType } from '../../../../../../src/core/server';
export const noteSavedObjectType = 'siem-ui-timeline-note';
-export const noteSavedObjectMappings = {
+export const noteSavedObjectMappings: SavedObjectsType['mappings'] = {
properties: {
timelineId: {
type: 'keyword',
diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts
index 1a4cd3fce575d..d352764930d7f 100644
--- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts
+++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts
@@ -8,7 +8,7 @@ import { SavedObjectsType } from '../../../../../../src/core/server';
export const pinnedEventSavedObjectType = 'siem-ui-timeline-pinned-event';
-export const pinnedEventSavedObjectMappings = {
+export const pinnedEventSavedObjectMappings: SavedObjectsType['mappings'] = {
properties: {
timelineId: {
type: 'keyword',
diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts
index 1cab24d0879ff..4d9ae19bfd6a2 100644
--- a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts
+++ b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts
@@ -8,7 +8,7 @@ import { SavedObjectsType } from '../../../../../../src/core/server';
export const timelineSavedObjectType = 'siem-ui-timeline';
-export const timelineSavedObjectMappings = {
+export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = {
properties: {
columns: {
properties: {
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx
index a794b7e7c2143..d3dae0a8c8b63 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx
@@ -4,13 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { FC } from 'react';
+import React, { useState, FC } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButton,
EuiButtonEmpty,
+ EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
@@ -18,11 +19,10 @@ import {
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiOverlayMask,
+ EuiSpacer,
EuiTitle,
} from '@elastic/eui';
-import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public';
-
import { getErrorMessage } from '../../../../../shared_imports';
import {
@@ -30,8 +30,7 @@ import {
TransformPivotConfig,
REFRESH_TRANSFORM_LIST_STATE,
} from '../../../../common';
-import { ToastNotificationText } from '../../../../components';
-import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
+import { useToastNotifications } from '../../../../app_dependencies';
import { useApi } from '../../../../hooks/use_api';
@@ -48,13 +47,14 @@ interface EditTransformFlyoutProps {
}
export const EditTransformFlyout: FC = ({ closeFlyout, config }) => {
- const { overlays } = useAppDependencies();
const api = useApi();
const toastNotifications = useToastNotifications();
const [state, dispatch] = useEditTransformFlyout(config);
+ const [errorMessage, setErrorMessage] = useState(undefined);
async function submitFormHandler() {
+ setErrorMessage(undefined);
const requestConfig = applyFormFieldsToTransformConfig(config, state.formFields);
const transformId = config.id;
@@ -69,12 +69,7 @@ export const EditTransformFlyout: FC = ({ closeFlyout,
closeFlyout();
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
} catch (e) {
- toastNotifications.addDanger({
- title: i18n.translate('xpack.transform.transformList.editTransformGenericErrorMessage', {
- defaultMessage: 'An error occurred calling the API endpoint to update transforms.',
- }),
- text: toMountPoint(),
- });
+ setErrorMessage(getErrorMessage(e));
}
}
@@ -97,6 +92,24 @@ export const EditTransformFlyout: FC = ({ closeFlyout,
}>
+ {errorMessage !== undefined && (
+ <>
+
+
+ {errorMessage}
+
+ >
+ )}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 3fa3ca4836a4a..78cc13a5c1c5f 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -4209,6 +4209,7 @@
"xpack.apm.errorRateAlertTrigger.isAbove": "の下限は",
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "エラーメッセージと原因",
"xpack.apm.errorsTable.groupIdColumnLabel": "グループ ID",
+ "xpack.apm.errorsTable.groupIdColumnDescription": "スタックトレースのハッシュ。動的パラメーターによりエラーメッセージが異なる場合でも、同様のエラーをグループ化します。",
"xpack.apm.errorsTable.latestOccurrenceColumnLabel": "最近のオカレンス",
"xpack.apm.errorsTable.noErrorsLabel": "エラーが見つかりませんでした",
"xpack.apm.errorsTable.occurrencesColumnLabel": "オカレンス",
@@ -4313,7 +4314,6 @@
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription": "現在 {serviceName} ({transactionType}) の実行中のジョブがあります。",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "既存のジョブを表示",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle": "ジョブが既に存在します",
- "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription": "ここでは、{serviceName} 数列内の APM トランザクションの期間の異常スコアを計算する機械学習ジョブを作成できます。有効にすると、{transactionDurationGraphText} が予測バウンドを表示し、異常スコアが >=75 の場合グラフに注釈が追加されます。",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText": "トランザクション時間のグラフ",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel": "ジョブを作成",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle": "異常検知を有効にする",
@@ -10052,7 +10052,6 @@
"xpack.ml.models.jobValidation.messages.jobIdInvalidMessage": "ジョブ ID が無効です。アルファベットの小文字 (a-z と 0-9)、ハイフンまたはアンダーラインが使用でき、最初と最後を英数字にする必要があります。",
"xpack.ml.models.jobValidation.messages.jobIdValidHeading": "ジョブ ID のフォーマットは有効です。",
"xpack.ml.models.jobValidation.messages.jobIdValidMessage": "アルファベットの小文字 (a-z と 0-9)、ハイフンまたはアンダーライン、最初と最後を英数字にし、{maxLength, plural, one {# 文字} other {# 文字}}以内にする必要があります。",
- "xpack.ml.models.jobValidation.messages.mmlGreaterThanMaxMmlMessage": "モデルメモリー制限が、このクラスターに構成された最大モデルメモリー制限を超えています。",
"xpack.ml.models.jobValidation.messages.mmlValueInvalidMessage": "{mml} はモデルメモリー制限の有効な値ではありません。この値は最低 1MB で、バイト (例: 10MB) で指定する必要があります。",
"xpack.ml.models.jobValidation.messages.skippedExtendedTestsMessage": "ジョブの構成の基本要件が満たされていないため、他のチェックをスキップしました。",
"xpack.ml.models.jobValidation.messages.successBucketSpanHeading": "バケットスパン",
@@ -12199,7 +12198,6 @@
"xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle": "{reportObjectType}「{reportObjectTitle}」のレポートが作成されました",
"xpack.reporting.registerFeature.reportingDescription": "ディスカバリ、可視化、ダッシュボードから生成されたレポートを管理します。",
"xpack.reporting.registerFeature.reportingTitle": "レポート",
- "xpack.reporting.screencapture.asyncTook": "{description} にかかった時間は {took}ms でした",
"xpack.reporting.screencapture.couldntFinishRendering": "{count} 件のビジュアライゼーションのレンダリングが完了するのを待つ間にエラーが発生しました。「{configKey}」を増やす必要があるかもしれません。 {error}",
"xpack.reporting.screencapture.couldntLoadKibana": "Kibana URL を開こうとするときにエラーが発生しました。「{configKey}」を増やす必要があるかもしれません。 {error}",
"xpack.reporting.screencapture.injectCss": "Kibana CSS をレポート用に更新しようとしたときにエラーが発生しました。{error}",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 2a3b47449009b..1274253883d6e 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -4209,6 +4209,7 @@
"xpack.apm.errorRateAlertTrigger.errors": "错误",
"xpack.apm.errorRateAlertTrigger.isAbove": "高于",
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "错误消息和原因",
+ "xpack.apm.errorsTable.groupIdColumnDescription": "堆栈跟踪的哈希值。即使由于动态参数而导致错误消息不同,也将相似的错误归为一组。",
"xpack.apm.errorsTable.groupIdColumnLabel": "组 ID",
"xpack.apm.errorsTable.latestOccurrenceColumnLabel": "最新一次发生",
"xpack.apm.errorsTable.noErrorsLabel": "未找到任何错误",
@@ -4314,7 +4315,6 @@
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription": "当前有 {serviceName}({transactionType})的作业正在运行。",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "查看现有作业",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle": "作业已存在",
- "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription": "在这里可以创建 Machine Learning 作业以基于 {serviceName} 服务内 APM 事务的持续时间计算异常分数。启用后,一旦异常分数 >=75,{transactionDurationGraphText}将显示预期边界并标注图表。",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText": "事务持续时间图表",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel": "创建作业",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle": "启用异常检测",
@@ -10058,7 +10058,6 @@
"xpack.ml.models.jobValidation.messages.jobIdInvalidMessage": "作业 ID 无效.其可以包含小写字母数字(a-z 和 0-9)字符、连字符或下划线,且必须以字母数字字符开头和结尾。",
"xpack.ml.models.jobValidation.messages.jobIdValidHeading": "作业 ID 格式有效",
"xpack.ml.models.jobValidation.messages.jobIdValidMessage": "小写字母数字(a-z 和 0-9)字符、连字符或下划线,以字母数字字符开头和结尾,且长度不超过 {maxLength, plural, one {# 个字符} other {# 个字符}}。",
- "xpack.ml.models.jobValidation.messages.mmlGreaterThanMaxMmlMessage": "模型内存限制大于为此集群配置的最大模型内存限制。",
"xpack.ml.models.jobValidation.messages.mmlValueInvalidMessage": "{mml} 不是有效的模型内存限制值。该值需要至少 1MB,且应以字节为单位(例如 10MB)指定。",
"xpack.ml.models.jobValidation.messages.skippedExtendedTestsMessage": "已跳过其他检查,因为未满足作业配置的基本要求。",
"xpack.ml.models.jobValidation.messages.successBucketSpanHeading": "存储桶跨度",
@@ -12206,7 +12205,6 @@
"xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle": "已为 {reportObjectType}“{reportObjectTitle}”创建报告",
"xpack.reporting.registerFeature.reportingDescription": "管理您从 Discover、Visualize 和 Dashboard 生成的报告。",
"xpack.reporting.registerFeature.reportingTitle": "报告",
- "xpack.reporting.screencapture.asyncTook": "{description} 花费了 {took}ms",
"xpack.reporting.screencapture.couldntFinishRendering": "尝试等候 {count} 个可视化完成渲染时发生错误。您可能需要增加“{configKey}”。{error}",
"xpack.reporting.screencapture.couldntLoadKibana": "尝试打开 Kibana URL 时发生了错误。您可能需要增加“{configKey}”。{error}",
"xpack.reporting.screencapture.injectCss": "尝试为 Reporting 更新 Kibana CSS 时发生错误。{error}",
diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md
index ece1791c66e11..c5f02863ba8a1 100644
--- a/x-pack/plugins/triggers_actions_ui/README.md
+++ b/x-pack/plugins/triggers_actions_ui/README.md
@@ -985,8 +985,8 @@ Each action type should be defined as an `ActionTypeModel` object with the follo
|selectMessage|Short description of action type responsibility, that will be displayed on the select card in UI.|
|validateConnector|Validation function for action connector.|
|validateParams|Validation function for action params.|
-|actionConnectorFields|React functional component for building UI of current action type connector.|
-|actionParamsFields|React functional component for building UI of current action type params. Displayed as a part of Create Alert flyout.|
+|actionConnectorFields|A lazy loaded React component for building UI of current action type connector.|
+|actionParamsFields|A lazy loaded React component for building UI of current action type params. Displayed as a part of Create Alert flyout.|
## Register action type model
@@ -1082,8 +1082,8 @@ export function getActionType(): ActionTypeModel {
}
return validationResult;
},
- actionConnectorFields: ExampleConnectorFields,
- actionParamsFields: ExampleParamsFields,
+ actionConnectorFields: lazy(() => import('./example_connector_fields')),
+ actionParamsFields: lazy(() => import('./example_params_fields')),
};
}
```
@@ -1130,6 +1130,9 @@ const ExampleConnectorFields: React.FunctionComponent
);
};
+
+// Export as default in order to support lazy loading
+export {ExampleConnectorFields as default};
```
3. Define action type params fields using the property of `ActionTypeModel` `actionParamsFields`:
@@ -1175,6 +1178,9 @@ const ExampleParamsFields: React.FunctionComponent
);
};
+
+// Export as default in order to support lazy loading
+export {ExampleParamsFields as default};
```
4. Extend registration code with the new action type register in the file `x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts`
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx
index 0593940a0d105..63860e062c8da 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx
@@ -3,8 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
-import { Switch, Route, Redirect, HashRouter } from 'react-router-dom';
+import React, { lazy, Suspense } from 'react';
+import { Switch, Route, Redirect, HashRouter, RouteComponentProps } from 'react-router-dom';
import {
ChromeStart,
DocLinksStart,
@@ -15,17 +15,21 @@ import {
ChromeBreadcrumb,
CoreStart,
} from 'kibana/public';
+import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { BASE_PATH, Section, routeToAlertDetails } from './constants';
-import { TriggersActionsUIHome } from './home';
import { AppContextProvider, useAppDependencies } from './app_context';
import { hasShowAlertsCapability } from './lib/capabilities';
import { ActionTypeModel, AlertTypeModel } from '../types';
import { TypeRegistry } from './type_registry';
-import { AlertDetailsRouteWithApi as AlertDetailsRoute } from './sections/alert_details/components/alert_details_route';
import { ChartsPluginStart } from '../../../../../src/plugins/charts/public';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { PluginStartContract as AlertingStart } from '../../../alerting/public';
+const TriggersActionsUIHome = lazy(async () => import('./home'));
+const AlertDetailsRoute = lazy(() =>
+ import('./sections/alert_details/components/alert_details_route')
+);
+
export interface AppDeps {
dataPlugin: DataPublicPluginStart;
charts: ChartsPluginStart;
@@ -62,9 +66,32 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) =
const DEFAULT_SECTION: Section = canShowAlerts ? 'alerts' : 'connectors';
return (
-
- {canShowAlerts && }
+
+ {canShowAlerts && (
+
+ )}
);
};
+
+function suspendedRouteComponent(
+ RouteComponent: React.ComponentType>
+) {
+ return (props: RouteComponentProps) => (
+
+
+
+
+
+ }
+ >
+
+
+ );
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx
deleted file mode 100644
index dff697297f3e4..0000000000000
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx
+++ /dev/null
@@ -1,609 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React, { Fragment, useState, useEffect } from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiFieldText,
- EuiFlexItem,
- EuiFlexGroup,
- EuiFieldNumber,
- EuiFieldPassword,
- EuiComboBox,
- EuiTextArea,
- EuiButtonEmpty,
- EuiSwitch,
- EuiFormRow,
-} from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import {
- ActionTypeModel,
- ActionConnectorFieldsProps,
- ValidationResult,
- ActionParamsProps,
-} from '../../../types';
-import { EmailActionParams, EmailActionConnector } from './types';
-import { AddMessageVariables } from '../add_message_variables';
-
-export function getActionType(): ActionTypeModel {
- const mailformat = /^[^@\s]+@[^@\s]+$/;
- return {
- id: '.email',
- iconClass: 'email',
- selectMessage: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText',
- {
- defaultMessage: 'Send email from your server.',
- }
- ),
- actionTypeTitle: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle',
- {
- defaultMessage: 'Send to email',
- }
- ),
- validateConnector: (action: EmailActionConnector): ValidationResult => {
- const validationResult = { errors: {} };
- const errors = {
- from: new Array(),
- port: new Array(),
- host: new Array(),
- user: new Array(),
- password: new Array(),
- };
- validationResult.errors = errors;
- if (!action.config.from) {
- errors.from.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText',
- {
- defaultMessage: 'Sender is required.',
- }
- )
- );
- }
- if (action.config.from && !action.config.from.trim().match(mailformat)) {
- errors.from.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText',
- {
- defaultMessage: 'Sender is not a valid email address.',
- }
- )
- );
- }
- if (!action.config.port) {
- errors.port.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText',
- {
- defaultMessage: 'Port is required.',
- }
- )
- );
- }
- if (!action.config.host) {
- errors.host.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText',
- {
- defaultMessage: 'Host is required.',
- }
- )
- );
- }
- if (action.secrets.user && !action.secrets.password) {
- errors.password.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText',
- {
- defaultMessage: 'Password is required when username is used.',
- }
- )
- );
- }
- if (!action.secrets.user && action.secrets.password) {
- errors.user.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText',
- {
- defaultMessage: 'Username is required when password is used.',
- }
- )
- );
- }
- return validationResult;
- },
- validateParams: (actionParams: EmailActionParams): ValidationResult => {
- const validationResult = { errors: {} };
- const errors = {
- to: new Array(),
- cc: new Array(),
- bcc: new Array(),
- message: new Array(),
- subject: new Array(),
- };
- validationResult.errors = errors;
- if (
- (!(actionParams.to instanceof Array) || actionParams.to.length === 0) &&
- (!(actionParams.cc instanceof Array) || actionParams.cc.length === 0) &&
- (!(actionParams.bcc instanceof Array) || actionParams.bcc.length === 0)
- ) {
- const errorText = i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText',
- {
- defaultMessage: 'No To, Cc, or Bcc entry. At least one entry is required.',
- }
- );
- errors.to.push(errorText);
- errors.cc.push(errorText);
- errors.bcc.push(errorText);
- }
- if (!actionParams.message?.length) {
- errors.message.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText',
- {
- defaultMessage: 'Message is required.',
- }
- )
- );
- }
- if (!actionParams.subject?.length) {
- errors.subject.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText',
- {
- defaultMessage: 'Subject is required.',
- }
- )
- );
- }
- return validationResult;
- },
- actionConnectorFields: EmailActionConnectorFields,
- actionParamsFields: EmailParamsFields,
- };
-}
-
-const EmailActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => {
- const { from, host, port, secure } = action.config;
- const { user, password } = action.secrets;
-
- return (
-
-
-
- 0 && from !== undefined}
- label={i18n.translate(
- 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel',
- {
- defaultMessage: 'Sender',
- }
- )}
- >
- 0 && from !== undefined}
- name="from"
- value={from || ''}
- data-test-subj="emailFromInput"
- onChange={e => {
- editActionConfig('from', e.target.value);
- }}
- onBlur={() => {
- if (!from) {
- editActionConfig('from', '');
- }
- }}
- />
-
-
-
-
-
- 0 && host !== undefined}
- label={i18n.translate(
- 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel',
- {
- defaultMessage: 'Host',
- }
- )}
- >
- 0 && host !== undefined}
- name="host"
- value={host || ''}
- data-test-subj="emailHostInput"
- onChange={e => {
- editActionConfig('host', e.target.value);
- }}
- onBlur={() => {
- if (!host) {
- editActionConfig('host', '');
- }
- }}
- />
-
-
-
-
-
- 0 && port !== undefined}
- label={i18n.translate(
- 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel',
- {
- defaultMessage: 'Port',
- }
- )}
- >
- 0 && port !== undefined}
- fullWidth
- name="port"
- value={port || ''}
- data-test-subj="emailPortInput"
- onChange={e => {
- editActionConfig('port', parseInt(e.target.value, 10));
- }}
- onBlur={() => {
- if (!port) {
- editActionConfig('port', 0);
- }
- }}
- />
-
-
-
-
-
- {
- editActionConfig('secure', e.target.checked);
- }}
- />
-
-
-
-
-
-
-
-
- 0}
- label={i18n.translate(
- 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel',
- {
- defaultMessage: 'Username',
- }
- )}
- >
- 0}
- name="user"
- value={user || ''}
- data-test-subj="emailUserInput"
- onChange={e => {
- editActionSecrets('user', nullableString(e.target.value));
- }}
- />
-
-
-
- 0}
- label={i18n.translate(
- 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel',
- {
- defaultMessage: 'Password',
- }
- )}
- >
- 0}
- name="password"
- value={password || ''}
- data-test-subj="emailPasswordInput"
- onChange={e => {
- editActionSecrets('password', nullableString(e.target.value));
- }}
- />
-
-
-
-
- );
-};
-
-const EmailParamsFields: React.FunctionComponent> = ({
- actionParams,
- editAction,
- index,
- errors,
- messageVariables,
- defaultMessage,
-}) => {
- const { to, cc, bcc, subject, message } = actionParams;
- const toOptions = to ? to.map((label: string) => ({ label })) : [];
- const ccOptions = cc ? cc.map((label: string) => ({ label })) : [];
- const bccOptions = bcc ? bcc.map((label: string) => ({ label })) : [];
- const [addCC, setAddCC] = useState(false);
- const [addBCC, setAddBCC] = useState(false);
-
- useEffect(() => {
- if (!message && defaultMessage && defaultMessage.length > 0) {
- editAction('message', defaultMessage, index);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const onSelectMessageVariable = (paramsProperty: string, variable: string) => {
- editAction(
- paramsProperty,
- ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`),
- index
- );
- };
-
- return (
-
- 0 && to !== undefined}
- label={i18n.translate(
- 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientTextFieldLabel',
- {
- defaultMessage: 'To',
- }
- )}
- labelAppend={
-
-
- {!addCC ? (
- setAddCC(true)}>
-
-
- ) : null}
- {!addBCC ? (
- setAddBCC(true)}>
-
-
- ) : null}
-
-
- }
- >
- 0 && to !== undefined}
- fullWidth
- data-test-subj="toEmailAddressInput"
- selectedOptions={toOptions}
- onCreateOption={(searchValue: string) => {
- const newOptions = [...toOptions, { label: searchValue }];
- editAction(
- 'to',
- newOptions.map(newOption => newOption.label),
- index
- );
- }}
- onChange={(selectedOptions: Array<{ label: string }>) => {
- editAction(
- 'to',
- selectedOptions.map(selectedOption => selectedOption.label),
- index
- );
- }}
- onBlur={() => {
- if (!to) {
- editAction('to', [], index);
- }
- }}
- />
-
- {addCC ? (
- 0 && cc !== undefined}
- label={i18n.translate(
- 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientCopyTextFieldLabel',
- {
- defaultMessage: 'Cc',
- }
- )}
- >
- 0 && cc !== undefined}
- fullWidth
- data-test-subj="ccEmailAddressInput"
- selectedOptions={ccOptions}
- onCreateOption={(searchValue: string) => {
- const newOptions = [...ccOptions, { label: searchValue }];
- editAction(
- 'cc',
- newOptions.map(newOption => newOption.label),
- index
- );
- }}
- onChange={(selectedOptions: Array<{ label: string }>) => {
- editAction(
- 'cc',
- selectedOptions.map(selectedOption => selectedOption.label),
- index
- );
- }}
- onBlur={() => {
- if (!cc) {
- editAction('cc', [], index);
- }
- }}
- />
-
- ) : null}
- {addBCC ? (
- 0 && bcc !== undefined}
- label={i18n.translate(
- 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientBccTextFieldLabel',
- {
- defaultMessage: 'Bcc',
- }
- )}
- >
- 0 && bcc !== undefined}
- fullWidth
- data-test-subj="bccEmailAddressInput"
- selectedOptions={bccOptions}
- onCreateOption={(searchValue: string) => {
- const newOptions = [...bccOptions, { label: searchValue }];
- editAction(
- 'bcc',
- newOptions.map(newOption => newOption.label),
- index
- );
- }}
- onChange={(selectedOptions: Array<{ label: string }>) => {
- editAction(
- 'bcc',
- selectedOptions.map(selectedOption => selectedOption.label),
- index
- );
- }}
- onBlur={() => {
- if (!bcc) {
- editAction('bcc', [], index);
- }
- }}
- />
-
- ) : null}
- 0 && subject !== undefined}
- label={i18n.translate(
- 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel',
- {
- defaultMessage: 'Subject',
- }
- )}
- labelAppend={
-
- onSelectMessageVariable('subject', variable)
- }
- paramsProperty="subject"
- />
- }
- >
- 0 && subject !== undefined}
- name="subject"
- data-test-subj="emailSubjectInput"
- value={subject || ''}
- onChange={e => {
- editAction('subject', e.target.value, index);
- }}
- onBlur={() => {
- if (!subject) {
- editAction('subject', '', index);
- }
- }}
- />
-
- 0 && message !== undefined}
- label={i18n.translate(
- 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.messageTextAreaFieldLabel',
- {
- defaultMessage: 'Message',
- }
- )}
- labelAppend={
-
- onSelectMessageVariable('message', variable)
- }
- paramsProperty="message"
- />
- }
- >
- 0 && message !== undefined}
- value={message || ''}
- name="message"
- data-test-subj="emailMessageInput"
- onChange={e => {
- editAction('message', e.target.value, index);
- }}
- onBlur={() => {
- if (!message) {
- editAction('message', '', index);
- }
- }}
- />
-
-
- );
-};
-
-// if the string == null or is empty, return null, else return string
-function nullableString(str: string | null | undefined) {
- if (str == null || str.trim() === '') return null;
- return str;
-}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx
similarity index 62%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx
index af9e34071fd09..e823e848f52c2 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx
@@ -3,12 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { FunctionComponent } from 'react';
-import { mountWithIntl } from 'test_utils/enzyme_helpers';
-import { TypeRegistry } from '../../type_registry';
-import { registerBuiltInActionTypes } from './index';
-import { ActionTypeModel, ActionParamsProps } from '../../../types';
-import { EmailActionParams, EmailActionConnector } from './types';
+import { TypeRegistry } from '../../../type_registry';
+import { registerBuiltInActionTypes } from '../index';
+import { ActionTypeModel } from '../../../../types';
+import { EmailActionConnector } from '../types';
const ACTION_TYPE_ID = '.email';
let actionTypeModel: ActionTypeModel;
@@ -206,80 +204,3 @@ describe('action params validation', () => {
});
});
});
-
-describe('EmailActionConnectorFields renders', () => {
- test('all connector fields is rendered', () => {
- expect(actionTypeModel.actionConnectorFields).not.toBeNull();
- if (!actionTypeModel.actionConnectorFields) {
- return;
- }
- const ConnectorFields = actionTypeModel.actionConnectorFields;
- const actionConnector = {
- secrets: {
- user: 'user',
- password: 'pass',
- },
- id: 'test',
- actionTypeId: '.email',
- name: 'email',
- config: {
- from: 'test@test.com',
- },
- } as EmailActionConnector;
- const wrapper = mountWithIntl(
- {}}
- editActionSecrets={() => {}}
- />
- );
- expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy();
- expect(
- wrapper
- .find('[data-test-subj="emailFromInput"]')
- .first()
- .prop('value')
- ).toBe('test@test.com');
- expect(wrapper.find('[data-test-subj="emailHostInput"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="emailPortInput"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeTruthy();
- });
-});
-
-describe('EmailParamsFields renders', () => {
- test('all params fields is rendered', () => {
- expect(actionTypeModel.actionParamsFields).not.toBeNull();
- if (!actionTypeModel.actionParamsFields) {
- return;
- }
- const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent<
- ActionParamsProps
- >;
- const actionParams = {
- cc: [],
- bcc: [],
- to: ['test@test.com'],
- subject: 'test',
- message: 'test message',
- };
- const wrapper = mountWithIntl(
- {}}
- 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="emailSubjectInput"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="emailMessageInput"]').length > 0).toBeTruthy();
- });
-});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx
new file mode 100644
index 0000000000000..abb102c04b054
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx
@@ -0,0 +1,150 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { lazy } from 'react';
+import { i18n } from '@kbn/i18n';
+import { ActionTypeModel, ValidationResult } from '../../../../types';
+import { EmailActionParams, EmailActionConnector } from '../types';
+
+export function getActionType(): ActionTypeModel {
+ const mailformat = /^[^@\s]+@[^@\s]+$/;
+ return {
+ id: '.email',
+ iconClass: 'email',
+ selectMessage: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText',
+ {
+ defaultMessage: 'Send email from your server.',
+ }
+ ),
+ actionTypeTitle: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle',
+ {
+ defaultMessage: 'Send to email',
+ }
+ ),
+ validateConnector: (action: EmailActionConnector): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ from: new Array(),
+ port: new Array(),
+ host: new Array(),
+ user: new Array(),
+ password: new Array(),
+ };
+ validationResult.errors = errors;
+ if (!action.config.from) {
+ errors.from.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText',
+ {
+ defaultMessage: 'Sender is required.',
+ }
+ )
+ );
+ }
+ if (action.config.from && !action.config.from.trim().match(mailformat)) {
+ errors.from.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText',
+ {
+ defaultMessage: 'Sender is not a valid email address.',
+ }
+ )
+ );
+ }
+ if (!action.config.port) {
+ errors.port.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText',
+ {
+ defaultMessage: 'Port is required.',
+ }
+ )
+ );
+ }
+ if (!action.config.host) {
+ errors.host.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText',
+ {
+ defaultMessage: 'Host is required.',
+ }
+ )
+ );
+ }
+ if (action.secrets.user && !action.secrets.password) {
+ errors.password.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText',
+ {
+ defaultMessage: 'Password is required when username is used.',
+ }
+ )
+ );
+ }
+ if (!action.secrets.user && action.secrets.password) {
+ errors.user.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText',
+ {
+ defaultMessage: 'Username is required when password is used.',
+ }
+ )
+ );
+ }
+ return validationResult;
+ },
+ validateParams: (actionParams: EmailActionParams): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ to: new Array(),
+ cc: new Array(),
+ bcc: new Array(),
+ message: new Array(),
+ subject: new Array(),
+ };
+ validationResult.errors = errors;
+ if (
+ (!(actionParams.to instanceof Array) || actionParams.to.length === 0) &&
+ (!(actionParams.cc instanceof Array) || actionParams.cc.length === 0) &&
+ (!(actionParams.bcc instanceof Array) || actionParams.bcc.length === 0)
+ ) {
+ const errorText = i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText',
+ {
+ defaultMessage: 'No To, Cc, or Bcc entry. At least one entry is required.',
+ }
+ );
+ errors.to.push(errorText);
+ errors.cc.push(errorText);
+ errors.bcc.push(errorText);
+ }
+ if (!actionParams.message?.length) {
+ errors.message.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText',
+ {
+ defaultMessage: 'Message is required.',
+ }
+ )
+ );
+ }
+ if (!actionParams.subject?.length) {
+ errors.subject.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText',
+ {
+ defaultMessage: 'Subject is required.',
+ }
+ )
+ );
+ }
+ return validationResult;
+ },
+ actionConnectorFields: lazy(() => import('./email_connector')),
+ actionParamsFields: lazy(() => import('./email_params')),
+ };
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx
new file mode 100644
index 0000000000000..67514e815bc49
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { EmailActionConnector } from '../types';
+import EmailActionConnectorFields from './email_connector';
+import { DocLinksStart } from 'kibana/public';
+
+describe('EmailActionConnectorFields renders', () => {
+ test('all connector fields is rendered', () => {
+ const actionConnector = {
+ secrets: {
+ user: 'user',
+ password: 'pass',
+ },
+ id: 'test',
+ actionTypeId: '.email',
+ name: 'email',
+ config: {
+ from: 'test@test.com',
+ },
+ } as EmailActionConnector;
+ const wrapper = mountWithIntl(
+ {}}
+ editActionSecrets={() => {}}
+ docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
+ />
+ );
+ expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="emailFromInput"]')
+ .first()
+ .prop('value')
+ ).toBe('test@test.com');
+ expect(wrapper.find('[data-test-subj="emailHostInput"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="emailPortInput"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx
new file mode 100644
index 0000000000000..4ef4c8a4d8617
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx
@@ -0,0 +1,209 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { Fragment } from 'react';
+import {
+ EuiFieldText,
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiFieldNumber,
+ EuiFieldPassword,
+ EuiSwitch,
+ EuiFormRow,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { ActionConnectorFieldsProps } from '../../../../types';
+import { EmailActionConnector } from '../types';
+
+export const EmailActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => {
+ const { from, host, port, secure } = action.config;
+ const { user, password } = action.secrets;
+
+ return (
+
+
+
+ 0 && from !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel',
+ {
+ defaultMessage: 'Sender',
+ }
+ )}
+ >
+ 0 && from !== undefined}
+ name="from"
+ value={from || ''}
+ data-test-subj="emailFromInput"
+ onChange={e => {
+ editActionConfig('from', e.target.value);
+ }}
+ onBlur={() => {
+ if (!from) {
+ editActionConfig('from', '');
+ }
+ }}
+ />
+
+
+
+
+
+ 0 && host !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel',
+ {
+ defaultMessage: 'Host',
+ }
+ )}
+ >
+ 0 && host !== undefined}
+ name="host"
+ value={host || ''}
+ data-test-subj="emailHostInput"
+ onChange={e => {
+ editActionConfig('host', e.target.value);
+ }}
+ onBlur={() => {
+ if (!host) {
+ editActionConfig('host', '');
+ }
+ }}
+ />
+
+
+
+
+
+ 0 && port !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel',
+ {
+ defaultMessage: 'Port',
+ }
+ )}
+ >
+ 0 && port !== undefined}
+ fullWidth
+ name="port"
+ value={port || ''}
+ data-test-subj="emailPortInput"
+ onChange={e => {
+ editActionConfig('port', parseInt(e.target.value, 10));
+ }}
+ onBlur={() => {
+ if (!port) {
+ editActionConfig('port', 0);
+ }
+ }}
+ />
+
+
+
+
+
+ {
+ editActionConfig('secure', e.target.checked);
+ }}
+ />
+
+
+
+
+
+
+
+
+ 0}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel',
+ {
+ defaultMessage: 'Username',
+ }
+ )}
+ >
+ 0}
+ name="user"
+ value={user || ''}
+ data-test-subj="emailUserInput"
+ onChange={e => {
+ editActionSecrets('user', nullableString(e.target.value));
+ }}
+ />
+
+
+
+ 0}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel',
+ {
+ defaultMessage: 'Password',
+ }
+ )}
+ >
+ 0}
+ name="password"
+ value={password || ''}
+ data-test-subj="emailPasswordInput"
+ onChange={e => {
+ editActionSecrets('password', nullableString(e.target.value));
+ }}
+ />
+
+
+
+
+ );
+};
+
+// if the string == null or is empty, return null, else return string
+function nullableString(str: string | null | undefined) {
+ if (str == null || str.trim() === '') return null;
+ return str;
+}
+
+// eslint-disable-next-line import/no-default-export
+export { EmailActionConnectorFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx
new file mode 100644
index 0000000000000..a2b5ccf988afb
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import EmailParamsFields from './email_params';
+
+describe('EmailParamsFields renders', () => {
+ test('all params fields is rendered', () => {
+ const actionParams = {
+ cc: [],
+ bcc: [],
+ to: ['test@test.com'],
+ subject: 'test',
+ message: 'test message',
+ };
+ const wrapper = mountWithIntl(
+ {}}
+ 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="emailSubjectInput"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="emailMessageInput"]').length > 0).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx
new file mode 100644
index 0000000000000..13e791f1069e3
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx
@@ -0,0 +1,267 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { Fragment, useState, useEffect } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiFieldText, EuiComboBox, EuiTextArea, EuiButtonEmpty, EuiFormRow } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { ActionParamsProps } from '../../../../types';
+import { EmailActionParams } from '../types';
+import { AddMessageVariables } from '../../add_message_variables';
+
+export const EmailParamsFields = ({
+ actionParams,
+ editAction,
+ index,
+ errors,
+ messageVariables,
+ defaultMessage,
+}: ActionParamsProps) => {
+ const { to, cc, bcc, subject, message } = actionParams;
+ const toOptions = to ? to.map((label: string) => ({ label })) : [];
+ const ccOptions = cc ? cc.map((label: string) => ({ label })) : [];
+ const bccOptions = bcc ? bcc.map((label: string) => ({ label })) : [];
+ const [addCC, setAddCC] = useState(false);
+ const [addBCC, setAddBCC] = useState(false);
+
+ useEffect(() => {
+ if (!message && defaultMessage && defaultMessage.length > 0) {
+ editAction('message', defaultMessage, index);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const onSelectMessageVariable = (paramsProperty: string, variable: string) => {
+ editAction(
+ paramsProperty,
+ ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`),
+ index
+ );
+ };
+
+ return (
+
+ 0 && to !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientTextFieldLabel',
+ {
+ defaultMessage: 'To',
+ }
+ )}
+ labelAppend={
+
+
+ {!addCC ? (
+ setAddCC(true)}>
+
+
+ ) : null}
+ {!addBCC ? (
+ setAddBCC(true)}>
+
+
+ ) : null}
+
+
+ }
+ >
+ 0 && to !== undefined}
+ fullWidth
+ data-test-subj="toEmailAddressInput"
+ selectedOptions={toOptions}
+ onCreateOption={(searchValue: string) => {
+ const newOptions = [...toOptions, { label: searchValue }];
+ editAction(
+ 'to',
+ newOptions.map(newOption => newOption.label),
+ index
+ );
+ }}
+ onChange={(selectedOptions: Array<{ label: string }>) => {
+ editAction(
+ 'to',
+ selectedOptions.map(selectedOption => selectedOption.label),
+ index
+ );
+ }}
+ onBlur={() => {
+ if (!to) {
+ editAction('to', [], index);
+ }
+ }}
+ />
+
+ {addCC ? (
+ 0 && cc !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientCopyTextFieldLabel',
+ {
+ defaultMessage: 'Cc',
+ }
+ )}
+ >
+ 0 && cc !== undefined}
+ fullWidth
+ data-test-subj="ccEmailAddressInput"
+ selectedOptions={ccOptions}
+ onCreateOption={(searchValue: string) => {
+ const newOptions = [...ccOptions, { label: searchValue }];
+ editAction(
+ 'cc',
+ newOptions.map(newOption => newOption.label),
+ index
+ );
+ }}
+ onChange={(selectedOptions: Array<{ label: string }>) => {
+ editAction(
+ 'cc',
+ selectedOptions.map(selectedOption => selectedOption.label),
+ index
+ );
+ }}
+ onBlur={() => {
+ if (!cc) {
+ editAction('cc', [], index);
+ }
+ }}
+ />
+
+ ) : null}
+ {addBCC ? (
+ 0 && bcc !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientBccTextFieldLabel',
+ {
+ defaultMessage: 'Bcc',
+ }
+ )}
+ >
+ 0 && bcc !== undefined}
+ fullWidth
+ data-test-subj="bccEmailAddressInput"
+ selectedOptions={bccOptions}
+ onCreateOption={(searchValue: string) => {
+ const newOptions = [...bccOptions, { label: searchValue }];
+ editAction(
+ 'bcc',
+ newOptions.map(newOption => newOption.label),
+ index
+ );
+ }}
+ onChange={(selectedOptions: Array<{ label: string }>) => {
+ editAction(
+ 'bcc',
+ selectedOptions.map(selectedOption => selectedOption.label),
+ index
+ );
+ }}
+ onBlur={() => {
+ if (!bcc) {
+ editAction('bcc', [], index);
+ }
+ }}
+ />
+
+ ) : null}
+ 0 && subject !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel',
+ {
+ defaultMessage: 'Subject',
+ }
+ )}
+ labelAppend={
+
+ onSelectMessageVariable('subject', variable)
+ }
+ paramsProperty="subject"
+ />
+ }
+ >
+ 0 && subject !== undefined}
+ name="subject"
+ data-test-subj="emailSubjectInput"
+ value={subject || ''}
+ onChange={e => {
+ editAction('subject', e.target.value, index);
+ }}
+ onBlur={() => {
+ if (!subject) {
+ editAction('subject', '', index);
+ }
+ }}
+ />
+
+ 0 && message !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.messageTextAreaFieldLabel',
+ {
+ defaultMessage: 'Message',
+ }
+ )}
+ labelAppend={
+
+ onSelectMessageVariable('message', variable)
+ }
+ paramsProperty="message"
+ />
+ }
+ >
+ 0 && message !== undefined}
+ value={message || ''}
+ name="message"
+ data-test-subj="emailMessageInput"
+ onChange={e => {
+ editAction('message', e.target.value, index);
+ }}
+ onBlur={() => {
+ if (!message) {
+ editAction('message', '', index);
+ }
+ }}
+ />
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export { EmailParamsFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/index.ts
new file mode 100644
index 0000000000000..e0dd24a44aa8f
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { getActionType as getEmailActionType } from './email';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx
deleted file mode 100644
index 04dc7b484ed48..0000000000000
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React, { FunctionComponent } from 'react';
-import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
-import { act } from 'react-dom/test-utils';
-import { TypeRegistry } from '../../type_registry';
-import { registerBuiltInActionTypes } from './index';
-import { ActionTypeModel, ActionParamsProps } from '../../../types';
-import { IndexActionParams, EsIndexActionConnector } from './types';
-import { coreMock } from '../../../../../../../src/core/public/mocks';
-jest.mock('../../../common/index_controls', () => ({
- firstFieldOption: jest.fn(),
- getFields: jest.fn(),
- getIndexOptions: jest.fn(),
- getIndexPatterns: jest.fn(),
-}));
-
-const ACTION_TYPE_ID = '.index';
-let actionTypeModel: ActionTypeModel;
-let deps: any;
-
-beforeAll(async () => {
- const actionTypeRegistry = new TypeRegistry();
- registerBuiltInActionTypes({ actionTypeRegistry });
- const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
- if (getResult !== null) {
- actionTypeModel = getResult;
- }
- const mocks = coreMock.createSetup();
- const [
- {
- application: { capabilities },
- },
- ] = await mocks.getStartServices();
- deps = {
- toastNotifications: mocks.notifications.toasts,
- http: mocks.http,
- capabilities: {
- ...capabilities,
- actions: {
- delete: true,
- save: true,
- show: true,
- },
- },
- actionTypeRegistry: actionTypeRegistry as any,
- docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
- };
-});
-
-describe('actionTypeRegistry.get() works', () => {
- test('action type .index is registered', () => {
- expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
- expect(actionTypeModel.iconClass).toEqual('indexOpen');
- });
-});
-
-describe('index connector validation', () => {
- test('connector validation succeeds when connector config is valid', () => {
- const actionConnector = {
- secrets: {},
- id: 'test',
- actionTypeId: '.index',
- name: 'es_index',
- config: {
- index: 'test_es_index',
- refresh: false,
- executionTimeField: '1',
- },
- } as EsIndexActionConnector;
-
- expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
- errors: {
- index: [],
- },
- });
- });
-});
-
-describe('index connector validation with minimal config', () => {
- test('connector validation succeeds when connector config is valid', () => {
- const actionConnector = {
- secrets: {},
- id: 'test',
- actionTypeId: '.index',
- name: 'es_index',
- config: {
- index: 'test_es_index',
- },
- } as EsIndexActionConnector;
-
- expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
- errors: {
- index: [],
- },
- });
- });
-});
-
-describe('action params validation', () => {
- test('action params validation succeeds when action params is valid', () => {
- const actionParams = {
- documents: ['test'],
- };
-
- expect(actionTypeModel.validateParams(actionParams)).toEqual({
- errors: {},
- });
-
- const emptyActionParams = {};
-
- expect(actionTypeModel.validateParams(emptyActionParams)).toEqual({
- errors: {},
- });
- });
-});
-
-describe('IndexActionConnectorFields renders', () => {
- test('all connector fields is rendered', async () => {
- expect(actionTypeModel.actionConnectorFields).not.toBeNull();
- if (!actionTypeModel.actionConnectorFields) {
- return;
- }
-
- const { getIndexPatterns } = jest.requireMock('../../../common/index_controls');
- getIndexPatterns.mockResolvedValueOnce([
- {
- id: 'indexPattern1',
- attributes: {
- title: 'indexPattern1',
- },
- },
- {
- id: 'indexPattern2',
- attributes: {
- title: 'indexPattern2',
- },
- },
- ]);
- const { getFields } = jest.requireMock('../../../common/index_controls');
- getFields.mockResolvedValueOnce([
- {
- type: 'date',
- name: 'test1',
- },
- {
- type: 'text',
- name: 'test2',
- },
- ]);
- const ConnectorFields = actionTypeModel.actionConnectorFields;
- const actionConnector = {
- secrets: {},
- id: 'test',
- actionTypeId: '.index',
- name: 'es_index',
- config: {
- index: 'test',
- refresh: false,
- executionTimeField: 'test1',
- },
- } as EsIndexActionConnector;
- const wrapper = mountWithIntl(
- {}}
- editActionSecrets={() => {}}
- http={deps!.http}
- />
- );
-
- await act(async () => {
- await nextTick();
- wrapper.update();
- });
-
- expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy();
-
- const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]');
- expect(indexSearchBoxValue.first().props().value).toEqual('');
-
- const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox');
- indexComboBox.first().simulate('click');
- const event = { target: { value: 'indexPattern1' } };
- indexComboBox
- .find('input')
- .first()
- .simulate('change', event);
-
- const indexSearchBoxValueBeforeEnterData = wrapper.find(
- '[data-test-subj="comboBoxSearchInput"]'
- );
- expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1');
-
- const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]');
- indexComboBoxClear.first().simulate('click');
-
- const indexSearchBoxValueAfterEnterData = wrapper.find(
- '[data-test-subj="comboBoxSearchInput"]'
- );
- expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1');
- });
-});
-
-describe('IndexParamsFields renders', () => {
- test('all params fields is rendered', () => {
- expect(actionTypeModel.actionParamsFields).not.toBeNull();
- if (!actionTypeModel.actionParamsFields) {
- return;
- }
- const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent<
- ActionParamsProps
- >;
- const actionParams = {
- documents: [{ test: 123 }],
- };
- const wrapper = mountWithIntl(
- {}}
- index={0}
- />
- );
- expect(
- wrapper
- .find('[data-test-subj="actionIndexDoc"]')
- .first()
- .prop('value')
- ).toBe(`{
- "test": 123
-}`);
- expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy();
- });
-});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx
new file mode 100644
index 0000000000000..417a9e09086a2
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx
@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { TypeRegistry } from '../../../type_registry';
+import { registerBuiltInActionTypes } from '../index';
+import { ActionTypeModel } from '../../../../types';
+import { EsIndexActionConnector } from '../types';
+
+const ACTION_TYPE_ID = '.index';
+let actionTypeModel: ActionTypeModel;
+
+beforeAll(() => {
+ const actionTypeRegistry = new TypeRegistry();
+ registerBuiltInActionTypes({ actionTypeRegistry });
+ const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
+ if (getResult !== null) {
+ actionTypeModel = getResult;
+ }
+});
+
+describe('actionTypeRegistry.get() works', () => {
+ test('action type .index is registered', () => {
+ expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
+ expect(actionTypeModel.iconClass).toEqual('indexOpen');
+ });
+});
+
+describe('index connector validation', () => {
+ test('connector validation succeeds when connector config is valid', () => {
+ const actionConnector = {
+ secrets: {},
+ id: 'test',
+ actionTypeId: '.index',
+ name: 'es_index',
+ config: {
+ index: 'test_es_index',
+ refresh: false,
+ executionTimeField: '1',
+ },
+ } as EsIndexActionConnector;
+
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {
+ index: [],
+ },
+ });
+ });
+});
+
+describe('index connector validation with minimal config', () => {
+ test('connector validation succeeds when connector config is valid', () => {
+ const actionConnector = {
+ secrets: {},
+ id: 'test',
+ actionTypeId: '.index',
+ name: 'es_index',
+ config: {
+ index: 'test_es_index',
+ },
+ } as EsIndexActionConnector;
+
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {
+ index: [],
+ },
+ });
+ });
+});
+
+describe('action params validation', () => {
+ test('action params validation succeeds when action params is valid', () => {
+ const actionParams = {
+ documents: ['test'],
+ };
+
+ expect(actionTypeModel.validateParams(actionParams)).toEqual({
+ errors: {},
+ });
+
+ const emptyActionParams = {};
+
+ expect(actionTypeModel.validateParams(emptyActionParams)).toEqual({
+ errors: {},
+ });
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx
new file mode 100644
index 0000000000000..3ee663a5fc8a0
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { lazy } from 'react';
+import { i18n } from '@kbn/i18n';
+import { ActionTypeModel, ValidationResult } from '../../../../types';
+import { EsIndexActionConnector, IndexActionParams } from '../types';
+
+export function getActionType(): ActionTypeModel {
+ return {
+ id: '.index',
+ iconClass: 'indexOpen',
+ selectMessage: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText',
+ {
+ defaultMessage: 'Index data into Elasticsearch.',
+ }
+ ),
+ actionTypeTitle: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle',
+ {
+ defaultMessage: 'Index data',
+ }
+ ),
+ validateConnector: (action: EsIndexActionConnector): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ index: new Array(),
+ };
+ validationResult.errors = errors;
+ if (!action.config.index) {
+ errors.index.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText',
+ {
+ defaultMessage: 'Index is required.',
+ }
+ )
+ );
+ }
+ return validationResult;
+ },
+ actionConnectorFields: lazy(() => import('./es_index_connector')),
+ actionParamsFields: lazy(() => import('./es_index_params')),
+ validateParams: (): ValidationResult => {
+ return { errors: {} };
+ },
+ };
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx
new file mode 100644
index 0000000000000..b0f21afeaa96c
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx
@@ -0,0 +1,126 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
+import { act } from 'react-dom/test-utils';
+import { EsIndexActionConnector } from '../types';
+import { coreMock } from '../../../../../../../../src/core/public/mocks';
+import IndexActionConnectorFields from './es_index_connector';
+import { TypeRegistry } from '../../../type_registry';
+import { DocLinksStart } from 'kibana/public';
+
+jest.mock('../../../../common/index_controls', () => ({
+ firstFieldOption: jest.fn(),
+ getFields: jest.fn(),
+ getIndexOptions: jest.fn(),
+ getIndexPatterns: jest.fn(),
+}));
+
+describe('IndexActionConnectorFields renders', () => {
+ test('all connector fields is rendered', async () => {
+ const mocks = coreMock.createSetup();
+ const [
+ {
+ application: { capabilities },
+ },
+ ] = await mocks.getStartServices();
+ const deps = {
+ toastNotifications: mocks.notifications.toasts,
+ http: mocks.http,
+ capabilities: {
+ ...capabilities,
+ actions: {
+ delete: true,
+ save: true,
+ show: true,
+ },
+ },
+ actionTypeRegistry: {} as TypeRegistry,
+ docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
+ };
+
+ const { getIndexPatterns } = jest.requireMock('../../../../common/index_controls');
+ getIndexPatterns.mockResolvedValueOnce([
+ {
+ id: 'indexPattern1',
+ attributes: {
+ title: 'indexPattern1',
+ },
+ },
+ {
+ id: 'indexPattern2',
+ attributes: {
+ title: 'indexPattern2',
+ },
+ },
+ ]);
+ const { getFields } = jest.requireMock('../../../../common/index_controls');
+ getFields.mockResolvedValueOnce([
+ {
+ type: 'date',
+ name: 'test1',
+ },
+ {
+ type: 'text',
+ name: 'test2',
+ },
+ ]);
+
+ const actionConnector = {
+ secrets: {},
+ id: 'test',
+ actionTypeId: '.index',
+ name: 'es_index',
+ config: {
+ index: 'test',
+ refresh: false,
+ executionTimeField: 'test1',
+ },
+ } as EsIndexActionConnector;
+ const wrapper = mountWithIntl(
+ {}}
+ editActionSecrets={() => {}}
+ http={deps!.http}
+ docLinks={deps!.docLinks}
+ />
+ );
+
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+
+ expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy();
+
+ const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]');
+ expect(indexSearchBoxValue.first().props().value).toEqual('');
+
+ const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox');
+ indexComboBox.first().simulate('click');
+ const event = { target: { value: 'indexPattern1' } };
+ indexComboBox
+ .find('input')
+ .first()
+ .simulate('change', event);
+
+ const indexSearchBoxValueBeforeEnterData = wrapper.find(
+ '[data-test-subj="comboBoxSearchInput"]'
+ );
+ expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1');
+
+ const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]');
+ indexComboBoxClear.first().simulate('click');
+
+ const indexSearchBoxValueAfterEnterData = wrapper.find(
+ '[data-test-subj="comboBoxSearchInput"]'
+ );
+ expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1');
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx
similarity index 66%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx
index 861d6ad7284c2..9cd3a18545345 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx
@@ -3,12 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, useState, useEffect } from 'react';
+import React, { useState, useEffect } from 'react';
import {
EuiFormRow,
EuiSwitch,
EuiSpacer,
- EuiCodeEditor,
EuiComboBox,
EuiComboBoxOptionOption,
EuiSelect,
@@ -17,64 +16,15 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { useXJsonMode } from '../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks';
-import {
- ActionTypeModel,
- ActionConnectorFieldsProps,
- ValidationResult,
- ActionParamsProps,
-} from '../../../types';
-import { IndexActionParams, EsIndexActionConnector } from './types';
-import { getTimeFieldOptions } from '../../../common/lib/get_time_options';
+import { ActionConnectorFieldsProps } from '../../../../types';
+import { EsIndexActionConnector } from '.././types';
+import { getTimeFieldOptions } from '../../../../common/lib/get_time_options';
import {
firstFieldOption,
getFields,
getIndexOptions,
getIndexPatterns,
-} from '../../../common/index_controls';
-import { AddMessageVariables } from '../add_message_variables';
-
-export function getActionType(): ActionTypeModel {
- return {
- id: '.index',
- iconClass: 'indexOpen',
- selectMessage: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText',
- {
- defaultMessage: 'Index data into Elasticsearch.',
- }
- ),
- actionTypeTitle: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle',
- {
- defaultMessage: 'Index data',
- }
- ),
- validateConnector: (action: EsIndexActionConnector): ValidationResult => {
- const validationResult = { errors: {} };
- const errors = {
- index: new Array(),
- };
- validationResult.errors = errors;
- if (!action.config.index) {
- errors.index.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText',
- {
- defaultMessage: 'Index is required.',
- }
- )
- );
- }
- return validationResult;
- },
- actionConnectorFields: IndexActionConnectorFields,
- actionParamsFields: IndexParamsFields,
- validateParams: (): ValidationResult => {
- return { errors: {} };
- },
- };
-}
+} from '../../../../common/index_controls';
const IndexActionConnectorFields: React.FunctionComponent> = ({
- actionParams,
- index,
- editAction,
- messageVariables,
-}) => {
- const { documents } = actionParams;
- const { xJsonMode, convertToJson, setXJson, xJson } = useXJsonMode(
- documents && documents.length > 0 ? documents[0] : null
- );
- const onSelectMessageVariable = (variable: string) => {
- const value = (xJson ?? '').concat(` {{${variable}}}`);
- setXJson(value);
- // Keep the documents in sync with the editor content
- onDocumentsChange(convertToJson(value));
- };
-
- function onDocumentsChange(updatedDocuments: string) {
- try {
- const documentsJSON = JSON.parse(updatedDocuments);
- editAction('documents', [documentsJSON], index);
- // eslint-disable-next-line no-empty
- } catch (e) {}
- }
- return (
-
- onSelectMessageVariable(variable)}
- paramsProperty="documents"
- />
- }
- >
- {
- setXJson(xjson);
- // Keep the documents in sync with the editor content
- onDocumentsChange(convertToJson(xjson));
- }}
- />
-
-
- );
-};
-
// if the string == null or is empty, return null, else return string
function nullableString(str: string | null | undefined) {
if (str == null || str.trim() === '') return null;
return str;
}
+
+// eslint-disable-next-line import/no-default-export
+export { IndexActionConnectorFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx
new file mode 100644
index 0000000000000..5f05a56a228e2
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import ParamsFields from './es_index_params';
+
+describe('IndexParamsFields renders', () => {
+ test('all params fields is rendered', () => {
+ const actionParams = {
+ documents: [{ test: 123 }],
+ };
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ />
+ );
+ expect(
+ wrapper
+ .find('[data-test-subj="actionIndexDoc"]')
+ .first()
+ .prop('value')
+ ).toBe(`{
+ "test": 123
+}`);
+ expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx
new file mode 100644
index 0000000000000..0b095cdc26984
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx
@@ -0,0 +1,81 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { Fragment } from 'react';
+import { EuiFormRow, EuiCodeEditor } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useXJsonMode } from '../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks';
+import { ActionParamsProps } from '../../../../types';
+import { IndexActionParams } from '.././types';
+import { AddMessageVariables } from '../../add_message_variables';
+
+export const IndexParamsFields = ({
+ actionParams,
+ index,
+ editAction,
+ messageVariables,
+}: ActionParamsProps) => {
+ const { documents } = actionParams;
+ const { xJsonMode, convertToJson, setXJson, xJson } = useXJsonMode(
+ documents && documents.length > 0 ? documents[0] : null
+ );
+ const onSelectMessageVariable = (variable: string) => {
+ const value = (xJson ?? '').concat(` {{${variable}}}`);
+ setXJson(value);
+ // Keep the documents in sync with the editor content
+ onDocumentsChange(convertToJson(value));
+ };
+
+ function onDocumentsChange(updatedDocuments: string) {
+ try {
+ const documentsJSON = JSON.parse(updatedDocuments);
+ editAction('documents', [documentsJSON], index);
+ // eslint-disable-next-line no-empty
+ } catch (e) {}
+ }
+ return (
+
+ onSelectMessageVariable(variable)}
+ paramsProperty="documents"
+ />
+ }
+ >
+ {
+ setXJson(xjson);
+ // Keep the documents in sync with the editor content
+ onDocumentsChange(convertToJson(xjson));
+ }}
+ />
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export { IndexParamsFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/index.ts
new file mode 100644
index 0000000000000..6a2ebd9c4bc71
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { getActionType as getIndexActionType } from './es_index';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts
index 6ffd9b2c9ffde..8f49fa46dd54e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts
@@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { getActionType as getServerLogActionType } from './server_log';
-import { getActionType as getSlackActionType } from './slack';
-import { getActionType as getEmailActionType } from './email';
-import { getActionType as getIndexActionType } from './es_index';
-import { getActionType as getPagerDutyActionType } from './pagerduty';
-import { getActionType as getWebhookActionType } from './webhook';
+import { getServerLogActionType } from './server_log';
+import { getSlackActionType } from './slack';
+import { getEmailActionType } from './email';
+import { getIndexActionType } from './es_index';
+import { getPagerDutyActionType } from './pagerduty';
+import { getWebhookActionType } from './webhook';
import { TypeRegistry } from '../../type_registry';
import { ActionTypeModel } from '../../../types';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx
deleted file mode 100644
index f628457dc5162..0000000000000
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React, { FunctionComponent } from 'react';
-import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
-import { act } from 'react-dom/test-utils';
-import { TypeRegistry } from '../../type_registry';
-import { registerBuiltInActionTypes } from './index';
-import { ActionTypeModel, ActionParamsProps } from '../../../types';
-import {
- PagerDutyActionParams,
- EventActionOptions,
- SeverityActionOptions,
- PagerDutyActionConnector,
-} from './types';
-
-const ACTION_TYPE_ID = '.pagerduty';
-let actionTypeModel: ActionTypeModel;
-let deps: any;
-
-beforeAll(async () => {
- const actionTypeRegistry = new TypeRegistry();
- registerBuiltInActionTypes({ actionTypeRegistry });
- const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
- if (getResult !== null) {
- actionTypeModel = getResult;
- }
- deps = {
- docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
- };
-});
-
-describe('actionTypeRegistry.get() works', () => {
- test('action type static data is as expected', () => {
- expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
- expect(actionTypeModel.iconClass).toEqual('test-file-stub');
- });
-});
-
-describe('pagerduty connector validation', () => {
- test('connector validation succeeds when connector config is valid', () => {
- const actionConnector = {
- secrets: {
- routingKey: 'test',
- },
- id: 'test',
- actionTypeId: '.pagerduty',
- name: 'pagerduty',
- config: {
- apiUrl: 'http:\\test',
- },
- } as PagerDutyActionConnector;
-
- expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
- errors: {
- routingKey: [],
- },
- });
-
- delete actionConnector.config.apiUrl;
- actionConnector.secrets.routingKey = 'test1';
- expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
- errors: {
- routingKey: [],
- },
- });
- });
-
- test('connector validation fails when connector config is not valid', () => {
- const actionConnector = {
- secrets: {},
- id: 'test',
- actionTypeId: '.pagerduty',
- name: 'pagerduty',
- config: {
- apiUrl: 'http:\\test',
- },
- } as PagerDutyActionConnector;
-
- expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
- errors: {
- routingKey: ['A routing key is required.'],
- },
- });
- });
-});
-
-describe('pagerduty action params validation', () => {
- test('action params validation succeeds when action params is valid', () => {
- const actionParams = {
- eventAction: 'trigger',
- dedupKey: 'test',
- summary: '2323',
- source: 'source',
- severity: 'critical',
- timestamp: new Date().toISOString(),
- component: 'test',
- group: 'group',
- class: 'test class',
- };
-
- expect(actionTypeModel.validateParams(actionParams)).toEqual({
- errors: {
- summary: [],
- timestamp: [],
- },
- });
- });
-});
-
-describe('PagerDutyActionConnectorFields renders', () => {
- test('all connector fields is rendered', async () => {
- expect(actionTypeModel.actionConnectorFields).not.toBeNull();
- if (!actionTypeModel.actionConnectorFields) {
- return;
- }
- const ConnectorFields = actionTypeModel.actionConnectorFields;
- const actionConnector = {
- secrets: {
- routingKey: 'test',
- },
- id: 'test',
- actionTypeId: '.pagerduty',
- name: 'pagerduty',
- config: {
- apiUrl: 'http:\\test',
- },
- } as PagerDutyActionConnector;
- const wrapper = mountWithIntl(
- {}}
- editActionSecrets={() => {}}
- docLinks={deps!.docLinks}
- />
- );
-
- await act(async () => {
- await nextTick();
- wrapper.update();
- });
- expect(wrapper.find('[data-test-subj="pagerdutyApiUrlInput"]').length > 0).toBeTruthy();
- expect(
- wrapper
- .find('[data-test-subj="pagerdutyApiUrlInput"]')
- .first()
- .prop('value')
- ).toBe('http:\\test');
- expect(wrapper.find('[data-test-subj="pagerdutyRoutingKeyInput"]').length > 0).toBeTruthy();
- });
-});
-
-describe('PagerDutyParamsFields renders', () => {
- test('all params fields is rendered', () => {
- expect(actionTypeModel.actionParamsFields).not.toBeNull();
- if (!actionTypeModel.actionParamsFields) {
- return;
- }
- const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent<
- ActionParamsProps
- >;
- const actionParams = {
- eventAction: EventActionOptions.TRIGGER,
- dedupKey: 'test',
- summary: '2323',
- source: 'source',
- severity: SeverityActionOptions.CRITICAL,
- timestamp: new Date().toISOString(),
- component: 'test',
- group: 'group',
- class: 'test class',
- };
- const wrapper = mountWithIntl(
- {}}
- index={0}
- />
- );
- expect(wrapper.find('[data-test-subj="severitySelect"]').length > 0).toBeTruthy();
- expect(
- wrapper
- .find('[data-test-subj="severitySelect"]')
- .first()
- .prop('value')
- ).toStrictEqual('critical');
- expect(wrapper.find('[data-test-subj="eventActionSelect"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="dedupKeyInput"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="timestampInput"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="componentInput"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="groupInput"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="sourceInput"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="pagerdutySummaryInput"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="dedupKeyAddVariableButton"]').length > 0).toBeTruthy();
- });
-});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/index.ts
new file mode 100644
index 0000000000000..9128ec81391ab
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { getActionType as getPagerDutyActionType } from './pagerduty';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.svg
similarity index 100%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.svg
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.svg
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx
new file mode 100644
index 0000000000000..ba7eb598c120d
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx
@@ -0,0 +1,99 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { TypeRegistry } from '../../../type_registry';
+import { registerBuiltInActionTypes } from '.././index';
+import { ActionTypeModel } from '../../../../types';
+import { PagerDutyActionConnector } from '.././types';
+
+const ACTION_TYPE_ID = '.pagerduty';
+let actionTypeModel: ActionTypeModel;
+
+beforeAll(() => {
+ const actionTypeRegistry = new TypeRegistry();
+ registerBuiltInActionTypes({ actionTypeRegistry });
+ const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
+ if (getResult !== null) {
+ actionTypeModel = getResult;
+ }
+});
+
+describe('actionTypeRegistry.get() works', () => {
+ test('action type static data is as expected', () => {
+ expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
+ expect(actionTypeModel.iconClass).toEqual('test-file-stub');
+ });
+});
+
+describe('pagerduty connector validation', () => {
+ test('connector validation succeeds when connector config is valid', () => {
+ const actionConnector = {
+ secrets: {
+ routingKey: 'test',
+ },
+ id: 'test',
+ actionTypeId: '.pagerduty',
+ name: 'pagerduty',
+ config: {
+ apiUrl: 'http:\\test',
+ },
+ } as PagerDutyActionConnector;
+
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {
+ routingKey: [],
+ },
+ });
+
+ delete actionConnector.config.apiUrl;
+ actionConnector.secrets.routingKey = 'test1';
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {
+ routingKey: [],
+ },
+ });
+ });
+
+ test('connector validation fails when connector config is not valid', () => {
+ const actionConnector = {
+ secrets: {},
+ id: 'test',
+ actionTypeId: '.pagerduty',
+ name: 'pagerduty',
+ config: {
+ apiUrl: 'http:\\test',
+ },
+ } as PagerDutyActionConnector;
+
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {
+ routingKey: ['A routing key is required.'],
+ },
+ });
+ });
+});
+
+describe('pagerduty action params validation', () => {
+ test('action params validation succeeds when action params is valid', () => {
+ const actionParams = {
+ eventAction: 'trigger',
+ dedupKey: 'test',
+ summary: '2323',
+ source: 'source',
+ severity: 'critical',
+ timestamp: new Date().toISOString(),
+ component: 'test',
+ group: 'group',
+ class: 'test class',
+ };
+
+ expect(actionTypeModel.validateParams(actionParams)).toEqual({
+ errors: {
+ summary: [],
+ timestamp: [],
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx
new file mode 100644
index 0000000000000..5e29fca397180
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx
@@ -0,0 +1,96 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { lazy } from 'react';
+import { i18n } from '@kbn/i18n';
+import moment from 'moment';
+import { ActionTypeModel, ValidationResult } from '../../../../types';
+import { PagerDutyActionParams, PagerDutyActionConnector } from '.././types';
+import pagerDutySvg from './pagerduty.svg';
+import { hasMustacheTokens } from '../../../lib/has_mustache_tokens';
+
+export function getActionType(): ActionTypeModel {
+ return {
+ id: '.pagerduty',
+ iconClass: pagerDutySvg,
+ selectMessage: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText',
+ {
+ defaultMessage: 'Send an event in PagerDuty.',
+ }
+ ),
+ actionTypeTitle: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle',
+ {
+ defaultMessage: 'Send to PagerDuty',
+ }
+ ),
+ validateConnector: (action: PagerDutyActionConnector): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ routingKey: new Array(),
+ };
+ validationResult.errors = errors;
+ if (!action.secrets.routingKey) {
+ errors.routingKey.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText',
+ {
+ defaultMessage: 'A routing key is required.',
+ }
+ )
+ );
+ }
+ return validationResult;
+ },
+ validateParams: (actionParams: PagerDutyActionParams): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ summary: new Array(),
+ timestamp: new Array(),
+ };
+ validationResult.errors = errors;
+ if (!actionParams.summary?.length) {
+ errors.summary.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText',
+ {
+ defaultMessage: 'Summary is required.',
+ }
+ )
+ );
+ }
+ if (actionParams.timestamp && !hasMustacheTokens(actionParams.timestamp)) {
+ if (isNaN(Date.parse(actionParams.timestamp))) {
+ const { nowShortFormat, nowLongFormat } = getValidTimestampExamples();
+ errors.timestamp.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.invalidTimestamp',
+ {
+ defaultMessage:
+ 'Timestamp must be a valid date, such as {nowShortFormat} or {nowLongFormat}.',
+ values: {
+ nowShortFormat,
+ nowLongFormat,
+ },
+ }
+ )
+ );
+ }
+ }
+ return validationResult;
+ },
+ actionConnectorFields: lazy(() => import('./pagerduty_connectors')),
+ actionParamsFields: lazy(() => import('./pagerduty_params')),
+ };
+}
+
+function getValidTimestampExamples() {
+ const now = moment();
+ return {
+ nowShortFormat: now.format('YYYY-MM-DD'),
+ nowLongFormat: now.format('YYYY-MM-DD h:mm:ss'),
+ };
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx
new file mode 100644
index 0000000000000..3f3fba1599bd2
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
+import { act } from 'react-dom/test-utils';
+import { PagerDutyActionConnector } from '.././types';
+import PagerDutyActionConnectorFields from './pagerduty_connectors';
+import { DocLinksStart } from 'kibana/public';
+
+describe('PagerDutyActionConnectorFields renders', () => {
+ test('all connector fields is rendered', async () => {
+ const actionConnector = {
+ secrets: {
+ routingKey: 'test',
+ },
+ id: 'test',
+ actionTypeId: '.pagerduty',
+ name: 'pagerduty',
+ config: {
+ apiUrl: 'http:\\test',
+ },
+ } as PagerDutyActionConnector;
+ const deps = {
+ docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
+ };
+
+ const wrapper = mountWithIntl(
+ {}}
+ editActionSecrets={() => {}}
+ docLinks={deps!.docLinks}
+ />
+ );
+
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+
+ expect(wrapper.find('[data-test-subj="pagerdutyApiUrlInput"]').length > 0).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="pagerdutyApiUrlInput"]')
+ .first()
+ .prop('value')
+ ).toBe('http:\\test');
+ expect(wrapper.find('[data-test-subj="pagerdutyRoutingKeyInput"]').length > 0).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx
new file mode 100644
index 0000000000000..48da3f1778b48
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { Fragment } from 'react';
+import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { ActionConnectorFieldsProps } from '../../../../types';
+import { PagerDutyActionConnector } from '.././types';
+
+const PagerDutyActionConnectorFields: React.FunctionComponent> = ({ errors, action, editActionConfig, editActionSecrets, docLinks }) => {
+ const { apiUrl } = action.config;
+ const { routingKey } = action.secrets;
+ return (
+
+
+ ) => {
+ editActionConfig('apiUrl', e.target.value);
+ }}
+ onBlur={() => {
+ if (!apiUrl) {
+ editActionConfig('apiUrl', '');
+ }
+ }}
+ />
+
+
+
+
+ }
+ error={errors.routingKey}
+ isInvalid={errors.routingKey.length > 0 && routingKey !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel',
+ {
+ defaultMessage: 'Integration key',
+ }
+ )}
+ >
+ 0 && routingKey !== undefined}
+ name="routingKey"
+ value={routingKey || ''}
+ data-test-subj="pagerdutyRoutingKeyInput"
+ onChange={(e: React.ChangeEvent) => {
+ editActionSecrets('routingKey', e.target.value);
+ }}
+ onBlur={() => {
+ if (!routingKey) {
+ editActionSecrets('routingKey', '');
+ }
+ }}
+ />
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export { PagerDutyActionConnectorFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx
new file mode 100644
index 0000000000000..d1b32f545c335
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { EventActionOptions, SeverityActionOptions } from '.././types';
+import PagerDutyParamsFields from './pagerduty_params';
+
+describe('PagerDutyParamsFields renders', () => {
+ test('all params fields is rendered', () => {
+ const actionParams = {
+ eventAction: EventActionOptions.TRIGGER,
+ dedupKey: 'test',
+ summary: '2323',
+ source: 'source',
+ severity: SeverityActionOptions.CRITICAL,
+ timestamp: new Date().toISOString(),
+ component: 'test',
+ group: 'group',
+ class: 'test class',
+ };
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ />
+ );
+ expect(wrapper.find('[data-test-subj="severitySelect"]').length > 0).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="severitySelect"]')
+ .first()
+ .prop('value')
+ ).toStrictEqual('critical');
+ expect(wrapper.find('[data-test-subj="eventActionSelect"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="dedupKeyInput"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="timestampInput"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="componentInput"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="groupInput"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="sourceInput"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="pagerdutySummaryInput"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="dedupKeyAddVariableButton"]').length > 0).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx
similarity index 67%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx
index 5ad1f2fffecce..590eba5dad936 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx
@@ -4,178 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
-import {
- EuiFieldText,
- EuiFlexGroup,
- EuiFlexItem,
- EuiFormRow,
- EuiSelect,
- EuiLink,
-} from '@elastic/eui';
+import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import moment from 'moment';
-import {
- ActionTypeModel,
- ActionConnectorFieldsProps,
- ValidationResult,
- ActionParamsProps,
-} from '../../../types';
-import { PagerDutyActionParams, PagerDutyActionConnector } from './types';
-import pagerDutySvg from './pagerduty.svg';
-import { AddMessageVariables } from '../add_message_variables';
-import { hasMustacheTokens } from '../../lib/has_mustache_tokens';
-
-export function getActionType(): ActionTypeModel {
- return {
- id: '.pagerduty',
- iconClass: pagerDutySvg,
- selectMessage: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText',
- {
- defaultMessage: 'Send an event in PagerDuty.',
- }
- ),
- actionTypeTitle: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle',
- {
- defaultMessage: 'Send to PagerDuty',
- }
- ),
- validateConnector: (action: PagerDutyActionConnector): ValidationResult => {
- const validationResult = { errors: {} };
- const errors = {
- routingKey: new Array(),
- };
- validationResult.errors = errors;
- if (!action.secrets.routingKey) {
- errors.routingKey.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText',
- {
- defaultMessage: 'A routing key is required.',
- }
- )
- );
- }
- return validationResult;
- },
- validateParams: (actionParams: PagerDutyActionParams): ValidationResult => {
- const validationResult = { errors: {} };
- const errors = {
- summary: new Array(),
- timestamp: new Array(),
- };
- validationResult.errors = errors;
- if (!actionParams.summary?.length) {
- errors.summary.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText',
- {
- defaultMessage: 'Summary is required.',
- }
- )
- );
- }
- if (actionParams.timestamp && !hasMustacheTokens(actionParams.timestamp)) {
- if (isNaN(Date.parse(actionParams.timestamp))) {
- const { nowShortFormat, nowLongFormat } = getValidTimestampExamples();
- errors.timestamp.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.invalidTimestamp',
- {
- defaultMessage:
- 'Timestamp must be a valid date, such as {nowShortFormat} or {nowLongFormat}.',
- values: {
- nowShortFormat,
- nowLongFormat,
- },
- }
- )
- );
- }
- }
- return validationResult;
- },
- actionConnectorFields: PagerDutyActionConnectorFields,
- actionParamsFields: PagerDutyParamsFields,
- };
-}
-
-const PagerDutyActionConnectorFields: React.FunctionComponent> = ({ errors, action, editActionConfig, editActionSecrets, docLinks }) => {
- const { apiUrl } = action.config;
- const { routingKey } = action.secrets;
- return (
-
-
- ) => {
- editActionConfig('apiUrl', e.target.value);
- }}
- onBlur={() => {
- if (!apiUrl) {
- editActionConfig('apiUrl', '');
- }
- }}
- />
-
-
-
-
- }
- error={errors.routingKey}
- isInvalid={errors.routingKey.length > 0 && routingKey !== undefined}
- label={i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel',
- {
- defaultMessage: 'Integration key',
- }
- )}
- >
- 0 && routingKey !== undefined}
- name="routingKey"
- value={routingKey || ''}
- data-test-subj="pagerdutyRoutingKeyInput"
- onChange={(e: React.ChangeEvent) => {
- editActionSecrets('routingKey', e.target.value);
- }}
- onBlur={() => {
- if (!routingKey) {
- editActionSecrets('routingKey', '');
- }
- }}
- />
-
-
- );
-};
+import { ActionParamsProps } from '../../../../types';
+import { PagerDutyActionParams } from '.././types';
+import { AddMessageVariables } from '../../add_message_variables';
const PagerDutyParamsFields: React.FunctionComponent> = ({
actionParams,
@@ -561,10 +394,5 @@ const PagerDutyParamsFields: React.FunctionComponent {
- const actionTypeRegistry = new TypeRegistry();
- registerBuiltInActionTypes({ actionTypeRegistry });
- const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
- if (getResult !== null) {
- actionTypeModel = getResult;
- }
-});
-
-describe('actionTypeRegistry.get() works', () => {
- test('action type static data is as expected', () => {
- expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
- expect(actionTypeModel.iconClass).toEqual('logsApp');
- });
-});
-
-describe('server-log connector validation', () => {
- test('connector validation succeeds when connector config is valid', () => {
- const actionConnector = {
- secrets: {},
- id: 'test',
- actionTypeId: '.server-log',
- name: 'server-log',
- config: {},
- } as ActionConnector;
-
- expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
- errors: {},
- });
- });
-});
-
-describe('action params validation', () => {
- test('action params validation succeeds when action params is valid', () => {
- const actionParams = {
- message: 'test message',
- level: 'trace',
- };
-
- expect(actionTypeModel.validateParams(actionParams)).toEqual({
- errors: { message: [] },
- });
- });
-});
-
-describe('ServerLogParamsFields renders', () => {
- test('all params fields is rendered', () => {
- expect(actionTypeModel.actionParamsFields).not.toBeNull();
- if (!actionTypeModel.actionParamsFields) {
- return;
- }
- const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent<
- ActionParamsProps
- >;
- const actionParams = {
- level: ServerLogLevelOptions.TRACE,
- message: 'test',
- };
- const wrapper = mountWithIntl(
- {}}
- index={0}
- defaultMessage={'test default message'}
- />
- );
- expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy();
- expect(
- wrapper
- .find('[data-test-subj="loggingLevelSelect"]')
- .first()
- .prop('value')
- ).toStrictEqual('trace');
- expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy();
- });
-
- test('level param field is rendered with default value if not selected', () => {
- expect(actionTypeModel.actionParamsFields).not.toBeNull();
- if (!actionTypeModel.actionParamsFields) {
- return;
- }
- const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent<
- ActionParamsProps
- >;
- const actionParams = {
- message: 'test message',
- level: ServerLogLevelOptions.INFO,
- };
- const wrapper = mountWithIntl(
- {}}
- index={0}
- />
- );
- expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy();
- expect(
- wrapper
- .find('[data-test-subj="loggingLevelSelect"]')
- .first()
- .prop('value')
- ).toStrictEqual('info');
- expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy();
- });
-
- test('params validation fails when message is not valid', () => {
- const actionParams = {
- message: '',
- };
-
- expect(actionTypeModel.validateParams(actionParams)).toEqual({
- errors: {
- message: ['Message is required.'],
- },
- });
- });
-});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/index.ts
new file mode 100644
index 0000000000000..f85c7460d2ece
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { getActionType as getServerLogActionType } from './server_log';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx
new file mode 100644
index 0000000000000..3bb5ea68a3040
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { TypeRegistry } from '../../../type_registry';
+import { registerBuiltInActionTypes } from '.././index';
+import { ActionTypeModel, ActionConnector } from '../../../../types';
+
+const ACTION_TYPE_ID = '.server-log';
+let actionTypeModel: ActionTypeModel;
+
+beforeAll(() => {
+ const actionTypeRegistry = new TypeRegistry();
+ registerBuiltInActionTypes({ actionTypeRegistry });
+ const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
+ if (getResult !== null) {
+ actionTypeModel = getResult;
+ }
+});
+
+describe('actionTypeRegistry.get() works', () => {
+ test('action type static data is as expected', () => {
+ expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
+ expect(actionTypeModel.iconClass).toEqual('logsApp');
+ });
+});
+
+describe('server-log connector validation', () => {
+ test('connector validation succeeds when connector config is valid', () => {
+ const actionConnector = {
+ secrets: {},
+ id: 'test',
+ actionTypeId: '.server-log',
+ name: 'server-log',
+ config: {},
+ } as ActionConnector;
+
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {},
+ });
+ });
+});
+
+describe('action params validation', () => {
+ test('action params validation succeeds when action params is valid', () => {
+ const actionParams = {
+ message: 'test message',
+ level: 'trace',
+ };
+
+ expect(actionTypeModel.validateParams(actionParams)).toEqual({
+ errors: { message: [] },
+ });
+ });
+
+ test('params validation fails when message is not valid', () => {
+ const actionParams = {
+ message: '',
+ };
+
+ expect(actionTypeModel.validateParams(actionParams)).toEqual({
+ errors: {
+ message: ['Message is required.'],
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx
new file mode 100644
index 0000000000000..390ccf6a494e9
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { lazy } from 'react';
+import { i18n } from '@kbn/i18n';
+import { ActionTypeModel, ValidationResult } from '../../../../types';
+import { ServerLogActionParams } from '../types';
+
+export function getActionType(): ActionTypeModel {
+ return {
+ id: '.server-log',
+ iconClass: 'logsApp',
+ selectMessage: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText',
+ {
+ defaultMessage: 'Add a message to a Kibana log.',
+ }
+ ),
+ actionTypeTitle: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle',
+ {
+ defaultMessage: 'Send to Server log',
+ }
+ ),
+ validateConnector: (): ValidationResult => {
+ return { errors: {} };
+ },
+ validateParams: (actionParams: ServerLogActionParams): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ message: new Array(),
+ };
+ validationResult.errors = errors;
+ if (!actionParams.message?.length) {
+ errors.message.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText',
+ {
+ defaultMessage: 'Message is required.',
+ }
+ )
+ );
+ }
+ return validationResult;
+ },
+ actionConnectorFields: null,
+ actionParamsFields: lazy(() => import('./server_log_params')),
+ };
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx
new file mode 100644
index 0000000000000..d2e1d1e4500bc
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx
@@ -0,0 +1,58 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { ServerLogLevelOptions } from '.././types';
+import ServerLogParamsFields from './server_log_params';
+
+describe('ServerLogParamsFields renders', () => {
+ test('all params fields is rendered', () => {
+ const actionParams = {
+ level: ServerLogLevelOptions.TRACE,
+ message: 'test',
+ };
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ defaultMessage={'test default message'}
+ />
+ );
+ expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="loggingLevelSelect"]')
+ .first()
+ .prop('value')
+ ).toStrictEqual('trace');
+ expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy();
+ });
+
+ test('level param field is rendered with default value if not selected', () => {
+ const actionParams = {
+ message: 'test message',
+ level: ServerLogLevelOptions.INFO,
+ };
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ />
+ );
+ expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="loggingLevelSelect"]')
+ .first()
+ .prop('value')
+ ).toStrictEqual('info');
+ expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx
similarity index 67%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log.tsx
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx
index a4c83ce76f04e..64d39e238be76 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx
@@ -6,51 +6,9 @@
import React, { Fragment, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSelect, EuiTextArea, EuiFormRow } from '@elastic/eui';
-import { ActionTypeModel, ValidationResult, ActionParamsProps } from '../../../types';
-import { ServerLogActionParams } from './types';
-import { AddMessageVariables } from '../add_message_variables';
-
-export function getActionType(): ActionTypeModel {
- return {
- id: '.server-log',
- iconClass: 'logsApp',
- selectMessage: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText',
- {
- defaultMessage: 'Add a message to a Kibana log.',
- }
- ),
- actionTypeTitle: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle',
- {
- defaultMessage: 'Send to Server log',
- }
- ),
- validateConnector: (): ValidationResult => {
- return { errors: {} };
- },
- validateParams: (actionParams: ServerLogActionParams): ValidationResult => {
- const validationResult = { errors: {} };
- const errors = {
- message: new Array(),
- };
- validationResult.errors = errors;
- if (!actionParams.message?.length) {
- errors.message.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText',
- {
- defaultMessage: 'Message is required.',
- }
- )
- );
- }
- return validationResult;
- },
- actionConnectorFields: null,
- actionParamsFields: ServerLogParamsFields,
- };
-}
+import { ActionParamsProps } from '../../../../types';
+import { ServerLogActionParams } from '.././types';
+import { AddMessageVariables } from '../../add_message_variables';
export const ServerLogParamsFields: React.FunctionComponent
);
};
+
+// eslint-disable-next-line import/no-default-export
+export { ServerLogParamsFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.test.tsx
deleted file mode 100644
index a2865b27bc06c..0000000000000
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.test.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React, { FunctionComponent } from 'react';
-import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
-import { act } from 'react-dom/test-utils';
-import { TypeRegistry } from '../../type_registry';
-import { registerBuiltInActionTypes } from './index';
-import { ActionTypeModel, ActionParamsProps } from '../../../types';
-import { SlackActionParams, SlackActionConnector } from './types';
-
-const ACTION_TYPE_ID = '.slack';
-let actionTypeModel: ActionTypeModel;
-
-let deps: any;
-
-beforeAll(async () => {
- const actionTypeRegistry = new TypeRegistry();
- registerBuiltInActionTypes({ actionTypeRegistry });
- const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
- if (getResult !== null) {
- actionTypeModel = getResult;
- }
- deps = {
- docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
- };
-});
-
-describe('actionTypeRegistry.get() works', () => {
- test('action type static data is as expected', () => {
- expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
- expect(actionTypeModel.iconClass).toEqual('logoSlack');
- });
-});
-
-describe('slack connector validation', () => {
- test('connector validation succeeds when connector config is valid', () => {
- const actionConnector = {
- secrets: {
- webhookUrl: 'http:\\test',
- },
- id: 'test',
- actionTypeId: '.email',
- name: 'email',
- config: {},
- } as SlackActionConnector;
-
- expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
- errors: {
- webhookUrl: [],
- },
- });
- });
-
- test('connector validation fails when connector config is not valid', () => {
- const actionConnector = {
- secrets: {},
- id: 'test',
- actionTypeId: '.email',
- name: 'email',
- config: {},
- } as SlackActionConnector;
-
- expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
- errors: {
- webhookUrl: ['Webhook URL is required.'],
- },
- });
- });
-});
-
-describe('slack action params validation', () => {
- test('if action params validation succeeds when action params is valid', () => {
- const actionParams = {
- message: 'message {test}',
- };
-
- expect(actionTypeModel.validateParams(actionParams)).toEqual({
- errors: { message: [] },
- });
- });
-});
-
-describe('SlackActionFields renders', () => {
- test('all connector fields is rendered', async () => {
- expect(actionTypeModel.actionConnectorFields).not.toBeNull();
- if (!actionTypeModel.actionConnectorFields) {
- return;
- }
- const ConnectorFields = actionTypeModel.actionConnectorFields;
- const actionConnector = {
- secrets: {
- webhookUrl: 'http:\\test',
- },
- id: 'test',
- actionTypeId: '.email',
- name: 'email',
- config: {},
- } as SlackActionConnector;
- const wrapper = mountWithIntl(
- {}}
- editActionSecrets={() => {}}
- docLinks={deps!.docLinks}
- />
- );
-
- await act(async () => {
- await nextTick();
- wrapper.update();
- });
- expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').length > 0).toBeTruthy();
- expect(
- wrapper
- .find('[data-test-subj="slackWebhookUrlInput"]')
- .first()
- .prop('value')
- ).toBe('http:\\test');
- });
-});
-
-describe('SlackParamsFields renders', () => {
- test('all params fields is rendered', () => {
- expect(actionTypeModel.actionParamsFields).not.toBeNull();
- if (!actionTypeModel.actionParamsFields) {
- return;
- }
- const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent<
- ActionParamsProps
- >;
- const actionParams = {
- message: 'test message',
- };
- const wrapper = mountWithIntl(
- {}}
- index={0}
- />
- );
- expect(wrapper.find('[data-test-subj="slackMessageTextArea"]').length > 0).toBeTruthy();
- expect(
- wrapper
- .find('[data-test-subj="slackMessageTextArea"]')
- .first()
- .prop('value')
- ).toStrictEqual('test message');
- });
-
- test('params validation fails when message is not valid', () => {
- const actionParams = {
- message: '',
- };
-
- expect(actionTypeModel.validateParams(actionParams)).toEqual({
- errors: {
- message: ['Message is required.'],
- },
- });
- });
-});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.tsx
deleted file mode 100644
index 03f7a2f492d54..0000000000000
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React, { Fragment, useEffect } from 'react';
-import { EuiFieldText, EuiTextArea, EuiFormRow, EuiLink } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import {
- ActionTypeModel,
- ActionConnectorFieldsProps,
- ValidationResult,
- ActionParamsProps,
-} from '../../../types';
-import { SlackActionParams, SlackActionConnector } from './types';
-import { AddMessageVariables } from '../add_message_variables';
-
-export function getActionType(): ActionTypeModel {
- return {
- id: '.slack',
- iconClass: 'logoSlack',
- selectMessage: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText',
- {
- defaultMessage: 'Send a message to a Slack channel or user.',
- }
- ),
- actionTypeTitle: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle',
- {
- defaultMessage: 'Send to Slack',
- }
- ),
- validateConnector: (action: SlackActionConnector): ValidationResult => {
- const validationResult = { errors: {} };
- const errors = {
- webhookUrl: new Array(),
- };
- validationResult.errors = errors;
- if (!action.secrets.webhookUrl) {
- errors.webhookUrl.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText',
- {
- defaultMessage: 'Webhook URL is required.',
- }
- )
- );
- }
- return validationResult;
- },
- validateParams: (actionParams: SlackActionParams): ValidationResult => {
- const validationResult = { errors: {} };
- const errors = {
- message: new Array(),
- };
- validationResult.errors = errors;
- if (!actionParams.message?.length) {
- errors.message.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText',
- {
- defaultMessage: 'Message is required.',
- }
- )
- );
- }
- return validationResult;
- },
- actionConnectorFields: SlackActionFields,
- actionParamsFields: SlackParamsFields,
- };
-}
-
-const SlackActionFields: React.FunctionComponent> = ({ action, editActionSecrets, errors, docLinks }) => {
- const { webhookUrl } = action.secrets;
-
- return (
-
-
-
-
- }
- error={errors.webhookUrl}
- isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined}
- label={i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel',
- {
- defaultMessage: 'Webhook URL',
- }
- )}
- >
- 0 && webhookUrl !== undefined}
- name="webhookUrl"
- placeholder="Example: https://hooks.slack.com/services"
- value={webhookUrl || ''}
- data-test-subj="slackWebhookUrlInput"
- onChange={e => {
- editActionSecrets('webhookUrl', e.target.value);
- }}
- onBlur={() => {
- if (!webhookUrl) {
- editActionSecrets('webhookUrl', '');
- }
- }}
- />
-
-
- );
-};
-
-const SlackParamsFields: React.FunctionComponent> = ({
- actionParams,
- editAction,
- index,
- errors,
- messageVariables,
- defaultMessage,
-}) => {
- const { message } = actionParams;
- useEffect(() => {
- if (!message && defaultMessage && defaultMessage.length > 0) {
- editAction('message', defaultMessage, index);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const onSelectMessageVariable = (paramsProperty: string, variable: string) => {
- editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index);
- };
-
- return (
-
- 0 && message !== undefined}
- label={i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel',
- {
- defaultMessage: 'Message',
- }
- )}
- labelAppend={
-
- onSelectMessageVariable('message', variable)
- }
- paramsProperty="message"
- />
- }
- >
- 0 && message !== undefined}
- name="message"
- value={message || ''}
- data-test-subj="slackMessageTextArea"
- onChange={e => {
- editAction('message', e.target.value, index);
- }}
- onBlur={() => {
- if (!message) {
- editAction('message', '', index);
- }
- }}
- />
-
-
- );
-};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/index.ts
new file mode 100644
index 0000000000000..64ab6670754c9
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { getActionType as getSlackActionType } from './slack';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx
new file mode 100644
index 0000000000000..78f4161cac827
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx
@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { TypeRegistry } from '../../../type_registry';
+import { registerBuiltInActionTypes } from '.././index';
+import { ActionTypeModel } from '../../../../types';
+import { SlackActionConnector } from '../types';
+
+const ACTION_TYPE_ID = '.slack';
+let actionTypeModel: ActionTypeModel;
+
+beforeAll(async () => {
+ const actionTypeRegistry = new TypeRegistry();
+ registerBuiltInActionTypes({ actionTypeRegistry });
+ const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
+ if (getResult !== null) {
+ actionTypeModel = getResult;
+ }
+});
+
+describe('actionTypeRegistry.get() works', () => {
+ test('action type static data is as expected', () => {
+ expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
+ expect(actionTypeModel.iconClass).toEqual('logoSlack');
+ });
+});
+
+describe('slack connector validation', () => {
+ test('connector validation succeeds when connector config is valid', () => {
+ const actionConnector = {
+ secrets: {
+ webhookUrl: 'http:\\test',
+ },
+ id: 'test',
+ actionTypeId: '.email',
+ name: 'email',
+ config: {},
+ } as SlackActionConnector;
+
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {
+ webhookUrl: [],
+ },
+ });
+ });
+
+ test('connector validation fails when connector config is not valid', () => {
+ const actionConnector = {
+ secrets: {},
+ id: 'test',
+ actionTypeId: '.email',
+ name: 'email',
+ config: {},
+ } as SlackActionConnector;
+
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {
+ webhookUrl: ['Webhook URL is required.'],
+ },
+ });
+ });
+});
+
+describe('slack action params validation', () => {
+ test('if action params validation succeeds when action params is valid', () => {
+ const actionParams = {
+ message: 'message {test}',
+ };
+
+ expect(actionTypeModel.validateParams(actionParams)).toEqual({
+ errors: { message: [] },
+ });
+ });
+
+ test('params validation fails when message is not valid', () => {
+ const actionParams = {
+ message: '',
+ };
+
+ expect(actionTypeModel.validateParams(actionParams)).toEqual({
+ errors: {
+ message: ['Message is required.'],
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx
new file mode 100644
index 0000000000000..5d39cdb5ac387
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { lazy } from 'react';
+import { i18n } from '@kbn/i18n';
+import { ActionTypeModel, ValidationResult } from '../../../../types';
+import { SlackActionParams, SlackActionConnector } from '../types';
+
+export function getActionType(): ActionTypeModel {
+ return {
+ id: '.slack',
+ iconClass: 'logoSlack',
+ selectMessage: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText',
+ {
+ defaultMessage: 'Send a message to a Slack channel or user.',
+ }
+ ),
+ actionTypeTitle: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle',
+ {
+ defaultMessage: 'Send to Slack',
+ }
+ ),
+ validateConnector: (action: SlackActionConnector): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ webhookUrl: new Array(),
+ };
+ validationResult.errors = errors;
+ if (!action.secrets.webhookUrl) {
+ errors.webhookUrl.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText',
+ {
+ defaultMessage: 'Webhook URL is required.',
+ }
+ )
+ );
+ }
+ return validationResult;
+ },
+ validateParams: (actionParams: SlackActionParams): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ message: new Array(),
+ };
+ validationResult.errors = errors;
+ if (!actionParams.message?.length) {
+ errors.message.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText',
+ {
+ defaultMessage: 'Message is required.',
+ }
+ )
+ );
+ }
+ return validationResult;
+ },
+ actionConnectorFields: lazy(() => import('./slack_connectors')),
+ actionParamsFields: lazy(() => import('./slack_params')),
+ };
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx
new file mode 100644
index 0000000000000..7d7f6fc086928
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
+import { act } from '@testing-library/react';
+import { SlackActionConnector } from '../types';
+import SlackActionFields from './slack_connectors';
+import { DocLinksStart } from 'kibana/public';
+
+describe('SlackActionFields renders', () => {
+ test('all connector fields is rendered', async () => {
+ const actionConnector = {
+ secrets: {
+ webhookUrl: 'http:\\test',
+ },
+ id: 'test',
+ actionTypeId: '.email',
+ name: 'email',
+ config: {},
+ } as SlackActionConnector;
+ const deps = {
+ docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
+ };
+ const wrapper = mountWithIntl(
+ {}}
+ editActionSecrets={() => {}}
+ docLinks={deps!.docLinks}
+ />
+ );
+
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+ expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').length > 0).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="slackWebhookUrlInput"]')
+ .first()
+ .prop('value')
+ ).toBe('http:\\test');
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx
new file mode 100644
index 0000000000000..ad3e76ad8ae6c
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { Fragment } from 'react';
+import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { ActionConnectorFieldsProps } from '../../../../types';
+import { SlackActionConnector } from '../types';
+
+const SlackActionFields: React.FunctionComponent> = ({ action, editActionSecrets, errors, docLinks }) => {
+ const { webhookUrl } = action.secrets;
+
+ return (
+
+
+
+
+ }
+ error={errors.webhookUrl}
+ isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel',
+ {
+ defaultMessage: 'Webhook URL',
+ }
+ )}
+ >
+ 0 && webhookUrl !== undefined}
+ name="webhookUrl"
+ placeholder="Example: https://hooks.slack.com/services"
+ value={webhookUrl || ''}
+ data-test-subj="slackWebhookUrlInput"
+ onChange={e => {
+ editActionSecrets('webhookUrl', e.target.value);
+ }}
+ onBlur={() => {
+ if (!webhookUrl) {
+ editActionSecrets('webhookUrl', '');
+ }
+ }}
+ />
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export { SlackActionFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx
new file mode 100644
index 0000000000000..4183aeb48dec7
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import SlackParamsFields from './slack_params';
+
+describe('SlackParamsFields renders', () => {
+ test('all params fields is rendered', () => {
+ const actionParams = {
+ message: 'test message',
+ };
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ />
+ );
+ expect(wrapper.find('[data-test-subj="slackMessageTextArea"]').length > 0).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="slackMessageTextArea"]')
+ .first()
+ .prop('value')
+ ).toStrictEqual('test message');
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx
new file mode 100644
index 0000000000000..42fefdd41ef67
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { Fragment, useEffect } from 'react';
+import { EuiTextArea, EuiFormRow } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { ActionParamsProps } from '../../../../types';
+import { SlackActionParams } from '../types';
+import { AddMessageVariables } from '../../add_message_variables';
+
+const SlackParamsFields: React.FunctionComponent> = ({
+ actionParams,
+ editAction,
+ index,
+ errors,
+ messageVariables,
+ defaultMessage,
+}) => {
+ const { message } = actionParams;
+ useEffect(() => {
+ if (!message && defaultMessage && defaultMessage.length > 0) {
+ editAction('message', defaultMessage, index);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const onSelectMessageVariable = (paramsProperty: string, variable: string) => {
+ editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index);
+ };
+
+ return (
+
+ 0 && message !== undefined}
+ label={i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel',
+ {
+ defaultMessage: 'Message',
+ }
+ )}
+ labelAppend={
+
+ onSelectMessageVariable('message', variable)
+ }
+ paramsProperty="message"
+ />
+ }
+ >
+ 0 && message !== undefined}
+ name="message"
+ value={message || ''}
+ data-test-subj="slackMessageTextArea"
+ onChange={e => {
+ editAction('message', e.target.value, index);
+ }}
+ onBlur={() => {
+ if (!message) {
+ editAction('message', '', index);
+ }
+ }}
+ />
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export { SlackParamsFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx
deleted file mode 100644
index 7d0082708075f..0000000000000
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React, { FunctionComponent } from 'react';
-import { mountWithIntl } from 'test_utils/enzyme_helpers';
-import { TypeRegistry } from '../../type_registry';
-import { registerBuiltInActionTypes } from './index';
-import { ActionTypeModel, ActionParamsProps } from '../../../types';
-import { WebhookActionParams, WebhookActionConnector } from './types';
-
-const ACTION_TYPE_ID = '.webhook';
-let actionTypeModel: ActionTypeModel;
-
-beforeAll(() => {
- const actionTypeRegistry = new TypeRegistry();
- registerBuiltInActionTypes({ actionTypeRegistry });
- const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
- if (getResult !== null) {
- actionTypeModel = getResult;
- }
-});
-
-describe('actionTypeRegistry.get() works', () => {
- test('action type static data is as expected', () => {
- expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
- expect(actionTypeModel.iconClass).toEqual('logoWebhook');
- });
-});
-
-describe('webhook connector validation', () => {
- test('connector validation succeeds when connector config is valid', () => {
- const actionConnector = {
- secrets: {
- user: 'user',
- password: 'pass',
- },
- id: 'test',
- actionTypeId: '.webhook',
- name: 'webhook',
- isPreconfigured: false,
- config: {
- method: 'PUT',
- url: 'http:\\test',
- headers: { 'content-type': 'text' },
- },
- } as WebhookActionConnector;
-
- expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
- errors: {
- url: [],
- method: [],
- user: [],
- password: [],
- },
- });
- });
-
- test('connector validation fails when connector config is not valid', () => {
- const actionConnector = {
- secrets: {
- user: 'user',
- },
- id: 'test',
- actionTypeId: '.webhook',
- name: 'webhook',
- config: {
- method: 'PUT',
- },
- } as WebhookActionConnector;
-
- expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
- errors: {
- url: ['URL is required.'],
- method: [],
- user: [],
- password: ['Password is required.'],
- },
- });
- });
-});
-
-describe('webhook action params validation', () => {
- test('action params validation succeeds when action params is valid', () => {
- const actionParams = {
- body: 'message {test}',
- };
-
- expect(actionTypeModel.validateParams(actionParams)).toEqual({
- errors: { body: [] },
- });
- });
-});
-
-describe('WebhookActionConnectorFields renders', () => {
- test('all connector fields is rendered', () => {
- expect(actionTypeModel.actionConnectorFields).not.toBeNull();
- if (!actionTypeModel.actionConnectorFields) {
- return;
- }
- const ConnectorFields = actionTypeModel.actionConnectorFields;
- const actionConnector = {
- secrets: {
- user: 'user',
- password: 'pass',
- },
- id: 'test',
- actionTypeId: '.webhook',
- isPreconfigured: false,
- name: 'webhook',
- config: {
- method: 'PUT',
- url: 'http:\\test',
- headers: { 'content-type': 'text' },
- },
- } as WebhookActionConnector;
- const wrapper = mountWithIntl(
- {}}
- editActionSecrets={() => {}}
- />
- );
- expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy();
- wrapper
- .find('[data-test-subj="webhookViewHeadersSwitch"]')
- .first()
- .simulate('click');
- expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy();
- expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy();
- });
-});
-
-describe('WebhookParamsFields renders', () => {
- test('all params fields is rendered', () => {
- expect(actionTypeModel.actionParamsFields).not.toBeNull();
- if (!actionTypeModel.actionParamsFields) {
- return;
- }
- const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent<
- ActionParamsProps
- >;
- const actionParams = {
- body: 'test message',
- };
- const wrapper = mountWithIntl(
- {}}
- index={0}
- />
- );
- expect(wrapper.find('[data-test-subj="webhookBodyEditor"]').length > 0).toBeTruthy();
- expect(
- wrapper
- .find('[data-test-subj="webhookBodyEditor"]')
- .first()
- .prop('value')
- ).toStrictEqual('test message');
- expect(wrapper.find('[data-test-subj="bodyAddVariableButton"]').length > 0).toBeTruthy();
- });
-
- test('params validation fails when body is not valid', () => {
- const actionParams = {
- body: '',
- };
-
- expect(actionTypeModel.validateParams(actionParams)).toEqual({
- errors: {
- body: ['Body is required.'],
- },
- });
- });
-});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/index.ts
new file mode 100644
index 0000000000000..c43cab26b072e
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { getActionType as getWebhookActionType } from './webhook';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx
new file mode 100644
index 0000000000000..3413465d70d93
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx
@@ -0,0 +1,104 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { TypeRegistry } from '../../../type_registry';
+import { registerBuiltInActionTypes } from '.././index';
+import { ActionTypeModel } from '../../../../types';
+import { WebhookActionConnector } from '../types';
+
+const ACTION_TYPE_ID = '.webhook';
+let actionTypeModel: ActionTypeModel;
+
+beforeAll(() => {
+ const actionTypeRegistry = new TypeRegistry();
+ registerBuiltInActionTypes({ actionTypeRegistry });
+ const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
+ if (getResult !== null) {
+ actionTypeModel = getResult;
+ }
+});
+
+describe('actionTypeRegistry.get() works', () => {
+ test('action type static data is as expected', () => {
+ expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
+ expect(actionTypeModel.iconClass).toEqual('logoWebhook');
+ });
+});
+
+describe('webhook connector validation', () => {
+ test('connector validation succeeds when connector config is valid', () => {
+ const actionConnector = {
+ secrets: {
+ user: 'user',
+ password: 'pass',
+ },
+ id: 'test',
+ actionTypeId: '.webhook',
+ name: 'webhook',
+ isPreconfigured: false,
+ config: {
+ method: 'PUT',
+ url: 'http:\\test',
+ headers: { 'content-type': 'text' },
+ },
+ } as WebhookActionConnector;
+
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {
+ url: [],
+ method: [],
+ user: [],
+ password: [],
+ },
+ });
+ });
+
+ test('connector validation fails when connector config is not valid', () => {
+ const actionConnector = {
+ secrets: {
+ user: 'user',
+ },
+ id: 'test',
+ actionTypeId: '.webhook',
+ name: 'webhook',
+ config: {
+ method: 'PUT',
+ },
+ } as WebhookActionConnector;
+
+ expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
+ errors: {
+ url: ['URL is required.'],
+ method: [],
+ user: [],
+ password: ['Password is required.'],
+ },
+ });
+ });
+});
+
+describe('webhook action params validation', () => {
+ test('action params validation succeeds when action params is valid', () => {
+ const actionParams = {
+ body: 'message {test}',
+ };
+
+ expect(actionTypeModel.validateParams(actionParams)).toEqual({
+ errors: { body: [] },
+ });
+ });
+
+ test('params validation fails when body is not valid', () => {
+ const actionParams = {
+ body: '',
+ };
+
+ expect(actionTypeModel.validateParams(actionParams)).toEqual({
+ errors: {
+ body: ['Body is required.'],
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx
new file mode 100644
index 0000000000000..9f33e4491233a
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx
@@ -0,0 +1,99 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { lazy } from 'react';
+import { i18n } from '@kbn/i18n';
+import { ActionTypeModel, ValidationResult } from '../../../../types';
+import { WebhookActionParams, WebhookActionConnector } from '../types';
+
+export function getActionType(): ActionTypeModel {
+ return {
+ id: '.webhook',
+ iconClass: 'logoWebhook',
+ selectMessage: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText',
+ {
+ defaultMessage: 'Send a request to a web service.',
+ }
+ ),
+ actionTypeTitle: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle',
+ {
+ defaultMessage: 'Webhook data',
+ }
+ ),
+ validateConnector: (action: WebhookActionConnector): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ url: new Array(),
+ method: new Array(),
+ user: new Array(),
+ password: new Array(),
+ };
+ validationResult.errors = errors;
+ if (!action.config.url) {
+ errors.url.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText',
+ {
+ defaultMessage: 'URL is required.',
+ }
+ )
+ );
+ }
+ if (!action.config.method) {
+ errors.method.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText',
+ {
+ defaultMessage: 'Method is required.',
+ }
+ )
+ );
+ }
+ if (!action.secrets.user && action.secrets.password) {
+ errors.user.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHostText',
+ {
+ defaultMessage: 'Username is required.',
+ }
+ )
+ );
+ }
+ if (!action.secrets.password && action.secrets.user) {
+ errors.password.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText',
+ {
+ defaultMessage: 'Password is required.',
+ }
+ )
+ );
+ }
+ return validationResult;
+ },
+ validateParams: (actionParams: WebhookActionParams): ValidationResult => {
+ const validationResult = { errors: {} };
+ const errors = {
+ body: new Array(),
+ };
+ validationResult.errors = errors;
+ if (!actionParams.body?.length) {
+ errors.body.push(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText',
+ {
+ defaultMessage: 'Body is required.',
+ }
+ )
+ );
+ }
+ return validationResult;
+ },
+ actionConnectorFields: lazy(() => import('./webhook_connectors')),
+ actionParamsFields: lazy(() => import('./webhook_params')),
+ };
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx
new file mode 100644
index 0000000000000..842ec51785355
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { WebhookActionConnector } from '../types';
+import WebhookActionConnectorFields from './webhook_connectors';
+import { DocLinksStart } from 'kibana/public';
+
+describe('WebhookActionConnectorFields renders', () => {
+ test('all connector fields is rendered', () => {
+ const actionConnector = {
+ secrets: {
+ user: 'user',
+ password: 'pass',
+ },
+ id: 'test',
+ actionTypeId: '.webhook',
+ isPreconfigured: false,
+ name: 'webhook',
+ config: {
+ method: 'PUT',
+ url: 'http:\\test',
+ headers: { 'content-type': 'text' },
+ },
+ } as WebhookActionConnector;
+ const wrapper = mountWithIntl(
+ {}}
+ editActionSecrets={() => {}}
+ docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
+ />
+ );
+ expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy();
+ wrapper
+ .find('[data-test-subj="webhookViewHeadersSwitch"]')
+ .first()
+ .simulate('click');
+ expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx
similarity index 71%
rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx
rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx
index daa5a6caeabe9..e163463602d9f 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx
@@ -19,112 +19,15 @@ import {
EuiDescriptionListDescription,
EuiDescriptionListTitle,
EuiTitle,
- EuiCodeEditor,
EuiSwitch,
EuiButtonEmpty,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import {
- ActionTypeModel,
- ActionConnectorFieldsProps,
- ValidationResult,
- ActionParamsProps,
-} from '../../../types';
-import { WebhookActionParams, WebhookActionConnector } from './types';
-import { AddMessageVariables } from '../add_message_variables';
+import { ActionConnectorFieldsProps } from '../../../../types';
+import { WebhookActionConnector } from '../types';
const HTTP_VERBS = ['post', 'put'];
-export function getActionType(): ActionTypeModel {
- return {
- id: '.webhook',
- iconClass: 'logoWebhook',
- selectMessage: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText',
- {
- defaultMessage: 'Send a request to a web service.',
- }
- ),
- actionTypeTitle: i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle',
- {
- defaultMessage: 'Webhook data',
- }
- ),
- validateConnector: (action: WebhookActionConnector): ValidationResult => {
- const validationResult = { errors: {} };
- const errors = {
- url: new Array(),
- method: new Array(),
- user: new Array(),
- password: new Array(),
- };
- validationResult.errors = errors;
- if (!action.config.url) {
- errors.url.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText',
- {
- defaultMessage: 'URL is required.',
- }
- )
- );
- }
- if (!action.config.method) {
- errors.method.push(
- i18n.translate(
- 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText',
- {
- defaultMessage: 'Method is required.',
- }
- )
- );
- }
- if (!action.secrets.user && action.secrets.password) {
- errors.user.push(
- i18n.translate(
- 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHostText',
- {
- defaultMessage: 'Username is required.',
- }
- )
- );
- }
- if (!action.secrets.password && action.secrets.user) {
- errors.password.push(
- i18n.translate(
- 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText',
- {
- defaultMessage: 'Password is required.',
- }
- )
- );
- }
- return validationResult;
- },
- validateParams: (actionParams: WebhookActionParams): ValidationResult => {
- const validationResult = { errors: {} };
- const errors = {
- body: new Array(),
- };
- validationResult.errors = errors;
- if (!actionParams.body?.length) {
- errors.body.push(
- i18n.translate(
- 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText',
- {
- defaultMessage: 'Body is required.',
- }
- )
- );
- }
- return validationResult;
- },
- actionConnectorFields: WebhookActionConnectorFields,
- actionParamsFields: WebhookParamsFields,
- };
-}
-
const WebhookActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => {
@@ -457,56 +360,5 @@ const WebhookActionConnectorFields: React.FunctionComponent> = ({
- actionParams,
- editAction,
- index,
- messageVariables,
- errors,
-}) => {
- const { body } = actionParams;
- const onSelectMessageVariable = (paramsProperty: string, variable: string) => {
- editAction(paramsProperty, (body ?? '').concat(` {{${variable}}}`), index);
- };
- return (
-
- 0 && body !== undefined}
- fullWidth
- error={errors.body}
- labelAppend={
- onSelectMessageVariable('body', variable)}
- paramsProperty="body"
- />
- }
- >
- {
- editAction('body', json, index);
- }}
- />
-
-
- );
-};
+// eslint-disable-next-line import/no-default-export
+export { WebhookActionConnectorFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx
new file mode 100644
index 0000000000000..5ca27a53083f9
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import WebhookParamsFields from './webhook_params';
+
+describe('WebhookParamsFields renders', () => {
+ test('all params fields is rendered', () => {
+ const actionParams = {
+ body: 'test message',
+ };
+ const wrapper = mountWithIntl(
+ {}}
+ index={0}
+ />
+ );
+ expect(wrapper.find('[data-test-subj="webhookBodyEditor"]').length > 0).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="webhookBodyEditor"]')
+ .first()
+ .prop('value')
+ ).toStrictEqual('test message');
+ expect(wrapper.find('[data-test-subj="bodyAddVariableButton"]').length > 0).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx
new file mode 100644
index 0000000000000..9e802b96e16be
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { Fragment } from 'react';
+import { EuiFormRow, EuiCodeEditor } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { ActionParamsProps } from '../../../../types';
+import { WebhookActionParams } from '../types';
+import { AddMessageVariables } from '../../add_message_variables';
+
+const WebhookParamsFields: React.FunctionComponent> = ({
+ actionParams,
+ editAction,
+ index,
+ messageVariables,
+ errors,
+}) => {
+ const { body } = actionParams;
+ const onSelectMessageVariable = (paramsProperty: string, variable: string) => {
+ editAction(paramsProperty, (body ?? '').concat(` {{${variable}}}`), index);
+ };
+ return (
+
+ 0 && body !== undefined}
+ fullWidth
+ error={errors.body}
+ labelAppend={
+ onSelectMessageVariable('body', variable)}
+ paramsProperty="body"
+ />
+ }
+ >
+ {
+ editAction('body', json, index);
+ }}
+ />
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export { WebhookParamsFields as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx
index 4d0a9980f2231..b5f3b63c58a93 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx
@@ -167,3 +167,6 @@ export const TriggersActionsUIHome: React.FunctionComponent
);
};
+
+// eslint-disable-next-line import/no-default-export
+export { TriggersActionsUIHome as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx
index 6bb8a8f4e4c10..06ddce39567a4 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment } from 'react';
+import React, { Fragment, Suspense } from 'react';
import {
EuiForm,
EuiCallOut,
@@ -12,6 +12,9 @@ import {
EuiSpacer,
EuiFieldText,
EuiFormRow,
+ EuiLoadingSpinner,
+ EuiFlexGroup,
+ EuiFlexItem,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -151,14 +154,24 @@ export const ActionConnectorForm = ({
{FieldsComponent !== null ? (
-
+
+
+
+
+
+ }
+ >
+
+
) : null}
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
index 6935dda358d9c..ae179f56f0c83 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, useState, useEffect } from 'react';
+import React, { Fragment, Suspense, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -27,6 +27,7 @@ import {
EuiCallOut,
EuiHorizontalRule,
EuiText,
+ EuiLoadingSpinner,
} from '@elastic/eui';
import { HttpSetup, ToastsApi, ApplicationStart, DocLinksStart } from 'kibana/public';
import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
@@ -282,14 +283,24 @@ export const ActionForm = ({
{ParamsFieldsComponent ? (
-
+
+
+
+
+
+ }
+ >
+
+
) : null}
) : (
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx
index 3440bb28b2468..8511ab468ca80 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx
@@ -127,26 +127,27 @@ export const AlertDetails: React.FunctionComponent = ({
defaultMessage="Edit"
/>
-
-
-
+ {editFlyoutVisible && (
+
+ setEditFlyoutVisibility(false)}
+ />
+
+ )}
) : null}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx
index 9198607df7863..0caa880c4df00 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx
@@ -118,6 +118,6 @@ export async function getAlertData(
}
}
-export const AlertDetailsRouteWithApi = withActionOperations(
- withBulkAlertOperations(AlertDetailsRoute)
-);
+const AlertDetailsRouteWithApi = withActionOperations(withBulkAlertOperations(AlertDetailsRoute));
+// eslint-disable-next-line import/no-default-export
+export { AlertDetailsRouteWithApi as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx
index 722db146a54ce..4d8801d8b7484 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx
@@ -131,11 +131,7 @@ describe('alert_edit', () => {
capabilities: deps!.capabilities,
}}
>
- {}}
- initialAlert={alert}
- />
+ {}} initialAlert={alert} />
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx
index 00bc9874face1..747464d2212f4 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx
@@ -31,15 +31,10 @@ import { PLUGIN } from '../../constants/plugin';
interface AlertEditProps {
initialAlert: Alert;
- editFlyoutVisible: boolean;
- setEditFlyoutVisibility: React.Dispatch>;
+ onClose(): void;
}
-export const AlertEdit = ({
- initialAlert,
- editFlyoutVisible,
- setEditFlyoutVisibility,
-}: AlertEditProps) => {
+export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => {
const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert });
const [isSaving, setIsSaving] = useState(false);
const [hasActionsDisabled, setHasActionsDisabled] = useState(false);
@@ -57,14 +52,10 @@ export const AlertEdit = ({
} = useAlertsContext();
const closeFlyout = useCallback(() => {
- setEditFlyoutVisibility(false);
+ onClose();
setAlert('alert', initialAlert);
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [setEditFlyoutVisibility]);
-
- if (!editFlyoutVisible) {
- return null;
- }
+ }, [onClose]);
const alertType = alertTypeRegistry.get(alert.alertTypeId);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts
index 93e61cf5b4f43..62173a6196b98 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts
@@ -23,7 +23,11 @@ const getTestAlertType = (id?: string, name?: string, iconClass?: string) => {
};
};
-const getTestActionType = (id?: string, iconClass?: string, selectedMessage?: string) => {
+const getTestActionType = (
+ id?: string,
+ iconClass?: string,
+ selectedMessage?: string
+): ActionTypeModel => {
return {
id: id || 'my-action-type',
iconClass: iconClass || 'test',
diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts
index 6f33bcb8b226d..cc511434267cc 100644
--- a/x-pack/plugins/triggers_actions_ui/public/types.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/types.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpSetup, DocLinksStart } from 'kibana/public';
+import { ComponentType } from 'react';
import { ActionGroup } from '../../alerting/common';
import { ActionType } from '../../actions/common';
import { TypeRegistry } from './application/type_registry';
@@ -19,14 +20,16 @@ export { ActionType };
export type ActionTypeIndex = Record;
export type AlertTypeIndex = Record;
-export type ActionTypeRegistryContract = PublicMethodsOf>;
+export type ActionTypeRegistryContract = PublicMethodsOf<
+ TypeRegistry>
+>;
export type AlertTypeRegistryContract = PublicMethodsOf>;
export interface ActionConnectorFieldsProps {
action: TActionConnector;
editActionConfig: (property: string, value: any) => void;
editActionSecrets: (property: string, value: any) => void;
- errors: { [key: string]: string[] };
+ errors: IErrorObject;
docLinks: DocLinksStart;
http?: HttpSetup;
}
@@ -35,7 +38,7 @@ export interface ActionParamsProps {
actionParams: TParams;
index: number;
editAction: (property: string, value: any, index: number) => void;
- errors: { [key: string]: string[] };
+ errors: IErrorObject;
messageVariables?: string[];
defaultMessage?: string;
}
@@ -45,15 +48,19 @@ export interface Pagination {
size: number;
}
-export interface ActionTypeModel {
+export interface ActionTypeModel {
id: string;
iconClass: string;
selectMessage: string;
actionTypeTitle?: string;
validateConnector: (connector: any) => ValidationResult;
validateParams: (actionParams: any) => ValidationResult;
- actionConnectorFields: React.FunctionComponent | null;
- actionParamsFields: any;
+ actionConnectorFields: React.LazyExoticComponent<
+ ComponentType>
+ > | null;
+ actionParamsFields: React.LazyExoticComponent<
+ ComponentType>
+ > | null;
}
export interface ValidationResult {
diff --git a/x-pack/plugins/uptime/README.md b/x-pack/plugins/uptime/README.md
index 92162341ff426..10c1fc0edcd00 100644
--- a/x-pack/plugins/uptime/README.md
+++ b/x-pack/plugins/uptime/README.md
@@ -75,3 +75,19 @@ We can run these tests like described above, but with some special config.
`node scripts/functional_tests_server.js --config=test/functional_with_es_ssl/config.ts`
`node scripts/functional_test_runner.js --config=test/functional_with_es_ssl/config.ts`
+
+#### Running accessibility tests
+
+We maintain a suite of Accessibility tests (you may see them referred to elsewhere as `a11y` tests).
+
+These tests render each of our pages and ensure that the inputs and other elements contain the
+attributes necessary to ensure all users are able to make use of Kibana (for example, users relying
+on screen readers).
+
+The commands for running these tests are very similar to the other functional tests described above.
+
+From the `~/x-pack` directory:
+
+Start the server: `node scripts/functional_tests_server --config test/accessibility/config.ts`
+
+Run the uptime `a11y` tests: `node scripts/functional_test_runner.js --config test/accessibility/config.ts --grep=uptime`
diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap
index c9b17db5532f4..b4e5a34412212 100644
--- a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap
+++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap
@@ -36,6 +36,7 @@ Array [
class="euiToolTipAnchor"
>