From 6da7c00b5db79df491cea9fdb5a17f79f167a2ea Mon Sep 17 00:00:00 2001
From: Corey Robertson
Date: Mon, 6 Apr 2020 11:49:34 -0400
Subject: [PATCH 01/15] Remove the action_value_click action in canvas (#62215)
Co-authored-by: Elastic Machine
---
.../plugins/canvas/public/application.tsx | 34 ++++++++++++++++++-
x-pack/legacy/plugins/canvas/public/legacy.ts | 1 +
.../legacy/plugins/canvas/public/plugin.tsx | 4 ++-
3 files changed, 37 insertions(+), 2 deletions(-)
diff --git a/x-pack/legacy/plugins/canvas/public/application.tsx b/x-pack/legacy/plugins/canvas/public/application.tsx
index e26157aadebcb1c..79b3918fef99be8 100644
--- a/x-pack/legacy/plugins/canvas/public/application.tsx
+++ b/x-pack/legacy/plugins/canvas/public/application.tsx
@@ -26,9 +26,24 @@ import { getDocumentationLinks } from './lib/documentation_links';
import { HelpMenu } from './components/help_menu/help_menu';
import { createStore } from './store';
+import { VALUE_CLICK_TRIGGER, ActionByType } from '../../../../../src/plugins/ui_actions/public';
+/* eslint-disable */
+import { ACTION_VALUE_CLICK } from '../../../../../src/plugins/data/public/actions/value_click_action';
+/* eslint-enable */
+
import { CapabilitiesStrings } from '../i18n';
const { ReadOnlyBadge: strings } = CapabilitiesStrings;
+let restoreAction: ActionByType | undefined;
+const emptyAction = {
+ id: 'empty-action',
+ type: '',
+ getDisplayName: () => 'empty action',
+ getIconType: () => undefined,
+ isCompatible: async () => true,
+ execute: async () => undefined,
+} as ActionByType;
+
export const renderApp = (
coreStart: CoreStart,
plugins: CanvasStartDeps,
@@ -94,13 +109,30 @@ export const initializeCanvas = async (
},
});
+ // TODO: We need this to disable the filtering modal from popping up in lens embeds until
+ // they honor the disableTriggers parameter
+ const action = startPlugins.uiActions.getAction(ACTION_VALUE_CLICK);
+
+ if (action) {
+ restoreAction = action;
+
+ startPlugins.uiActions.detachAction(VALUE_CLICK_TRIGGER, action.id);
+ startPlugins.uiActions.attachAction(VALUE_CLICK_TRIGGER, emptyAction);
+ }
+
return canvasStore;
};
-export const teardownCanvas = (coreStart: CoreStart) => {
+export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDeps) => {
destroyRegistries();
resetInterpreter();
+ startPlugins.uiActions.detachAction(VALUE_CLICK_TRIGGER, emptyAction.id);
+ if (restoreAction) {
+ startPlugins.uiActions.attachAction(VALUE_CLICK_TRIGGER, restoreAction);
+ restoreAction = undefined;
+ }
+
coreStart.chrome.setBadge(undefined);
coreStart.chrome.setHelpExtension(undefined);
};
diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts
index 9bccc958f726310..a6caa1985325ef1 100644
--- a/x-pack/legacy/plugins/canvas/public/legacy.ts
+++ b/x-pack/legacy/plugins/canvas/public/legacy.ts
@@ -27,6 +27,7 @@ const shimSetupPlugins: CanvasSetupDeps = {
const shimStartPlugins: CanvasStartDeps = {
...npStart.plugins,
expressions: npStart.plugins.expressions,
+ uiActions: npStart.plugins.uiActions,
__LEGACY: {
// ToDo: Copy directly into canvas
absoluteToParsedUrl,
diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx
index f4a3aed28a0a459..d9e5e6b4b084bff 100644
--- a/x-pack/legacy/plugins/canvas/public/plugin.tsx
+++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx
@@ -10,6 +10,7 @@ import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public';
import { initLoadingIndicator } from './lib/loading_indicator';
import { featureCatalogueEntry } from './feature_catalogue_entry';
import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public';
+import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
// @ts-ignore untyped local
import { argTypeSpecs } from './expression_types/arg_types';
import { transitions } from './transitions';
@@ -31,6 +32,7 @@ export interface CanvasSetupDeps {
export interface CanvasStartDeps {
expressions: ExpressionsStart;
+ uiActions: UiActionsStart;
__LEGACY: {
absoluteToParsedUrl: (url: string, basePath: string) => any;
formatMsg: any;
@@ -70,7 +72,7 @@ export class CanvasPlugin
return () => {
unmount();
- teardownCanvas(coreStart);
+ teardownCanvas(coreStart, depsStart);
};
},
});
From 1e92dcf1f33ddb9af5ea794a4612a0c940edee49 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 6 Apr 2020 17:55:27 +0200
Subject: [PATCH 02/15] migrate saved object, reset validation counter and fix
typo attribute (#62442)
---
.../core_plugins/vis_type_timeseries/index.ts | 14 -
.../validation_telemetry/saved_object_type.ts | 45 ++
.../validation_telemetry_service.ts | 2 +
.../visualization_migrations.test.ts | 539 ++++++++++--------
.../saved_objects/visualization_migrations.ts | 33 ++
5 files changed, 390 insertions(+), 243 deletions(-)
create mode 100644 src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts
diff --git a/src/legacy/core_plugins/vis_type_timeseries/index.ts b/src/legacy/core_plugins/vis_type_timeseries/index.ts
index 3ad8ba3a31c17ea..596fd5b581a7193 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/index.ts
+++ b/src/legacy/core_plugins/vis_type_timeseries/index.ts
@@ -31,20 +31,6 @@ const metricsPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPlu
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
hacks: [resolve(__dirname, 'public/legacy')],
injectDefaultVars: server => ({}),
- mappings: {
- 'tsvb-validation-telemetry': {
- properties: {
- failedRequests: {
- type: 'long',
- },
- },
- },
- },
- savedObjectSchemas: {
- 'tsvb-validation-telemetry': {
- isNamespaceAgnostic: true,
- },
- },
},
config(Joi: any) {
return Joi.object({
diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts
new file mode 100644
index 000000000000000..77b49e824334fd8
--- /dev/null
+++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts
@@ -0,0 +1,45 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { flow } from 'lodash';
+import { SavedObjectMigrationFn, SavedObjectsType } from 'kibana/server';
+
+const resetCount: SavedObjectMigrationFn = doc => ({
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ failedRequests: 0,
+ },
+});
+
+export const tsvbTelemetrySavedObjectType: SavedObjectsType = {
+ name: 'tsvb-validation-telemetry',
+ hidden: false,
+ namespaceAgnostic: true,
+ mappings: {
+ properties: {
+ failedRequests: {
+ type: 'long',
+ },
+ },
+ },
+ migrations: {
+ '7.7.0': flow(resetCount),
+ },
+};
diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts
index e49664265b8bbb6..779d9441df2fd97 100644
--- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts
+++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts
@@ -19,6 +19,7 @@
import { APICaller, CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server';
import { UsageCollectionSetup } from '../../../usage_collection/server';
+import { tsvbTelemetrySavedObjectType } from './saved_object_type';
export interface ValidationTelemetryServiceSetup {
logFailedValidation: () => void;
@@ -36,6 +37,7 @@ export class ValidationTelemetryService implements Plugin {
this.kibanaIndex = config.kibana.index;
});
diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts
index c7f245e59551ff8..26f8278cd3d434d 100644
--- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts
+++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts
@@ -207,13 +207,13 @@ describe('migration visualization', () => {
},
});
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "visState": "{}",
- },
- "references": Array [],
-}
-`);
+ Object {
+ "attributes": Object {
+ "visState": "{}",
+ },
+ "references": Array [],
+ }
+ `);
});
it('skips errors when searchSourceJSON is null', () => {
@@ -231,25 +231,25 @@ Object {
const migratedDoc = migrate(doc);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": null,
- },
- "savedSearchRefName": "search_0",
- "visState": "{}",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "123",
- "name": "search_0",
- "type": "search",
- },
- ],
- "type": "visualization",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": null,
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+ }
+ `);
});
it('skips errors when searchSourceJSON is undefined', () => {
@@ -267,25 +267,25 @@ Object {
const migratedDoc = migrate(doc);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": undefined,
- },
- "savedSearchRefName": "search_0",
- "visState": "{}",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "123",
- "name": "search_0",
- "type": "search",
- },
- ],
- "type": "visualization",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": undefined,
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+ }
+ `);
});
it('skips error when searchSourceJSON is not a string', () => {
@@ -302,25 +302,25 @@ Object {
};
expect(migrate(doc)).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": 123,
- },
- "savedSearchRefName": "search_0",
- "visState": "{}",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "123",
- "name": "search_0",
- "type": "search",
- },
- ],
- "type": "visualization",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": 123,
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+ }
+ `);
});
it('skips error when searchSourceJSON is invalid json', () => {
@@ -337,25 +337,25 @@ Object {
};
expect(migrate(doc)).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": "{abc123}",
- },
- "savedSearchRefName": "search_0",
- "visState": "{}",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "123",
- "name": "search_0",
- "type": "search",
- },
- ],
- "type": "visualization",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{abc123}",
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+ }
+ `);
});
it('skips error when "index" and "filter" is missing from searchSourceJSON', () => {
@@ -373,25 +373,25 @@ Object {
const migratedDoc = migrate(doc);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": "{\\"bar\\":true}",
- },
- "savedSearchRefName": "search_0",
- "visState": "{}",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "123",
- "name": "search_0",
- "type": "search",
- },
- ],
- "type": "visualization",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true}",
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+ }
+ `);
});
it('extracts "index" attribute from doc', () => {
@@ -409,30 +409,30 @@ Object {
const migratedDoc = migrate(doc);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
- },
- "savedSearchRefName": "search_0",
- "visState": "{}",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "pattern*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
- "type": "index-pattern",
- },
- Object {
- "id": "123",
- "name": "search_0",
- "type": "search",
- },
- ],
- "type": "visualization",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "pattern*",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+ }
+ `);
});
it('extracts index patterns from the filter', () => {
@@ -457,30 +457,30 @@ Object {
const migratedDoc = migrate(doc);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}",
- },
- "savedSearchRefName": "search_0",
- "visState": "{}",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "my-index",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
- "type": "index-pattern",
- },
- Object {
- "id": "123",
- "name": "search_0",
- "type": "search",
- },
- ],
- "type": "visualization",
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}",
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "my-index",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+ }
+ `);
});
it('extracts index patterns from controls', () => {
@@ -508,22 +508,22 @@ Object {
const migratedDoc = migrate(doc);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "foo": true,
- "visState": "{\\"bar\\":false,\\"params\\":{\\"controls\\":[{\\"bar\\":true,\\"indexPatternRefName\\":\\"control_0_index_pattern\\"},{\\"foo\\":true}]}}",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "pattern*",
- "name": "control_0_index_pattern",
- "type": "index-pattern",
- },
- ],
- "type": "visualization",
-}
-`);
+ Object {
+ "attributes": Object {
+ "foo": true,
+ "visState": "{\\"bar\\":false,\\"params\\":{\\"controls\\":[{\\"bar\\":true,\\"indexPatternRefName\\":\\"control_0_index_pattern\\"},{\\"foo\\":true}]}}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "pattern*",
+ "name": "control_0_index_pattern",
+ "type": "index-pattern",
+ },
+ ],
+ "type": "visualization",
+ }
+ `);
});
it('skips extracting savedSearchId when missing', () => {
@@ -539,17 +539,17 @@ Object {
const migratedDoc = migrate(doc);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": "{}",
- },
- "visState": "{}",
- },
- "id": "1",
- "references": Array [],
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{}",
+ },
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [],
+ }
+ `);
});
it('extract savedSearchId from doc', () => {
@@ -566,24 +566,24 @@ Object {
const migratedDoc = migrate(doc);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": "{}",
- },
- "savedSearchRefName": "search_0",
- "visState": "{}",
- },
- "id": "1",
- "references": Array [
- Object {
- "id": "123",
- "name": "search_0",
- "type": "search",
- },
- ],
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{}",
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ }
+ `);
});
it('delete savedSearchId when empty string in doc', () => {
@@ -600,17 +600,17 @@ Object {
const migratedDoc = migrate(doc);
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "kibanaSavedObjectMeta": Object {
- "searchSourceJSON": "{}",
- },
- "visState": "{}",
- },
- "id": "1",
- "references": Array [],
-}
-`);
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{}",
+ },
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [],
+ }
+ `);
});
it('should return a new object if vis is table and has multiple split aggs', () => {
@@ -930,12 +930,12 @@ Object {
},
});
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"horizontal\\"}}}",
- },
-}
-`);
+ Object {
+ "attributes": Object {
+ "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"horizontal\\"}}}",
+ },
+ }
+ `);
});
it('migrates type = gauge verticalSplit: false to alignment: horizontal', () => {
@@ -946,12 +946,12 @@ Object {
});
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"vertical\\"}}}",
- },
-}
-`);
+ Object {
+ "attributes": Object {
+ "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"vertical\\"}}}",
+ },
+ }
+ `);
});
it('doesnt migrate type = gauge containing invalid visState object, adds message to log', () => {
@@ -962,18 +962,18 @@ Object {
});
expect(migratedDoc).toMatchInlineSnapshot(`
-Object {
- "attributes": Object {
- "visState": "{\\"type\\":\\"gauge\\"}",
- },
-}
-`);
+ Object {
+ "attributes": Object {
+ "visState": "{\\"type\\":\\"gauge\\"}",
+ },
+ }
+ `);
expect(logMsgArr).toMatchInlineSnapshot(`
-Array [
- "Exception @ migrateGaugeVerticalSplitToAlignment! TypeError: Cannot read property 'gauge' of undefined",
- "Exception @ migrateGaugeVerticalSplitToAlignment! Payload: {\\"type\\":\\"gauge\\"}",
-]
-`);
+ Array [
+ "Exception @ migrateGaugeVerticalSplitToAlignment! TypeError: Cannot read property 'gauge' of undefined",
+ "Exception @ migrateGaugeVerticalSplitToAlignment! Payload: {\\"type\\":\\"gauge\\"}",
+ ]
+ `);
});
describe('filters agg query migration', () => {
@@ -1379,4 +1379,85 @@ Array [
expect(timeSeriesParams.series[0].split_filters[0].filter.language).toEqual('lucene');
});
});
+
+ describe('7.7.0 tsvb opperator typo migration', () => {
+ const migrate = (doc: any) =>
+ visualizationSavedObjectTypeMigrations['7.7.0'](
+ doc as Parameters[0],
+ savedObjectMigrationContext
+ );
+ const generateDoc = (params: any) => ({
+ attributes: {
+ title: 'My Vis',
+ description: 'This is my super cool vis.',
+ visState: JSON.stringify({ params }),
+ uiStateJSON: '{}',
+ version: 1,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: '{}',
+ },
+ },
+ });
+
+ it('should remove the misspelled opperator key if it exists', () => {
+ const params = {
+ type: 'timeseries',
+ filter: {
+ query: 'bytes:>1000',
+ language: 'lucene',
+ },
+ series: [],
+ gauge_color_rules: [
+ {
+ value: 0,
+ id: '020e3d50-75a6-11ea-8f61-71579ff7f64d',
+ gauge: 'rgba(69,39,217,1)',
+ opperator: 'lt',
+ },
+ ],
+ };
+ const timeSeriesDoc = generateDoc(params);
+ const migratedtimeSeriesDoc = migrate(timeSeriesDoc);
+ const migratedParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params;
+
+ expect(migratedParams.gauge_color_rules[0]).toMatchInlineSnapshot(`
+ Object {
+ "gauge": "rgba(69,39,217,1)",
+ "id": "020e3d50-75a6-11ea-8f61-71579ff7f64d",
+ "opperator": "lt",
+ "value": 0,
+ }
+ `);
+ });
+
+ it('should not change color rules with the correct spelling', () => {
+ const params = {
+ type: 'timeseries',
+ filter: {
+ query: 'bytes:>1000',
+ language: 'lucene',
+ },
+ series: [],
+ gauge_color_rules: [
+ {
+ value: 0,
+ id: '020e3d50-75a6-11ea-8f61-71579ff7f64d',
+ gauge: 'rgba(69,39,217,1)',
+ opperator: 'lt',
+ },
+ {
+ value: 0,
+ id: '020e3d50-75a6-11ea-8f61-71579ff7f64d',
+ gauge: 'rgba(69,39,217,1)',
+ operator: 'lt',
+ },
+ ],
+ };
+ const timeSeriesDoc = generateDoc(params);
+ const migratedtimeSeriesDoc = migrate(timeSeriesDoc);
+ const migratedParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params;
+
+ expect(migratedParams.gauge_color_rules[1]).toEqual(params.gauge_color_rules[1]);
+ });
+ });
});
diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts
index db87006dde3eec8..80783e41863eafd 100644
--- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts
+++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts
@@ -99,6 +99,38 @@ const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => {
return doc;
};
+// [TSVB] Remove stale opperator key
+const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => {
+ const visStateJSON = get(doc, 'attributes.visState');
+ let visState;
+
+ if (visStateJSON) {
+ try {
+ visState = JSON.parse(visStateJSON);
+ } catch (e) {
+ // Let it go, the data is invalid and we'll leave it as is
+ }
+ if (visState && visState.type === 'metrics') {
+ const gaugeColorRules: any[] = get(visState, 'params.gauge_color_rules') || [];
+
+ gaugeColorRules.forEach(colorRule => {
+ if (colorRule.opperator) {
+ delete colorRule.opperator;
+ }
+ });
+
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ visState: JSON.stringify(visState),
+ },
+ };
+ }
+ }
+ return doc;
+};
+
// Migrate date histogram aggregation (remove customInterval)
const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => {
const visStateJSON = get(doc, 'attributes.visState');
@@ -606,4 +638,5 @@ export const visualizationSavedObjectTypeMigrations = {
),
'7.3.1': flow(migrateFiltersAggQueryStringQueries),
'7.4.2': flow(transformSplitFiltersStringToQueryObject),
+ '7.7.0': flow(migrateOperatorKeyTypo),
};
From 70ae8aa6f1418beff1aeb853dce0415814d0a890 Mon Sep 17 00:00:00 2001
From: Michael Marcialis
Date: Mon, 6 Apr 2020 12:10:20 -0400
Subject: [PATCH 03/15] add ownFocus prop to help menu (#62492)
---
src/core/public/chrome/ui/header/header_help_menu.tsx | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx
index 80a45b4c61f077e..1023a561a0fe331 100644
--- a/src/core/public/chrome/ui/header/header_help_menu.tsx
+++ b/src/core/public/chrome/ui/header/header_help_menu.tsx
@@ -314,13 +314,14 @@ class HeaderHelpMenuUI extends Component {
return (
// @ts-ignore repositionOnScroll doesn't exist in EuiPopover
}>
+ {msgTooltip}}
+ data-test-subj="configure-case-tooltip"
+ >
{configureCaseButton}
) : (
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx
new file mode 100644
index 000000000000000..209dce9aedffc57
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 { mount, ReactWrapper } from 'enzyme';
+
+import { ClosureOptions, ClosureOptionsProps } from './closure_options';
+import { TestProviders } from '../../../../mock';
+import { ClosureOptionsRadio } from './closure_options_radio';
+
+describe('ClosureOptions', () => {
+ let wrapper: ReactWrapper;
+ const onChangeClosureType = jest.fn();
+ const props: ClosureOptionsProps = {
+ disabled: false,
+ closureTypeSelected: 'close-by-user',
+ onChangeClosureType,
+ };
+
+ beforeAll(() => {
+ wrapper = mount(, { wrappingComponent: TestProviders });
+ });
+
+ test('it shows the closure options form group', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-closure-options-form-group"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it shows the closure options form row', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-closure-options-form-row"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it shows closure options', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-closure-options-radio"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it pass the correct props to child', () => {
+ const closureOptionsRadioComponent = wrapper.find(ClosureOptionsRadio);
+ expect(closureOptionsRadioComponent.props().disabled).toEqual(false);
+ expect(closureOptionsRadioComponent.props().closureTypeSelected).toEqual('close-by-user');
+ expect(closureOptionsRadioComponent.props().onChangeClosureType).toEqual(onChangeClosureType);
+ });
+
+ test('the closure type is changed successfully', () => {
+ wrapper.find('input[id="close-by-pushing"]').simulate('change');
+
+ expect(onChangeClosureType).toHaveBeenCalled();
+ expect(onChangeClosureType).toHaveBeenCalledWith('close-by-pushing');
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx
index 9879b9149059ae9..6fa97818dd0ce38 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx
@@ -11,7 +11,7 @@ import { ClosureType } from '../../../../containers/case/configure/types';
import { ClosureOptionsRadio } from './closure_options_radio';
import * as i18n from './translations';
-interface ClosureOptionsProps {
+export interface ClosureOptionsProps {
closureTypeSelected: ClosureType;
disabled: boolean;
onChangeClosureType: (newClosureType: ClosureType) => void;
@@ -27,12 +27,18 @@ const ClosureOptionsComponent: React.FC = ({
fullWidth
title={{i18n.CASE_CLOSURE_OPTIONS_TITLE}
}
description={i18n.CASE_CLOSURE_OPTIONS_DESC}
+ data-test-subj="case-closure-options-form-group"
>
-
+
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx
new file mode 100644
index 000000000000000..f2ef2c2d55c2880
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 { ReactWrapper, mount } from 'enzyme';
+
+import { ClosureOptionsRadio, ClosureOptionsRadioComponentProps } from './closure_options_radio';
+import { TestProviders } from '../../../../mock';
+
+describe('ClosureOptionsRadio', () => {
+ let wrapper: ReactWrapper;
+ const onChangeClosureType = jest.fn();
+ const props: ClosureOptionsRadioComponentProps = {
+ disabled: false,
+ closureTypeSelected: 'close-by-user',
+ onChangeClosureType,
+ };
+
+ beforeAll(() => {
+ wrapper = mount(, { wrappingComponent: TestProviders });
+ });
+
+ test('it renders', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="closure-options-radio-group"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it shows the correct number of radio buttons', () => {
+ expect(wrapper.find('input[name="closure_options"]')).toHaveLength(2);
+ });
+
+ test('it renders close by user radio button', () => {
+ expect(wrapper.find('input[id="close-by-user"]').exists()).toBeTruthy();
+ });
+
+ test('it renders close by pushing radio button', () => {
+ expect(wrapper.find('input[id="close-by-pushing"]').exists()).toBeTruthy();
+ });
+
+ test('it disables the close by user radio button', () => {
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ expect(newWrapper.find('input[id="close-by-user"]').prop('disabled')).toEqual(true);
+ });
+
+ test('it disables correctly the close by pushing radio button', () => {
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ expect(newWrapper.find('input[id="close-by-pushing"]').prop('disabled')).toEqual(true);
+ });
+
+ test('it selects the correct radio button', () => {
+ const newWrapper = mount(
+ ,
+ {
+ wrappingComponent: TestProviders,
+ }
+ );
+ expect(newWrapper.find('input[id="close-by-pushing"]').prop('checked')).toEqual(true);
+ });
+
+ test('it calls the onChangeClosureType function', () => {
+ wrapper.find('input[id="close-by-pushing"]').simulate('change');
+ wrapper.update();
+ expect(onChangeClosureType).toHaveBeenCalled();
+ expect(onChangeClosureType).toHaveBeenCalledWith('close-by-pushing');
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx
index f32f867b2471d0f..d2cdb7ecda7ba41 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx
@@ -26,7 +26,7 @@ const radios: ClosureRadios[] = [
},
];
-interface ClosureOptionsRadioComponentProps {
+export interface ClosureOptionsRadioComponentProps {
closureTypeSelected: ClosureType;
disabled: boolean;
onChangeClosureType: (newClosureType: ClosureType) => void;
@@ -51,6 +51,7 @@ const ClosureOptionsRadioComponent: React.FC
idSelected={closureTypeSelected}
onChange={onChangeLocal}
name="closure_options"
+ data-test-subj="closure-options-radio-group"
/>
);
};
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx
new file mode 100644
index 000000000000000..5fb52c374b482b4
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 { mount, ReactWrapper } from 'enzyme';
+
+import { Connectors, Props } from './connectors';
+import { TestProviders } from '../../../../mock';
+import { ConnectorsDropdown } from './connectors_dropdown';
+import { connectors } from './__mock__';
+
+describe('Connectors', () => {
+ let wrapper: ReactWrapper;
+ const onChangeConnector = jest.fn();
+ const handleShowAddFlyout = jest.fn();
+ const props: Props = {
+ disabled: false,
+ connectors,
+ selectedConnector: 'none',
+ isLoading: false,
+ onChangeConnector,
+ handleShowAddFlyout,
+ };
+
+ beforeAll(() => {
+ wrapper = mount(, { wrappingComponent: TestProviders });
+ });
+
+ test('it shows the connectors from group', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-connectors-form-group"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it shows the connectors form row', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-connectors-form-row"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it shows the connectors dropdown', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-connectors-dropdown"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it pass the correct props to child', () => {
+ const connectorsDropdownProps = wrapper.find(ConnectorsDropdown).props();
+ expect(connectorsDropdownProps).toMatchObject({
+ disabled: false,
+ isLoading: false,
+ connectors,
+ selectedConnector: 'none',
+ onChange: props.onChangeConnector,
+ });
+ });
+
+ test('the connector is changed successfully', () => {
+ wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
+ wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click');
+
+ expect(onChangeConnector).toHaveBeenCalled();
+ expect(onChangeConnector).toHaveBeenCalledWith('456');
+ });
+
+ test('the connector is changed successfully to none', () => {
+ onChangeConnector.mockClear();
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ newWrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
+ newWrapper.find('button[data-test-subj="dropdown-connector-no-connector"]').simulate('click');
+
+ expect(onChangeConnector).toHaveBeenCalled();
+ expect(onChangeConnector).toHaveBeenCalledWith('none');
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx
index 8fb1cfb1aa6cc48..de6d5f76cfad070 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx
@@ -28,7 +28,7 @@ const EuiFormRowExtended = styled(EuiFormRow)`
}
`;
-interface Props {
+export interface Props {
connectors: Connector[];
disabled: boolean;
isLoading: boolean;
@@ -48,7 +48,11 @@ const ConnectorsComponent: React.FC = ({
{i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL}
-
+
{i18n.ADD_NEW_CONNECTOR}
@@ -61,14 +65,20 @@ const ConnectorsComponent: React.FC = ({
fullWidth
title={{i18n.INCIDENT_MANAGEMENT_SYSTEM_TITLE}
}
description={i18n.INCIDENT_MANAGEMENT_SYSTEM_DESC}
+ data-test-subj="case-connectors-form-group"
>
-
+
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx
new file mode 100644
index 000000000000000..044108962efc7bd
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx
@@ -0,0 +1,86 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { EuiSuperSelect } from '@elastic/eui';
+
+import { ConnectorsDropdown, Props } from './connectors_dropdown';
+import { TestProviders } from '../../../../mock';
+import { connectors } from './__mock__';
+
+describe('ConnectorsDropdown', () => {
+ let wrapper: ReactWrapper;
+ const props: Props = {
+ disabled: false,
+ connectors,
+ isLoading: false,
+ onChange: jest.fn(),
+ selectedConnector: 'none',
+ };
+
+ beforeAll(() => {
+ wrapper = mount(, { wrappingComponent: TestProviders });
+ });
+
+ test('it renders', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="dropdown-connectors"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it formats the connectors correctly', () => {
+ const selectProps = wrapper.find(EuiSuperSelect).props();
+
+ expect(selectProps.options).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: 'none',
+ 'data-test-subj': 'dropdown-connector-no-connector',
+ }),
+ expect.objectContaining({ value: '123', 'data-test-subj': 'dropdown-connector-123' }),
+ expect.objectContaining({ value: '456', 'data-test-subj': 'dropdown-connector-456' }),
+ ])
+ );
+ });
+
+ test('it disables the dropdown', () => {
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="dropdown-connectors"]')
+ .first()
+ .prop('disabled')
+ ).toEqual(true);
+ });
+
+ test('it loading correctly', () => {
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="dropdown-connectors"]')
+ .first()
+ .prop('isLoading')
+ ).toEqual(true);
+ });
+
+ test('it selects the correct connector', () => {
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ expect(newWrapper.find('button span').text()).toEqual('My Connector');
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx
index a0a0ad6cd3e7ff0..15066e73eee829f 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx
@@ -12,7 +12,7 @@ import { Connector } from '../../../../containers/case/configure/types';
import { connectors as connectorsDefinition } from '../../../../lib/connectors/config';
import * as i18n from './translations';
-interface Props {
+export interface Props {
connectors: Connector[];
disabled: boolean;
isLoading: boolean;
@@ -34,7 +34,7 @@ const noConnectorOption = {
{i18n.NO_CONNECTOR}
>
),
- 'data-test-subj': 'no-connector',
+ 'data-test-subj': 'dropdown-connector-no-connector',
};
const ConnectorsDropdownComponent: React.FC = ({
@@ -60,7 +60,7 @@ const ConnectorsDropdownComponent: React.FC = ({
{connector.name}
>
),
- 'data-test-subj': connector.id,
+ 'data-test-subj': `dropdown-connector-${connector.id}`,
},
],
[noConnectorOption]
@@ -76,6 +76,7 @@ const ConnectorsDropdownComponent: React.FC = ({
valueOfSelected={selectedConnector}
fullWidth
onChange={onChange}
+ data-test-subj="dropdown-connectors"
/>
);
};
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx
new file mode 100644
index 000000000000000..9ab752bb589c0a8
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 { mount, ReactWrapper } from 'enzyme';
+
+import { FieldMapping, FieldMappingProps } from './field_mapping';
+import { mapping } from './__mock__';
+import { FieldMappingRow } from './field_mapping_row';
+import { defaultMapping } from '../../../../lib/connectors/config';
+import { TestProviders } from '../../../../mock';
+
+describe('FieldMappingRow', () => {
+ let wrapper: ReactWrapper;
+ const onChangeMapping = jest.fn();
+ const props: FieldMappingProps = {
+ disabled: false,
+ mapping,
+ onChangeMapping,
+ };
+
+ beforeAll(() => {
+ wrapper = mount(, { wrappingComponent: TestProviders });
+ });
+
+ test('it renders', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-configure-field-mapping-cols"]')
+ .first()
+ .exists()
+ ).toBe(true);
+
+ expect(
+ wrapper
+ .find('[data-test-subj="case-configure-field-mapping-row-wrapper"]')
+ .first()
+ .exists()
+ ).toBe(true);
+
+ expect(wrapper.find(FieldMappingRow).length).toEqual(3);
+ });
+
+ test('it shows the correct number of FieldMappingRow with default mapping', () => {
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ expect(newWrapper.find(FieldMappingRow).length).toEqual(3);
+ });
+
+ test('it pass the corrects props to mapping row', () => {
+ const rows = wrapper.find(FieldMappingRow);
+ rows.forEach((row, index) => {
+ expect(row.prop('siemField')).toEqual(mapping[index].source);
+ expect(row.prop('selectedActionType')).toEqual(mapping[index].actionType);
+ expect(row.prop('selectedThirdParty')).toEqual(mapping[index].target);
+ });
+ });
+
+ test('it pass the default mapping when mapping is null', () => {
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ const rows = newWrapper.find(FieldMappingRow);
+ rows.forEach((row, index) => {
+ expect(row.prop('siemField')).toEqual(defaultMapping[index].source);
+ expect(row.prop('selectedActionType')).toEqual(defaultMapping[index].actionType);
+ expect(row.prop('selectedThirdParty')).toEqual(defaultMapping[index].target);
+ });
+ });
+
+ test('it should show zero rows on empty array', () => {
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ expect(newWrapper.find(FieldMappingRow).length).toEqual(0);
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx
index 0c0dc14f1c218da..2934b1056e29cd7 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx
@@ -18,6 +18,7 @@ import { FieldMappingRow } from './field_mapping_row';
import * as i18n from './translations';
import { defaultMapping } from '../../../../lib/connectors/config';
+import { setActionTypeToMapping, setThirdPartyToMapping } from './utils';
const FieldRowWrapper = styled.div`
margin-top: 8px;
@@ -28,22 +29,26 @@ const supportedThirdPartyFields: Array> =
{
value: 'not_mapped',
inputDisplay: {i18n.FIELD_MAPPING_FIELD_NOT_MAPPED},
+ 'data-test-subj': 'third-party-field-not-mapped',
},
{
value: 'short_description',
inputDisplay: {i18n.FIELD_MAPPING_FIELD_SHORT_DESC},
+ 'data-test-subj': 'third-party-field-short-description',
},
{
value: 'comments',
inputDisplay: {i18n.FIELD_MAPPING_FIELD_COMMENTS},
+ 'data-test-subj': 'third-party-field-comments',
},
{
value: 'description',
inputDisplay: {i18n.FIELD_MAPPING_FIELD_DESC},
+ 'data-test-subj': 'third-party-field-description',
},
];
-interface FieldMappingProps {
+export interface FieldMappingProps {
disabled: boolean;
mapping: CasesConfigurationMapping[] | null;
onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void;
@@ -57,14 +62,7 @@ const FieldMappingComponent: React.FC = ({
const onChangeActionType = useCallback(
(caseField: CaseField, newActionType: ActionType) => {
const myMapping = mapping ?? defaultMapping;
- const findItemIndex = myMapping.findIndex(item => item.source === caseField);
- if (findItemIndex >= 0) {
- onChangeMapping([
- ...myMapping.slice(0, findItemIndex),
- { ...myMapping[findItemIndex], actionType: newActionType },
- ...myMapping.slice(findItemIndex + 1),
- ]);
- }
+ onChangeMapping(setActionTypeToMapping(caseField, newActionType, myMapping));
},
[mapping]
);
@@ -72,22 +70,13 @@ const FieldMappingComponent: React.FC = ({
const onChangeThirdParty = useCallback(
(caseField: CaseField, newThirdPartyField: ThirdPartyField) => {
const myMapping = mapping ?? defaultMapping;
- onChangeMapping(
- myMapping.map(item => {
- if (item.source !== caseField && item.target === newThirdPartyField) {
- return { ...item, target: 'not_mapped' };
- } else if (item.source === caseField) {
- return { ...item, target: newThirdPartyField };
- }
- return item;
- })
- );
+ onChangeMapping(setThirdPartyToMapping(caseField, newThirdPartyField, myMapping));
},
[mapping]
);
return (
<>
-
+
{i18n.FIELD_MAPPING_FIRST_COL}
@@ -100,7 +89,7 @@ const FieldMappingComponent: React.FC = ({
-
+
{(mapping ?? defaultMapping).map(item => (
> = [
+ {
+ value: 'short_description',
+ inputDisplay: {'Short Description'},
+ 'data-test-subj': 'third-party-short-desc',
+ },
+ {
+ value: 'description',
+ inputDisplay: {'Description'},
+ 'data-test-subj': 'third-party-desc',
+ },
+];
+
+describe('FieldMappingRow', () => {
+ let wrapper: ReactWrapper;
+ const onChangeActionType = jest.fn();
+ const onChangeThirdParty = jest.fn();
+
+ const props: RowProps = {
+ disabled: false,
+ siemField: 'title',
+ thirdPartyOptions,
+ onChangeActionType,
+ onChangeThirdParty,
+ selectedActionType: 'nothing',
+ selectedThirdParty: 'short_description',
+ };
+
+ beforeAll(() => {
+ wrapper = mount(, { wrappingComponent: TestProviders });
+ });
+
+ test('it renders', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-configure-third-party-select"]')
+ .first()
+ .exists()
+ ).toBe(true);
+
+ expect(
+ wrapper
+ .find('[data-test-subj="case-configure-action-type-select"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it passes thirdPartyOptions correctly', () => {
+ const selectProps = wrapper
+ .find(EuiSuperSelect)
+ .first()
+ .props();
+
+ expect(selectProps.options).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: 'short_description',
+ 'data-test-subj': 'third-party-short-desc',
+ }),
+ expect.objectContaining({
+ value: 'description',
+ 'data-test-subj': 'third-party-desc',
+ }),
+ ])
+ );
+ });
+
+ test('it passes the correct actionTypeOptions', () => {
+ const selectProps = wrapper
+ .find(EuiSuperSelect)
+ .at(1)
+ .props();
+
+ expect(selectProps.options).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: 'nothing',
+ 'data-test-subj': 'edit-update-option-nothing',
+ }),
+ expect.objectContaining({
+ value: 'overwrite',
+ 'data-test-subj': 'edit-update-option-overwrite',
+ }),
+ expect.objectContaining({
+ value: 'append',
+ 'data-test-subj': 'edit-update-option-append',
+ }),
+ ])
+ );
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx
index 62e43c86af8d9e7..732a11a58d35a71 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx
@@ -21,7 +21,7 @@ import {
ThirdPartyField,
} from '../../../../containers/case/configure/types';
-interface RowProps {
+export interface RowProps {
disabled: boolean;
siemField: CaseField;
thirdPartyOptions: Array>;
@@ -77,6 +77,7 @@ const FieldMappingRowComponent: React.FC = ({
options={thirdPartyOptions}
valueOfSelected={selectedThirdParty}
onChange={onChangeThirdParty.bind(null, siemField)}
+ data-test-subj={'case-configure-third-party-select'}
/>
@@ -85,6 +86,7 @@ const FieldMappingRowComponent: React.FC = ({
options={actionTypeOptions}
valueOfSelected={selectedActionType}
onChange={onChangeActionType.bind(null, siemField)}
+ data-test-subj={'case-configure-action-type-select'}
/>
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx
new file mode 100644
index 000000000000000..5ea3f500c0349f5
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx
@@ -0,0 +1,748 @@
+/*
+ * 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, { useEffect } from 'react';
+import { ReactWrapper, mount } from 'enzyme';
+
+import { useKibana } from '../../../../lib/kibana';
+import { useConnectors } from '../../../../containers/case/configure/use_connectors';
+import { useCaseConfigure } from '../../../../containers/case/configure/use_configure';
+import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search';
+
+import {
+ connectors,
+ searchURL,
+ useCaseConfigureResponse,
+ useConnectorsResponse,
+ kibanaMockImplementationArgs,
+} from './__mock__';
+
+jest.mock('../../../../lib/kibana');
+jest.mock('../../../../containers/case/configure/use_connectors');
+jest.mock('../../../../containers/case/configure/use_configure');
+jest.mock('../../../../components/navigation/use_get_url_search');
+
+const useKibanaMock = useKibana as jest.Mock;
+const useConnectorsMock = useConnectors as jest.Mock;
+const useCaseConfigureMock = useCaseConfigure as jest.Mock;
+const useGetUrlSearchMock = useGetUrlSearch as jest.Mock;
+
+import { ConfigureCases } from './';
+import { TestProviders } from '../../../../mock';
+import { Connectors } from './connectors';
+import { ClosureOptions } from './closure_options';
+import { Mapping } from './mapping';
+import {
+ ActionsConnectorsContextProvider,
+ ConnectorAddFlyout,
+ ConnectorEditFlyout,
+} from '../../../../../../../../plugins/triggers_actions_ui/public';
+import { EuiBottomBar } from '@elastic/eui';
+
+describe('rendering', () => {
+ let wrapper: ReactWrapper;
+ beforeEach(() => {
+ jest.resetAllMocks();
+ useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
+ useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] }));
+ useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs);
+ useGetUrlSearchMock.mockImplementation(() => searchURL);
+
+ wrapper = mount(, { wrappingComponent: TestProviders });
+ });
+
+ test('it renders the Connectors', () => {
+ expect(wrapper.find('[data-test-subj="case-connectors-form-group"]').exists()).toBeTruthy();
+ });
+
+ test('it renders the ClosureType', () => {
+ expect(
+ wrapper.find('[data-test-subj="case-closure-options-form-group"]').exists()
+ ).toBeTruthy();
+ });
+
+ test('it renders the Mapping', () => {
+ expect(wrapper.find('[data-test-subj="case-mapping-form-group"]').exists()).toBeTruthy();
+ });
+
+ test('it renders the ActionsConnectorsContextProvider', () => {
+ // Components from triggers_actions_ui do not have a data-test-subj
+ expect(wrapper.find(ActionsConnectorsContextProvider).exists()).toBeTruthy();
+ });
+
+ test('it renders the ConnectorAddFlyout', () => {
+ // Components from triggers_actions_ui do not have a data-test-subj
+ expect(wrapper.find(ConnectorAddFlyout).exists()).toBeTruthy();
+ });
+
+ test('it does NOT render the ConnectorEditFlyout', () => {
+ // Components from triggers_actions_ui do not have a data-test-subj
+ expect(wrapper.find(ConnectorEditFlyout).exists()).toBeFalsy();
+ });
+
+ test('it does NOT render the EuiCallOut', () => {
+ expect(wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists()).toBeFalsy();
+ });
+
+ test('it does NOT render the EuiBottomBar', () => {
+ expect(
+ wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
+ ).toBeFalsy();
+ });
+});
+
+describe('ConfigureCases - Unhappy path', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
+ useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] }));
+ useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs);
+ useGetUrlSearchMock.mockImplementation(() => searchURL);
+ });
+
+ test('it shows the warning callout when configuration is invalid', () => {
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('not-id'), []);
+ return useCaseConfigureResponse;
+ }
+ );
+
+ const wrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(
+ wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists()
+ ).toBeTruthy();
+ });
+});
+
+describe('ConfigureCases - Happy path', () => {
+ let wrapper: ReactWrapper;
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('123'), []);
+ useEffect(() => setClosureType('close-by-user'), []);
+ useEffect(
+ () =>
+ setCurrentConfiguration({
+ connectorId: '123',
+ closureType: 'close-by-user',
+ }),
+ []
+ );
+ return useCaseConfigureResponse;
+ }
+ );
+ useConnectorsMock.mockImplementation(() => useConnectorsResponse);
+ useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs);
+ useGetUrlSearchMock.mockImplementation(() => searchURL);
+
+ wrapper = mount(, { wrappingComponent: TestProviders });
+ });
+
+ test('it renders the ConnectorEditFlyout', () => {
+ expect(wrapper.find(ConnectorEditFlyout).exists()).toBeTruthy();
+ });
+
+ test('it renders with correct props', () => {
+ // Connector
+ expect(wrapper.find(Connectors).prop('connectors')).toEqual(connectors);
+ expect(wrapper.find(Connectors).prop('disabled')).toBe(false);
+ expect(wrapper.find(Connectors).prop('isLoading')).toBe(false);
+ expect(wrapper.find(Connectors).prop('selectedConnector')).toBe('123');
+
+ // ClosureOptions
+ expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false);
+ expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user');
+
+ // Mapping
+ expect(wrapper.find(Mapping).prop('disabled')).toBe(true);
+ expect(wrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(false);
+ expect(wrapper.find(Mapping).prop('mapping')).toEqual(
+ connectors[0].config.casesConfiguration.mapping
+ );
+
+ // Flyouts
+ expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false);
+ expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([
+ {
+ id: '.servicenow',
+ name: 'ServiceNow',
+ enabled: true,
+ enabledInConfig: true,
+ enabledInLicense: true,
+ minimumLicenseRequired: 'platinum',
+ },
+ ]);
+
+ expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false);
+ expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[0]);
+ });
+
+ test('it disables correctly when the user cannot crud', () => {
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ expect(newWrapper.find(Connectors).prop('disabled')).toBe(true);
+ expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true);
+ expect(newWrapper.find(Mapping).prop('disabled')).toBe(true);
+ expect(newWrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(true);
+ });
+
+ test('it disables correctly Connector when loading connectors', () => {
+ useConnectorsMock.mockImplementation(() => ({
+ ...useConnectorsResponse,
+ loading: true,
+ }));
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(newWrapper.find(Connectors).prop('disabled')).toBe(true);
+ });
+
+ test('it disables correctly Connector when saving configuration', () => {
+ useCaseConfigureMock.mockImplementation(() => ({
+ ...useCaseConfigureResponse,
+ persistLoading: true,
+ }));
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(newWrapper.find(Connectors).prop('disabled')).toBe(true);
+ });
+
+ test('it pass the correct value to isLoading attribute on Connector', () => {
+ useConnectorsMock.mockImplementation(() => ({
+ ...useConnectorsResponse,
+ loading: true,
+ }));
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(newWrapper.find(Connectors).prop('isLoading')).toBe(true);
+ });
+
+ test('it set correctly the selected connector', () => {
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('456'), []);
+ return useCaseConfigureResponse;
+ }
+ );
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(newWrapper.find(Connectors).prop('selectedConnector')).toBe('456');
+ });
+
+ test('it show the add flyout when pressing the add connector button', () => {
+ wrapper.find('button[data-test-subj="case-configure-add-connector-button"]').simulate('click');
+ wrapper.update();
+
+ expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true);
+ expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy();
+ });
+
+ test('it disables correctly ClosureOptions when loading connectors', () => {
+ useConnectorsMock.mockImplementation(() => ({
+ ...useConnectorsResponse,
+ loading: true,
+ }));
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true);
+ });
+
+ test('it disables correctly ClosureOptions when saving configuration', () => {
+ useCaseConfigureMock.mockImplementation(() => ({
+ ...useCaseConfigureResponse,
+ persistLoading: true,
+ }));
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true);
+ });
+
+ test('it disables correctly ClosureOptions when the connector is set to none', () => {
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('none'), []);
+ return useCaseConfigureResponse;
+ }
+ );
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true);
+ });
+
+ test('it disables the mapping permanently', () => {
+ expect(wrapper.find(Mapping).prop('disabled')).toBe(true);
+ });
+
+ test('it disables the update connector button when loading the connectors', () => {
+ useConnectorsMock.mockImplementation(() => ({
+ ...useConnectorsResponse,
+ loading: true,
+ }));
+
+ expect(wrapper.find(Mapping).prop('disabled')).toBe(true);
+ });
+
+ test('it disables the update connector button when loading the configuration', () => {
+ useCaseConfigureMock.mockImplementation(() => ({
+ ...useCaseConfigureResponse,
+ loading: true,
+ }));
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(newWrapper.find(Mapping).prop('disabled')).toBe(true);
+ });
+
+ test('it disables the update connector button when saving the configuration', () => {
+ useCaseConfigureMock.mockImplementation(() => ({
+ ...useCaseConfigureResponse,
+ persistLoading: true,
+ }));
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(newWrapper.find(Mapping).prop('disabled')).toBe(true);
+ });
+
+ test('it disables the update connector button when the connectorId is invalid', () => {
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('not-id'), []);
+ return useCaseConfigureResponse;
+ }
+ );
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(newWrapper.find(Mapping).prop('disabled')).toBe(true);
+ });
+
+ test('it disables the update connector button when the connectorId is set to none', () => {
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('none'), []);
+ return useCaseConfigureResponse;
+ }
+ );
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(newWrapper.find(Mapping).prop('disabled')).toBe(true);
+ });
+
+ test('it show the edit flyout when pressing the update connector button', () => {
+ wrapper.find('button[data-test-subj="case-mapping-update-connector-button"]').simulate('click');
+ wrapper.update();
+
+ expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true);
+ expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy();
+ });
+
+ test('it sets the mapping of a connector correctly', () => {
+ expect(wrapper.find(Mapping).prop('mapping')).toEqual(
+ connectors[0].config.casesConfiguration.mapping
+ );
+ });
+
+ // TODO: When mapping is enabled the test.todo should be implemented.
+ test.todo('the mapping is changed successfully when changing the third party');
+ test.todo('the mapping is changed successfully when changing the action type');
+
+ test('it does not shows the action bar when there is no change', () => {
+ expect(
+ wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
+ ).toBeFalsy();
+ });
+
+ test('it shows the action bar when the connector is changed', () => {
+ wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
+ wrapper.update();
+ wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click');
+ wrapper.update();
+
+ expect(
+ wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
+ ).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]')
+ .first()
+ .text()
+ ).toBe('1 unsaved changes');
+ });
+
+ test('it shows the action bar when the closure type is changed', () => {
+ wrapper.find('input[id="close-by-pushing"]').simulate('change');
+ wrapper.update();
+
+ expect(
+ wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
+ ).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]')
+ .first()
+ .text()
+ ).toBe('1 unsaved changes');
+ });
+
+ test('it tracks the changes successfully', () => {
+ wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
+ wrapper.update();
+ wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click');
+ wrapper.update();
+ wrapper.find('input[id="close-by-pushing"]').simulate('change');
+ wrapper.update();
+
+ expect(
+ wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
+ ).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]')
+ .first()
+ .text()
+ ).toBe('2 unsaved changes');
+ });
+
+ test('it tracks and reverts the changes successfully ', () => {
+ // change settings
+ wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
+ wrapper.update();
+ wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click');
+ wrapper.update();
+ wrapper.find('input[id="close-by-pushing"]').simulate('change');
+ wrapper.update();
+
+ // revert back to initial settings
+ wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
+ wrapper.update();
+ wrapper.find('button[data-test-subj="dropdown-connector-123"]').simulate('click');
+ wrapper.update();
+ wrapper.find('input[id="close-by-user"]').simulate('change');
+ wrapper.update();
+
+ expect(
+ wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
+ ).toBeFalsy();
+ });
+
+ test('it close and restores the action bar when the add connector button is pressed', () => {
+ // Change closure type
+ wrapper.find('input[id="close-by-pushing"]').simulate('change');
+ wrapper.update();
+
+ // Press add connector button
+ wrapper.find('button[data-test-subj="case-configure-add-connector-button"]').simulate('click');
+ wrapper.update();
+
+ expect(
+ wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
+ ).toBeFalsy();
+
+ expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true);
+
+ // Close the add flyout
+ wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click');
+ wrapper.update();
+
+ expect(
+ wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
+ ).toBeTruthy();
+
+ expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false);
+
+ expect(
+ wrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]')
+ .first()
+ .text()
+ ).toBe('1 unsaved changes');
+ });
+
+ test('it close and restores the action bar when the update connector button is pressed', () => {
+ // Change closure type
+ wrapper.find('input[id="close-by-pushing"]').simulate('change');
+ wrapper.update();
+
+ // Press update connector button
+ wrapper.find('button[data-test-subj="case-mapping-update-connector-button"]').simulate('click');
+ wrapper.update();
+
+ expect(
+ wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
+ ).toBeFalsy();
+
+ expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true);
+
+ // Close the edit flyout
+ wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click');
+ wrapper.update();
+
+ expect(
+ wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
+ ).toBeTruthy();
+
+ expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false);
+
+ expect(
+ wrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]')
+ .first()
+ .text()
+ ).toBe('1 unsaved changes');
+ });
+
+ test('it disables the buttons of action bar when loading connectors', () => {
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('456'), []);
+ useEffect(() => setClosureType('close-by-user'), []);
+ useEffect(
+ () =>
+ setCurrentConfiguration({
+ connectorId: '123',
+ closureType: 'close-by-user',
+ }),
+ []
+ );
+ return useCaseConfigureResponse;
+ }
+ );
+
+ useConnectorsMock.mockImplementation(() => ({
+ ...useConnectorsResponse,
+ loading: true,
+ }));
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]')
+ .first()
+ .prop('isDisabled')
+ ).toBe(true);
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]')
+ .first()
+ .prop('isDisabled')
+ ).toBe(true);
+ });
+
+ test('it disables the buttons of action bar when loading configuration', () => {
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('456'), []);
+ useEffect(() => setClosureType('close-by-user'), []);
+ useEffect(
+ () =>
+ setCurrentConfiguration({
+ connectorId: '123',
+ closureType: 'close-by-user',
+ }),
+ []
+ );
+ return { ...useCaseConfigureResponse, loading: true };
+ }
+ );
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]')
+ .first()
+ .prop('isDisabled')
+ ).toBe(true);
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]')
+ .first()
+ .prop('isDisabled')
+ ).toBe(true);
+ });
+
+ test('it disables the buttons of action bar when saving configuration', () => {
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('456'), []);
+ useEffect(() => setClosureType('close-by-user'), []);
+ useEffect(
+ () =>
+ setCurrentConfiguration({
+ connectorId: '123',
+ closureType: 'close-by-user',
+ }),
+ []
+ );
+ return { ...useCaseConfigureResponse, persistLoading: true };
+ }
+ );
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]')
+ .first()
+ .prop('isDisabled')
+ ).toBe(true);
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]')
+ .first()
+ .prop('isDisabled')
+ ).toBe(true);
+ });
+
+ test('it shows the loading spinner when saving configuration', () => {
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('456'), []);
+ useEffect(() => setClosureType('close-by-user'), []);
+ useEffect(
+ () =>
+ setCurrentConfiguration({
+ connectorId: '123',
+ closureType: 'close-by-user',
+ }),
+ []
+ );
+ return { ...useCaseConfigureResponse, persistLoading: true };
+ }
+ );
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]')
+ .first()
+ .prop('isLoading')
+ ).toBe(true);
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]')
+ .first()
+ .prop('isLoading')
+ ).toBe(true);
+ });
+
+ test('it closes the action bar when pressing save', () => {
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('456'), []);
+ useEffect(() => setClosureType('close-by-user'), []);
+ useEffect(
+ () =>
+ setCurrentConfiguration({
+ connectorId: '123',
+ closureType: 'close-by-user',
+ }),
+ []
+ );
+ return useCaseConfigureResponse;
+ }
+ );
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ newWrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]')
+ .first()
+ .simulate('click');
+
+ newWrapper.update();
+
+ expect(
+ newWrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
+ ).toBeFalsy();
+ });
+
+ test('it submits the configuration correctly', () => {
+ const persistCaseConfigure = jest.fn();
+
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('456'), []);
+ useEffect(() => setClosureType('close-by-user'), []);
+ useEffect(
+ () =>
+ setCurrentConfiguration({
+ connectorId: '123',
+ closureType: 'close-by-pushing',
+ }),
+ []
+ );
+ return { ...useCaseConfigureResponse, persistCaseConfigure };
+ }
+ );
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ newWrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]')
+ .first()
+ .simulate('click');
+
+ newWrapper.update();
+
+ expect(persistCaseConfigure).toHaveBeenCalled();
+ expect(persistCaseConfigure).toHaveBeenCalledWith({
+ connectorId: '456',
+ connectorName: 'My Connector 2',
+ closureType: 'close-by-user',
+ });
+ });
+
+ test('it has the correct url on cancel button', () => {
+ const persistCaseConfigure = jest.fn();
+
+ useCaseConfigureMock.mockImplementation(
+ ({ setConnector, setClosureType, setCurrentConfiguration }) => {
+ useEffect(() => setConnector('456'), []);
+ useEffect(() => setClosureType('close-by-user'), []);
+ useEffect(
+ () =>
+ setCurrentConfiguration({
+ connectorId: '123',
+ closureType: 'close-by-user',
+ }),
+ []
+ );
+ return { ...useCaseConfigureResponse, persistCaseConfigure };
+ }
+ );
+
+ const newWrapper = mount(, { wrappingComponent: TestProviders });
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]')
+ .first()
+ .prop('href')
+ ).toBe(`#/link-to/case${searchURL}`);
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
index b8cf5a388080119..241dcef14a145d7 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
@@ -140,6 +140,7 @@ const ConfigureCasesComponent: React.FC = ({ userC
setClosureType,
setCurrentConfiguration,
});
+
const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors();
// ActionsConnectorsContextProvider reloadConnectors prop expects a Promise.
@@ -251,7 +252,12 @@ const ConfigureCasesComponent: React.FC = ({ userC
{!connectorIsValid && (
-
+
{i18n.WARNING_NO_CONNECTOR_MESSAGE}
@@ -283,11 +289,13 @@ const ConfigureCasesComponent: React.FC = ({ userC
/>
{actionBarVisible && (
-
+
- {i18n.UNSAVED_CHANGES(totalConfigurationChanges)}
+
+ {i18n.UNSAVED_CHANGES(totalConfigurationChanges)}
+
@@ -300,6 +308,7 @@ const ConfigureCasesComponent: React.FC = ({ userC
isLoading={persistLoading}
aria-label={i18n.CANCEL}
href={getCaseUrl(search)}
+ data-test-subj="case-configure-action-bottom-bar-cancel-button"
>
{i18n.CANCEL}
@@ -313,6 +322,7 @@ const ConfigureCasesComponent: React.FC = ({ userC
isDisabled={isLoadingAny}
isLoading={persistLoading}
onClick={handleSubmit}
+ data-test-subj="case-configure-action-bottom-bar-save-button"
>
{i18n.SAVE_CHANGES}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx
new file mode 100644
index 000000000000000..fefcb2ca8cf6a39
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.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 from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+
+import { TestProviders } from '../../../../mock';
+import { Mapping, MappingProps } from './mapping';
+import { mapping } from './__mock__';
+
+describe('Mapping', () => {
+ let wrapper: ReactWrapper;
+ const onChangeMapping = jest.fn();
+ const setEditFlyoutVisibility = jest.fn();
+ const props: MappingProps = {
+ disabled: false,
+ mapping,
+ updateConnectorDisabled: false,
+ onChangeMapping,
+ setEditFlyoutVisibility,
+ };
+
+ beforeAll(() => {
+ wrapper = mount(, { wrappingComponent: TestProviders });
+ });
+
+ test('it shows mapping form group', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-mapping-form-group"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it shows mapping form row', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-mapping-form-row"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it shows the update button', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-mapping-update-connector-button"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+
+ test('it shows the field mapping', () => {
+ expect(
+ wrapper
+ .find('[data-test-subj="case-mapping-field"]')
+ .first()
+ .exists()
+ ).toBe(true);
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx
index 8cba73d1249df1f..7340a49f6d0bbcf 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx
@@ -20,7 +20,7 @@ import * as i18n from './translations';
import { FieldMapping } from './field_mapping';
import { CasesConfigurationMapping } from '../../../../containers/case/configure/types';
-interface MappingProps {
+export interface MappingProps {
disabled: boolean;
updateConnectorDisabled: boolean;
mapping: CasesConfigurationMapping[] | null;
@@ -45,20 +45,27 @@ const MappingComponent: React.FC = ({
fullWidth
title={{i18n.FIELD_MAPPING_TITLE}
}
description={i18n.FIELD_MAPPING_DESC}
+ data-test-subj="case-mapping-form-group"
>
-
+
{i18n.UPDATE_CONNECTOR}
-
+
);
};
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts
new file mode 100644
index 000000000000000..df958b75dc6b890
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts
@@ -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 { configureCasesReducer, Action, State } from './reducer';
+import { initialState, mapping } from './__mock__';
+
+describe('Reducer', () => {
+ let reducer: (state: State, action: Action) => State;
+
+ beforeAll(() => {
+ reducer = configureCasesReducer();
+ });
+
+ test('it should set the correct configuration', () => {
+ const action: Action = {
+ type: 'setCurrentConfiguration',
+ currentConfiguration: { connectorId: '123', closureType: 'close-by-user' },
+ };
+ const state = reducer(initialState, action);
+
+ expect(state).toEqual({
+ ...state,
+ currentConfiguration: action.currentConfiguration,
+ });
+ });
+
+ test('it should set the correct connector id', () => {
+ const action: Action = {
+ type: 'setConnectorId',
+ connectorId: '456',
+ };
+ const state = reducer(initialState, action);
+
+ expect(state).toEqual({
+ ...state,
+ connectorId: action.connectorId,
+ });
+ });
+
+ test('it should set the closure type', () => {
+ const action: Action = {
+ type: 'setClosureType',
+ closureType: 'close-by-pushing',
+ };
+ const state = reducer(initialState, action);
+
+ expect(state).toEqual({
+ ...state,
+ closureType: action.closureType,
+ });
+ });
+
+ test('it should set the mapping', () => {
+ const action: Action = {
+ type: 'setMapping',
+ mapping,
+ };
+ const state = reducer(initialState, action);
+
+ expect(state).toEqual({
+ ...state,
+ mapping: action.mapping,
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx
new file mode 100644
index 000000000000000..1c6fc9b2d405f9a
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 { mapping } from './__mock__';
+import { setActionTypeToMapping, setThirdPartyToMapping } from './utils';
+import { CasesConfigurationMapping } from '../../../../containers/case/configure/types';
+
+describe('FieldMappingRow', () => {
+ test('it should change the action type', () => {
+ const newMapping = setActionTypeToMapping('title', 'nothing', mapping);
+ expect(newMapping[0].actionType).toBe('nothing');
+ });
+
+ test('it should not change other fields', () => {
+ const [newTitle, description, comments] = setActionTypeToMapping('title', 'nothing', mapping);
+ expect(newTitle).not.toEqual(mapping[0]);
+ expect(description).toEqual(mapping[1]);
+ expect(comments).toEqual(mapping[2]);
+ });
+
+ test('it should return a new array when changing action type', () => {
+ const newMapping = setActionTypeToMapping('title', 'nothing', mapping);
+ expect(newMapping).not.toBe(mapping);
+ });
+
+ test('it should change the third party', () => {
+ const newMapping = setThirdPartyToMapping('title', 'description', mapping);
+ expect(newMapping[0].target).toBe('description');
+ });
+
+ test('it should not change other fields when there is not a conflict', () => {
+ const tempMapping: CasesConfigurationMapping[] = [
+ {
+ source: 'title',
+ target: 'short_description',
+ actionType: 'overwrite',
+ },
+ {
+ source: 'comments',
+ target: 'comments',
+ actionType: 'append',
+ },
+ ];
+
+ const [newTitle, comments] = setThirdPartyToMapping('title', 'description', tempMapping);
+
+ expect(newTitle).not.toEqual(mapping[0]);
+ expect(comments).toEqual(tempMapping[1]);
+ });
+
+ test('it should return a new array when changing third party', () => {
+ const newMapping = setThirdPartyToMapping('title', 'description', mapping);
+ expect(newMapping).not.toBe(mapping);
+ });
+
+ test('it should change the target of the conflicting third party field to not_mapped', () => {
+ const newMapping = setThirdPartyToMapping('title', 'description', mapping);
+ expect(newMapping[1].target).toBe('not_mapped');
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts
new file mode 100644
index 000000000000000..2ac6cc1a385872a
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 {
+ CaseField,
+ ActionType,
+ CasesConfigurationMapping,
+ ThirdPartyField,
+} from '../../../../containers/case/configure/types';
+
+export const setActionTypeToMapping = (
+ caseField: CaseField,
+ newActionType: ActionType,
+ mapping: CasesConfigurationMapping[]
+): CasesConfigurationMapping[] => {
+ const findItemIndex = mapping.findIndex(item => item.source === caseField);
+
+ if (findItemIndex >= 0) {
+ return [
+ ...mapping.slice(0, findItemIndex),
+ { ...mapping[findItemIndex], actionType: newActionType },
+ ...mapping.slice(findItemIndex + 1),
+ ];
+ }
+
+ return [...mapping];
+};
+
+export const setThirdPartyToMapping = (
+ caseField: CaseField,
+ newThirdPartyField: ThirdPartyField,
+ mapping: CasesConfigurationMapping[]
+): CasesConfigurationMapping[] =>
+ mapping.map(item => {
+ if (item.source !== caseField && item.target === newThirdPartyField) {
+ return { ...item, target: 'not_mapped' };
+ } else if (item.source === caseField) {
+ return { ...item, target: newThirdPartyField };
+ }
+ return item;
+ });
From 0ebfe76b3fa0cb104c6accf8469fe390ba239b40 Mon Sep 17 00:00:00 2001
From: patrykkopycinski
Date: Mon, 6 Apr 2020 19:26:40 +0200
Subject: [PATCH 13/15] [SIEM][Detection Engine] Fix signals count in Rule
notifications (#62311)
---
.../notifications/get_signals_count.ts | 53 +--
.../rules_notification_alert_type.ts | 22 +-
.../schedule_notification_actions.ts | 4 +-
.../detection_engine/notifications/utils.ts | 13 +-
.../signals/search_after_bulk_create.test.ts | 64 ++-
.../signals/search_after_bulk_create.ts | 13 +-
.../signals/signal_rule_alert_type.test.ts | 399 ++++++++++++++++++
.../signals/signal_rule_alert_type.ts | 31 +-
.../signals/single_bulk_create.test.ts | 40 +-
.../signals/single_bulk_create.ts | 8 +-
10 files changed, 567 insertions(+), 80 deletions(-)
create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts
index 33cee6d074b7069..7ff6a4e5164bd74 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts
@@ -4,63 +4,40 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import moment from 'moment';
-import { getNotificationResultsLink } from './utils';
-import { NotificationExecutorOptions } from './types';
-import { parseScheduleDates } from '../signals/utils';
+import { AlertServices } from '../../../../../../../plugins/alerting/server';
import { buildSignalsSearchQuery } from './build_signals_query';
-interface SignalsCountResults {
- signalsCount: string;
- resultsLink: string;
-}
-
interface GetSignalsCount {
- from: Date | string;
- to: Date | string;
- ruleAlertId: string;
+ from?: string;
+ to?: string;
ruleId: string;
index: string;
- kibanaSiemAppUrl: string | undefined;
- callCluster: NotificationExecutorOptions['services']['callCluster'];
+ callCluster: AlertServices['callCluster'];
+}
+
+interface CountResult {
+ count: number;
}
export const getSignalsCount = async ({
from,
to,
- ruleAlertId,
ruleId,
index,
callCluster,
- kibanaSiemAppUrl = '',
-}: GetSignalsCount): Promise => {
- const fromMoment = moment.isDate(from) ? moment(from) : parseScheduleDates(from);
- const toMoment = moment.isDate(to) ? moment(to) : parseScheduleDates(to);
-
- if (!fromMoment || !toMoment) {
- throw new Error(`There was an issue with parsing ${from} or ${to} into Moment object`);
+}: GetSignalsCount): Promise => {
+ if (from == null || to == null) {
+ throw Error('"from" or "to" was not provided to signals count query');
}
- const fromInMs = fromMoment.format('x');
- const toInMs = toMoment.format('x');
-
const query = buildSignalsSearchQuery({
index,
ruleId,
- to: toInMs,
- from: fromInMs,
+ to,
+ from,
});
- const result = await callCluster('count', query);
- const resultsLink = getNotificationResultsLink({
- kibanaSiemAppUrl: `${kibanaSiemAppUrl}`,
- id: ruleAlertId,
- from: fromInMs,
- to: toInMs,
- });
+ const result: CountResult = await callCluster('count', query);
- return {
- signalsCount: result.count,
- resultsLink,
- };
+ return result.count;
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts
index e74da583e919325..546488caa5ee78b 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts
@@ -13,6 +13,8 @@ import { getSignalsCount } from './get_signals_count';
import { RuleAlertAttributes } from '../signals/types';
import { siemRuleActionGroups } from '../signals/siem_rule_action_groups';
import { scheduleNotificationActions } from './schedule_notification_actions';
+import { getNotificationResultsLink } from './utils';
+import { parseScheduleDates } from '../signals/utils';
export const rulesNotificationAlertType = ({
logger,
@@ -42,16 +44,26 @@ export const rulesNotificationAlertType = ({
const { params: ruleAlertParams, name: ruleName } = ruleAlertSavedObject.attributes;
const ruleParams = { ...ruleAlertParams, name: ruleName, id: ruleAlertSavedObject.id };
- const { signalsCount, resultsLink } = await getSignalsCount({
- from: previousStartedAt ?? `now-${ruleParams.interval}`,
- to: startedAt,
+ const fromInMs = parseScheduleDates(
+ previousStartedAt ? previousStartedAt.toISOString() : `now-${ruleParams.interval}`
+ )?.format('x');
+ const toInMs = parseScheduleDates(startedAt.toISOString())?.format('x');
+
+ const signalsCount = await getSignalsCount({
+ from: fromInMs,
+ to: toInMs,
index: ruleParams.outputIndex,
ruleId: ruleParams.ruleId!,
- kibanaSiemAppUrl: ruleAlertParams.meta?.kibanaSiemAppUrl as string,
- ruleAlertId: ruleAlertSavedObject.id,
callCluster: services.callCluster,
});
+ const resultsLink = getNotificationResultsLink({
+ from: fromInMs,
+ to: toInMs,
+ id: ruleAlertSavedObject.id,
+ kibanaSiemAppUrl: ruleAlertParams.meta?.kibanaSiemAppUrl as string,
+ });
+
logger.info(
`Found ${signalsCount} signals using signal rule name: "${ruleParams.name}", id: "${params.ruleAlertId}", rule_id: "${ruleParams.ruleId}" in "${ruleParams.outputIndex}" index`
);
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts
index b858b25377ffe91..749b892ef506f3b 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts
@@ -15,7 +15,7 @@ type NotificationRuleTypeParams = RuleTypeParams & {
interface ScheduleNotificationActions {
alertInstance: AlertInstance;
- signalsCount: string;
+ signalsCount: number;
resultsLink: string;
ruleParams: NotificationRuleTypeParams;
}
@@ -23,7 +23,7 @@ interface ScheduleNotificationActions {
export const scheduleNotificationActions = ({
alertInstance,
signalsCount,
- resultsLink,
+ resultsLink = '',
ruleParams,
}: ScheduleNotificationActions): AlertInstance =>
alertInstance
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts
index b8a3c4199c4f0c6..5dc7e7fc30b7fa6 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts
@@ -5,14 +5,17 @@
*/
export const getNotificationResultsLink = ({
- kibanaSiemAppUrl,
+ kibanaSiemAppUrl = '/app/siem',
id,
from,
to,
}: {
kibanaSiemAppUrl: string;
id: string;
- from: string;
- to: string;
-}) =>
- `${kibanaSiemAppUrl}#/detections/rules/id/${id}?timerange=(global:(linkTo:!(timeline),timerange:(from:${from},kind:absolute,to:${to})),timeline:(linkTo:!(global),timerange:(from:${from},kind:absolute,to:${to})))`;
+ from?: string;
+ to?: string;
+}) => {
+ if (from == null || to == null) return '';
+
+ return `${kibanaSiemAppUrl}#/detections/rules/id/${id}?timerange=(global:(linkTo:!(timeline),timerange:(from:${from},kind:absolute,to:${to})),timeline:(linkTo:!(global),timerange:(from:${from},kind:absolute,to:${to})))`;
+};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
index 06652028b374109..414270ffcdd5c8c 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
@@ -34,7 +34,7 @@ describe('searchAfterAndBulkCreate', () => {
test('if successful with empty search results', async () => {
const sampleParams = sampleRuleAlertParams();
- const { success } = await searchAfterAndBulkCreate({
+ const { success, createdSignalsCount } = await searchAfterAndBulkCreate({
someResult: sampleEmptyDocSearchResults(),
ruleParams: sampleParams,
services: mockService,
@@ -57,6 +57,7 @@ describe('searchAfterAndBulkCreate', () => {
});
expect(mockService.callCluster).toHaveBeenCalledTimes(0);
expect(success).toEqual(true);
+ expect(createdSignalsCount).toEqual(0);
});
test('if successful iteration of while loop with maxDocs', async () => {
@@ -70,6 +71,11 @@ describe('searchAfterAndBulkCreate', () => {
{
fakeItemValue: 'fakeItemKey',
},
+ {
+ create: {
+ status: 201,
+ },
+ },
],
})
.mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3)))
@@ -80,6 +86,11 @@ describe('searchAfterAndBulkCreate', () => {
{
fakeItemValue: 'fakeItemKey',
},
+ {
+ create: {
+ status: 201,
+ },
+ },
],
})
.mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6)))
@@ -90,9 +101,14 @@ describe('searchAfterAndBulkCreate', () => {
{
fakeItemValue: 'fakeItemKey',
},
+ {
+ create: {
+ status: 201,
+ },
+ },
],
});
- const { success } = await searchAfterAndBulkCreate({
+ const { success, createdSignalsCount } = await searchAfterAndBulkCreate({
someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)),
ruleParams: sampleParams,
services: mockService,
@@ -115,13 +131,14 @@ describe('searchAfterAndBulkCreate', () => {
});
expect(mockService.callCluster).toHaveBeenCalledTimes(5);
expect(success).toEqual(true);
+ expect(createdSignalsCount).toEqual(3);
});
test('if unsuccessful first bulk create', async () => {
const someGuids = Array.from({ length: 4 }).map(x => uuid.v4());
const sampleParams = sampleRuleAlertParams(10);
mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult);
- const { success } = await searchAfterAndBulkCreate({
+ const { success, createdSignalsCount } = await searchAfterAndBulkCreate({
someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
ruleParams: sampleParams,
services: mockService,
@@ -144,6 +161,7 @@ describe('searchAfterAndBulkCreate', () => {
});
expect(mockLogger.error).toHaveBeenCalled();
expect(success).toEqual(false);
+ expect(createdSignalsCount).toEqual(1);
});
test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => {
@@ -155,9 +173,14 @@ describe('searchAfterAndBulkCreate', () => {
{
fakeItemValue: 'fakeItemKey',
},
+ {
+ create: {
+ status: 201,
+ },
+ },
],
});
- const { success } = await searchAfterAndBulkCreate({
+ const { success, createdSignalsCount } = await searchAfterAndBulkCreate({
someResult: sampleDocSearchResultsNoSortId(),
ruleParams: sampleParams,
services: mockService,
@@ -180,6 +203,7 @@ describe('searchAfterAndBulkCreate', () => {
});
expect(mockLogger.error).toHaveBeenCalled();
expect(success).toEqual(false);
+ expect(createdSignalsCount).toEqual(1);
});
test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => {
@@ -191,9 +215,14 @@ describe('searchAfterAndBulkCreate', () => {
{
fakeItemValue: 'fakeItemKey',
},
+ {
+ create: {
+ status: 201,
+ },
+ },
],
});
- const { success } = await searchAfterAndBulkCreate({
+ const { success, createdSignalsCount } = await searchAfterAndBulkCreate({
someResult: sampleDocSearchResultsNoSortIdNoHits(),
ruleParams: sampleParams,
services: mockService,
@@ -215,6 +244,7 @@ describe('searchAfterAndBulkCreate', () => {
throttle: 'no_actions',
});
expect(success).toEqual(true);
+ expect(createdSignalsCount).toEqual(1);
});
test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => {
@@ -228,10 +258,15 @@ describe('searchAfterAndBulkCreate', () => {
{
fakeItemValue: 'fakeItemKey',
},
+ {
+ create: {
+ status: 201,
+ },
+ },
],
})
.mockReturnValueOnce(sampleDocSearchResultsNoSortId());
- const { success } = await searchAfterAndBulkCreate({
+ const { success, createdSignalsCount } = await searchAfterAndBulkCreate({
someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
ruleParams: sampleParams,
services: mockService,
@@ -253,6 +288,7 @@ describe('searchAfterAndBulkCreate', () => {
throttle: 'no_actions',
});
expect(success).toEqual(true);
+ expect(createdSignalsCount).toEqual(1);
});
test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => {
@@ -266,10 +302,15 @@ describe('searchAfterAndBulkCreate', () => {
{
fakeItemValue: 'fakeItemKey',
},
+ {
+ create: {
+ status: 201,
+ },
+ },
],
})
.mockReturnValueOnce(sampleEmptyDocSearchResults());
- const { success } = await searchAfterAndBulkCreate({
+ const { success, createdSignalsCount } = await searchAfterAndBulkCreate({
someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
ruleParams: sampleParams,
services: mockService,
@@ -291,6 +332,7 @@ describe('searchAfterAndBulkCreate', () => {
throttle: 'no_actions',
});
expect(success).toEqual(true);
+ expect(createdSignalsCount).toEqual(1);
});
test('if returns false when singleSearchAfter throws an exception', async () => {
@@ -304,12 +346,17 @@ describe('searchAfterAndBulkCreate', () => {
{
fakeItemValue: 'fakeItemKey',
},
+ {
+ create: {
+ status: 201,
+ },
+ },
],
})
.mockImplementation(() => {
throw Error('Fake Error');
});
- const { success } = await searchAfterAndBulkCreate({
+ const { success, createdSignalsCount } = await searchAfterAndBulkCreate({
someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
ruleParams: sampleParams,
services: mockService,
@@ -331,5 +378,6 @@ describe('searchAfterAndBulkCreate', () => {
throttle: 'no_actions',
});
expect(success).toEqual(false);
+ expect(createdSignalsCount).toEqual(1);
});
});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts
index a5d5dd0a7b71096..ff81730bc4a72bf 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts
@@ -39,6 +39,7 @@ export interface SearchAfterAndBulkCreateReturnType {
searchAfterTimes: string[];
bulkCreateTimes: string[];
lastLookBackDate: Date | null | undefined;
+ createdSignalsCount: number;
}
// search_after through documents and re-index using bulk endpoint.
@@ -68,6 +69,7 @@ export const searchAfterAndBulkCreate = async ({
searchAfterTimes: [],
bulkCreateTimes: [],
lastLookBackDate: null,
+ createdSignalsCount: 0,
};
if (someResult.hits.hits.length === 0) {
toReturn.success = true;
@@ -75,7 +77,7 @@ export const searchAfterAndBulkCreate = async ({
}
logger.debug('[+] starting bulk insertion');
- const { bulkCreateDuration } = await singleBulkCreate({
+ const { bulkCreateDuration, createdItemsCount } = await singleBulkCreate({
someResult,
ruleParams,
services,
@@ -97,6 +99,9 @@ export const searchAfterAndBulkCreate = async ({
someResult.hits.hits.length > 0
? new Date(someResult.hits.hits[someResult.hits.hits.length - 1]?._source['@timestamp'])
: null;
+ if (createdItemsCount) {
+ toReturn.createdSignalsCount = createdItemsCount;
+ }
if (bulkCreateDuration) {
toReturn.bulkCreateTimes.push(bulkCreateDuration);
}
@@ -156,7 +161,10 @@ export const searchAfterAndBulkCreate = async ({
}
sortId = sortIds[0];
logger.debug('next bulk index');
- const { bulkCreateDuration: bulkDuration } = await singleBulkCreate({
+ const {
+ bulkCreateDuration: bulkDuration,
+ createdItemsCount: createdCount,
+ } = await singleBulkCreate({
someResult: searchResult,
ruleParams,
services,
@@ -175,6 +183,7 @@ export const searchAfterAndBulkCreate = async ({
throttle,
});
logger.debug('finished next bulk index');
+ toReturn.createdSignalsCount += createdCount;
if (bulkDuration) {
toReturn.bulkCreateTimes.push(bulkDuration);
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
new file mode 100644
index 000000000000000..11d31f1805440f7
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
@@ -0,0 +1,399 @@
+/*
+ * 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 moment from 'moment';
+import { savedObjectsClientMock } from 'src/core/server/mocks';
+import { loggerMock } from 'src/core/server/logging/logger.mock';
+import { getResult, getMlResult } from '../routes/__mocks__/request_responses';
+import { signalRulesAlertType } from './signal_rule_alert_type';
+import { AlertInstance } from '../../../../../../../plugins/alerting/server';
+import { ruleStatusServiceFactory } from './rule_status_service';
+import { getGapBetweenRuns } from './utils';
+import { RuleExecutorOptions } from './types';
+import { searchAfterAndBulkCreate } from './search_after_bulk_create';
+import { scheduleNotificationActions } from '../notifications/schedule_notification_actions';
+import { RuleAlertType } from '../rules/types';
+import { findMlSignals } from './find_ml_signals';
+import { bulkCreateMlSignals } from './bulk_create_ml_signals';
+
+jest.mock('./rule_status_saved_objects_client');
+jest.mock('./rule_status_service');
+jest.mock('./search_after_bulk_create');
+jest.mock('./get_filter');
+jest.mock('./utils');
+jest.mock('../notifications/schedule_notification_actions');
+jest.mock('./find_ml_signals');
+jest.mock('./bulk_create_ml_signals');
+
+const getPayload = (
+ ruleAlert: RuleAlertType,
+ alertInstanceFactoryMock: () => AlertInstance,
+ savedObjectsClient: ReturnType,
+ callClusterMock: jest.Mock
+) => ({
+ alertId: ruleAlert.id,
+ services: {
+ savedObjectsClient,
+ alertInstanceFactory: alertInstanceFactoryMock,
+ callCluster: callClusterMock,
+ },
+ params: {
+ ...ruleAlert.params,
+ actions: [],
+ enabled: ruleAlert.enabled,
+ interval: ruleAlert.schedule.interval,
+ name: ruleAlert.name,
+ tags: ruleAlert.tags,
+ throttle: ruleAlert.throttle!,
+ scrollSize: 10,
+ scrollLock: '0',
+ },
+ state: {},
+ spaceId: '',
+ name: 'name',
+ tags: [],
+ startedAt: new Date('2019-12-13T16:50:33.400Z'),
+ previousStartedAt: new Date('2019-12-13T16:40:33.400Z'),
+ createdBy: 'elastic',
+ updatedBy: 'elastic',
+});
+
+describe('rules_notification_alert_type', () => {
+ const version = '8.0.0';
+ const jobsSummaryMock = jest.fn();
+ const mlMock = {
+ mlClient: {
+ callAsInternalUser: jest.fn(),
+ close: jest.fn(),
+ asScoped: jest.fn(),
+ },
+ jobServiceProvider: jest.fn().mockReturnValue({
+ jobsSummary: jobsSummaryMock,
+ }),
+ anomalyDetectorsProvider: jest.fn(),
+ mlSystemProvider: jest.fn(),
+ modulesProvider: jest.fn(),
+ resultsServiceProvider: jest.fn(),
+ };
+ let payload: RuleExecutorOptions;
+ let alert: ReturnType;
+ let alertInstanceMock: Record;
+ let alertInstanceFactoryMock: () => AlertInstance;
+ let savedObjectsClient: ReturnType;
+ let logger: ReturnType;
+ let callClusterMock: jest.Mock;
+ let ruleStatusService: Record;
+
+ beforeEach(() => {
+ alertInstanceMock = {
+ scheduleActions: jest.fn(),
+ replaceState: jest.fn(),
+ };
+ alertInstanceMock.replaceState.mockReturnValue(alertInstanceMock);
+ alertInstanceFactoryMock = jest.fn().mockReturnValue(alertInstanceMock);
+ callClusterMock = jest.fn();
+ savedObjectsClient = savedObjectsClientMock.create();
+ logger = loggerMock.create();
+ ruleStatusService = {
+ success: jest.fn(),
+ find: jest.fn(),
+ goingToRun: jest.fn(),
+ error: jest.fn(),
+ };
+ (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService);
+ (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0));
+ (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({
+ success: true,
+ searchAfterTimes: [],
+ createdSignalsCount: 10,
+ });
+ callClusterMock.mockResolvedValue({
+ hits: {
+ total: { value: 10 },
+ },
+ });
+ const ruleAlert = getResult();
+ savedObjectsClient.get.mockResolvedValue({
+ id: 'id',
+ type: 'type',
+ references: [],
+ attributes: ruleAlert,
+ });
+
+ payload = getPayload(ruleAlert, alertInstanceFactoryMock, savedObjectsClient, callClusterMock);
+
+ alert = signalRulesAlertType({
+ logger,
+ version,
+ ml: mlMock,
+ });
+ });
+
+ describe('executor', () => {
+ it('should warn about the gap between runs', async () => {
+ (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(1000));
+ await alert.executor(payload);
+ expect(logger.warn).toHaveBeenCalled();
+ expect(logger.warn.mock.calls[0][0]).toContain(
+ 'a few seconds (1000ms) has passed since last rule execution, and signals may have been missed.'
+ );
+ expect(ruleStatusService.error).toHaveBeenCalled();
+ expect(ruleStatusService.error.mock.calls[0][0]).toContain(
+ 'a few seconds (1000ms) has passed since last rule execution, and signals may have been missed.'
+ );
+ expect(ruleStatusService.error.mock.calls[0][1]).toEqual({
+ gap: 'a few seconds',
+ });
+ });
+
+ it('should call scheduleActions if signalsCount was greater than 0 and rule has actions defined', async () => {
+ const ruleAlert = getResult();
+ ruleAlert.actions = [
+ {
+ actionTypeId: '.slack',
+ params: {
+ message:
+ 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}',
+ },
+ group: 'default',
+ id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
+ },
+ ];
+
+ savedObjectsClient.get.mockResolvedValue({
+ id: 'id',
+ type: 'type',
+ references: [],
+ attributes: ruleAlert,
+ });
+
+ await alert.executor(payload);
+
+ expect(scheduleNotificationActions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ signalsCount: 10,
+ })
+ );
+ });
+
+ describe('ML rule', () => {
+ it('should throw an error if ML plugin was not available', async () => {
+ const ruleAlert = getMlResult();
+ payload = getPayload(
+ ruleAlert,
+ alertInstanceFactoryMock,
+ savedObjectsClient,
+ callClusterMock
+ );
+ alert = signalRulesAlertType({
+ logger,
+ version,
+ ml: undefined,
+ });
+ await alert.executor(payload);
+ expect(logger.error).toHaveBeenCalled();
+ expect(logger.error.mock.calls[0][0]).toContain(
+ 'ML plugin unavailable during rule execution'
+ );
+ });
+
+ it('should throw an error if machineLearningJobId or anomalyThreshold was not null', async () => {
+ const ruleAlert = getMlResult();
+ ruleAlert.params.anomalyThreshold = undefined;
+ payload = getPayload(
+ ruleAlert,
+ alertInstanceFactoryMock,
+ savedObjectsClient,
+ callClusterMock
+ );
+ await alert.executor(payload);
+ expect(logger.error).toHaveBeenCalled();
+ expect(logger.error.mock.calls[0][0]).toContain(
+ 'Machine learning rule is missing job id and/or anomaly threshold'
+ );
+ });
+
+ it('should throw an error if Machine learning job summary was null', async () => {
+ const ruleAlert = getMlResult();
+ payload = getPayload(
+ ruleAlert,
+ alertInstanceFactoryMock,
+ savedObjectsClient,
+ callClusterMock
+ );
+ jobsSummaryMock.mockResolvedValue([]);
+ await alert.executor(payload);
+ expect(logger.warn).toHaveBeenCalled();
+ expect(logger.warn.mock.calls[0][0]).toContain('Machine learning job is not started');
+ expect(ruleStatusService.error).toHaveBeenCalled();
+ expect(ruleStatusService.error.mock.calls[0][0]).toContain(
+ 'Machine learning job is not started'
+ );
+ });
+
+ it('should log an error if Machine learning job was not started', async () => {
+ const ruleAlert = getMlResult();
+ payload = getPayload(
+ ruleAlert,
+ alertInstanceFactoryMock,
+ savedObjectsClient,
+ callClusterMock
+ );
+ jobsSummaryMock.mockResolvedValue([
+ {
+ id: 'some_job_id',
+ jobState: 'starting',
+ datafeedState: 'started',
+ },
+ ]);
+ (findMlSignals as jest.Mock).mockResolvedValue({
+ hits: {
+ hits: [],
+ },
+ });
+ await alert.executor(payload);
+ expect(logger.warn).toHaveBeenCalled();
+ expect(logger.warn.mock.calls[0][0]).toContain('Machine learning job is not started');
+ expect(ruleStatusService.error).toHaveBeenCalled();
+ expect(ruleStatusService.error.mock.calls[0][0]).toContain(
+ 'Machine learning job is not started'
+ );
+ });
+
+ it('should not call ruleStatusService.success if no anomalies were found', async () => {
+ const ruleAlert = getMlResult();
+ payload = getPayload(
+ ruleAlert,
+ alertInstanceFactoryMock,
+ savedObjectsClient,
+ callClusterMock
+ );
+ jobsSummaryMock.mockResolvedValue([]);
+ (findMlSignals as jest.Mock).mockResolvedValue({
+ hits: {
+ hits: [],
+ },
+ });
+ (bulkCreateMlSignals as jest.Mock).mockResolvedValue({
+ success: true,
+ bulkCreateDuration: 0,
+ createdItemsCount: 0,
+ });
+ await alert.executor(payload);
+ expect(ruleStatusService.success).not.toHaveBeenCalled();
+ });
+
+ it('should call ruleStatusService.success if signals were created', async () => {
+ const ruleAlert = getMlResult();
+ payload = getPayload(
+ ruleAlert,
+ alertInstanceFactoryMock,
+ savedObjectsClient,
+ callClusterMock
+ );
+ jobsSummaryMock.mockResolvedValue([
+ {
+ id: 'some_job_id',
+ jobState: 'started',
+ datafeedState: 'started',
+ },
+ ]);
+ (findMlSignals as jest.Mock).mockResolvedValue({
+ hits: {
+ hits: [{}],
+ },
+ });
+ (bulkCreateMlSignals as jest.Mock).mockResolvedValue({
+ success: true,
+ bulkCreateDuration: 1,
+ createdItemsCount: 1,
+ });
+ await alert.executor(payload);
+ expect(ruleStatusService.success).toHaveBeenCalled();
+ });
+
+ it('should call scheduleActions if signalsCount was greater than 0 and rule has actions defined', async () => {
+ const ruleAlert = getMlResult();
+ ruleAlert.actions = [
+ {
+ actionTypeId: '.slack',
+ params: {
+ message:
+ 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}',
+ },
+ group: 'default',
+ id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
+ },
+ ];
+ payload = getPayload(
+ ruleAlert,
+ alertInstanceFactoryMock,
+ savedObjectsClient,
+ callClusterMock
+ );
+ savedObjectsClient.get.mockResolvedValue({
+ id: 'id',
+ type: 'type',
+ references: [],
+ attributes: ruleAlert,
+ });
+ jobsSummaryMock.mockResolvedValue([]);
+ (findMlSignals as jest.Mock).mockResolvedValue({
+ hits: {
+ hits: [{}],
+ },
+ });
+ (bulkCreateMlSignals as jest.Mock).mockResolvedValue({
+ success: true,
+ bulkCreateDuration: 1,
+ createdItemsCount: 1,
+ });
+
+ await alert.executor(payload);
+
+ expect(scheduleNotificationActions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ signalsCount: 1,
+ })
+ );
+ });
+ });
+ });
+
+ describe('should catch error', () => {
+ it('when bulk indexing failed', async () => {
+ (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({
+ success: false,
+ searchAfterTimes: [],
+ bulkCreateTimes: [],
+ lastLookBackDate: null,
+ createdSignalsCount: 0,
+ });
+ await alert.executor(payload);
+ expect(logger.error).toHaveBeenCalled();
+ expect(logger.error.mock.calls[0][0]).toContain(
+ 'Bulk Indexing of signals failed. Check logs for further details.'
+ );
+ expect(ruleStatusService.error).toHaveBeenCalled();
+ });
+
+ it('when error was thrown', async () => {
+ (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({});
+ await alert.executor(payload);
+ expect(logger.error).toHaveBeenCalled();
+ expect(logger.error.mock.calls[0][0]).toContain('An error occurred during rule execution');
+ expect(ruleStatusService.error).toHaveBeenCalled();
+ });
+
+ it('and call ruleStatusService with the default message', async () => {
+ (searchAfterAndBulkCreate as jest.Mock).mockRejectedValue({});
+ await alert.executor(payload);
+ expect(logger.error).toHaveBeenCalled();
+ expect(logger.error.mock.calls[0][0]).toContain('An error occurred during rule execution');
+ expect(ruleStatusService.error).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index 246701e94c99a28..417fcbbe42a565f 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -19,16 +19,16 @@ import {
} from './search_after_bulk_create';
import { getFilter } from './get_filter';
import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types';
-import { getGapBetweenRuns, makeFloatString } from './utils';
+import { getGapBetweenRuns, makeFloatString, parseScheduleDates } from './utils';
import { signalParamsSchema } from './signal_params_schema';
import { siemRuleActionGroups } from './siem_rule_action_groups';
import { findMlSignals } from './find_ml_signals';
import { bulkCreateMlSignals } from './bulk_create_ml_signals';
-import { getSignalsCount } from '../notifications/get_signals_count';
import { scheduleNotificationActions } from '../notifications/schedule_notification_actions';
import { ruleStatusServiceFactory } from './rule_status_service';
import { buildRuleMessageFactory } from './rule_messages';
import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client';
+import { getNotificationResultsLink } from '../notifications/utils';
export const signalRulesAlertType = ({
logger,
@@ -71,6 +71,7 @@ export const signalRulesAlertType = ({
bulkCreateTimes: [],
searchAfterTimes: [],
lastLookBackDate: null,
+ createdSignalsCount: 0,
};
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(services.savedObjectsClient);
const ruleStatusService = await ruleStatusServiceFactory({
@@ -161,7 +162,7 @@ export const signalRulesAlertType = ({
logger.info(buildRuleMessage(`Found ${anomalyCount} signals from ML anomalies.`));
}
- const { success, bulkCreateDuration } = await bulkCreateMlSignals({
+ const { success, bulkCreateDuration, createdItemsCount } = await bulkCreateMlSignals({
actions,
throttle,
someResult: anomalyResults,
@@ -180,6 +181,7 @@ export const signalRulesAlertType = ({
tags,
});
result.success = success;
+ result.createdSignalsCount = createdItemsCount;
if (bulkCreateDuration) {
result.bulkCreateTimes.push(bulkCreateDuration);
}
@@ -249,23 +251,26 @@ export const signalRulesAlertType = ({
name,
id: savedObject.id,
};
- const { signalsCount, resultsLink } = await getSignalsCount({
- from: `now-${interval}`,
- to: 'now',
- index: ruleParams.outputIndex,
- ruleId: ruleParams.ruleId!,
+
+ const fromInMs = parseScheduleDates(`now-${interval}`)?.format('x');
+ const toInMs = parseScheduleDates('now')?.format('x');
+
+ const resultsLink = getNotificationResultsLink({
+ from: fromInMs,
+ to: toInMs,
+ id: savedObject.id,
kibanaSiemAppUrl: meta?.kibanaSiemAppUrl as string,
- ruleAlertId: savedObject.id,
- callCluster: services.callCluster,
});
- logger.info(buildRuleMessage(`Found ${signalsCount} signals for notification.`));
+ logger.info(
+ buildRuleMessage(`Found ${result.createdSignalsCount} signals for notification.`)
+ );
- if (signalsCount) {
+ if (result.createdSignalsCount) {
const alertInstance = services.alertInstanceFactory(alertId);
scheduleNotificationActions({
alertInstance,
- signalsCount,
+ signalsCount: result.createdSignalsCount,
resultsLink,
ruleParams: notificationRuleParams,
});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts
index 45b5610e2d3c3f9..56f061cdfa3ca97 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts
@@ -144,7 +144,7 @@ describe('singleBulkCreate', () => {
},
],
});
- const { success } = await singleBulkCreate({
+ const { success, createdItemsCount } = await singleBulkCreate({
someResult: sampleDocSearchResultsNoSortId(),
ruleParams: sampleParams,
services: mockService,
@@ -163,6 +163,7 @@ describe('singleBulkCreate', () => {
throttle: 'no_actions',
});
expect(success).toEqual(true);
+ expect(createdItemsCount).toEqual(0);
});
test('create successful bulk create with docs with no versioning', async () => {
@@ -176,7 +177,7 @@ describe('singleBulkCreate', () => {
},
],
});
- const { success } = await singleBulkCreate({
+ const { success, createdItemsCount } = await singleBulkCreate({
someResult: sampleDocSearchResultsNoSortIdNoVersion(),
ruleParams: sampleParams,
services: mockService,
@@ -195,12 +196,13 @@ describe('singleBulkCreate', () => {
throttle: 'no_actions',
});
expect(success).toEqual(true);
+ expect(createdItemsCount).toEqual(0);
});
test('create unsuccessful bulk create due to empty search results', async () => {
const sampleParams = sampleRuleAlertParams();
mockService.callCluster.mockReturnValue(false);
- const { success } = await singleBulkCreate({
+ const { success, createdItemsCount } = await singleBulkCreate({
someResult: sampleEmptyDocSearchResults(),
ruleParams: sampleParams,
services: mockService,
@@ -219,13 +221,14 @@ describe('singleBulkCreate', () => {
throttle: 'no_actions',
});
expect(success).toEqual(true);
+ expect(createdItemsCount).toEqual(0);
});
test('create successful bulk create when bulk create has duplicate errors', async () => {
const sampleParams = sampleRuleAlertParams();
const sampleSearchResult = sampleDocSearchResultsNoSortId;
mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult);
- const { success } = await singleBulkCreate({
+ const { success, createdItemsCount } = await singleBulkCreate({
someResult: sampleSearchResult(),
ruleParams: sampleParams,
services: mockService,
@@ -246,13 +249,14 @@ describe('singleBulkCreate', () => {
expect(mockLogger.error).not.toHaveBeenCalled();
expect(success).toEqual(true);
+ expect(createdItemsCount).toEqual(1);
});
test('create successful bulk create when bulk create has multiple error statuses', async () => {
const sampleParams = sampleRuleAlertParams();
const sampleSearchResult = sampleDocSearchResultsNoSortId;
mockService.callCluster.mockReturnValue(sampleBulkCreateErrorResult);
- const { success } = await singleBulkCreate({
+ const { success, createdItemsCount } = await singleBulkCreate({
someResult: sampleSearchResult(),
ruleParams: sampleParams,
services: mockService,
@@ -273,6 +277,7 @@ describe('singleBulkCreate', () => {
expect(mockLogger.error).toHaveBeenCalled();
expect(success).toEqual(true);
+ expect(createdItemsCount).toEqual(1);
});
test('filter duplicate rules will return an empty array given an empty array', () => {
@@ -341,4 +346,29 @@ describe('singleBulkCreate', () => {
},
]);
});
+
+ test('create successful and returns proper createdItemsCount', async () => {
+ const sampleParams = sampleRuleAlertParams();
+ mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult);
+ const { success, createdItemsCount } = await singleBulkCreate({
+ someResult: sampleDocSearchResultsNoSortId(),
+ ruleParams: sampleParams,
+ services: mockService,
+ logger: mockLogger,
+ id: sampleRuleGuid,
+ signalsIndex: DEFAULT_SIGNALS_INDEX,
+ actions: [],
+ name: 'rule-name',
+ createdAt: '2020-01-28T15:58:34.810Z',
+ updatedAt: '2020-01-28T15:59:14.004Z',
+ createdBy: 'elastic',
+ updatedBy: 'elastic',
+ interval: '5m',
+ enabled: true,
+ tags: ['some fake tag 1', 'some fake tag 2'],
+ throttle: 'no_actions',
+ });
+ expect(success).toEqual(true);
+ expect(createdItemsCount).toEqual(1);
+ });
});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts
index ffec40b839bf649..6dd8823b57e4de0 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts
@@ -58,6 +58,7 @@ export const filterDuplicateRules = (
export interface SingleBulkCreateResponse {
success: boolean;
bulkCreateDuration?: string;
+ createdItemsCount: number;
}
// Bulk Index documents.
@@ -81,7 +82,7 @@ export const singleBulkCreate = async ({
}: SingleBulkCreateParams): Promise => {
someResult.hits.hits = filterDuplicateRules(id, someResult);
if (someResult.hits.hits.length === 0) {
- return { success: true };
+ return { success: true, createdItemsCount: 0 };
}
// index documents after creating an ID based on the
// source documents' originating index, and the original
@@ -145,5 +146,8 @@ export const singleBulkCreate = async ({
);
}
}
- return { success: true, bulkCreateDuration: makeFloatString(end - start) };
+
+ const createdItemsCount = countBy(response.items, 'create.status')['201'] ?? 0;
+
+ return { success: true, bulkCreateDuration: makeFloatString(end - start), createdItemsCount };
};
From dfa083dc6041edbe584ca58618ecb9fe2f81d81e Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Mon, 6 Apr 2020 13:45:46 -0400
Subject: [PATCH 14/15] Prep for embed saved object refactor + helper (#62486)
---
.../list_container/embeddable_list_item.tsx | 64 ---------------
.../public/list_container/list_container.tsx | 19 ++---
.../list_container_component.tsx | 26 ++++--
.../list_container/list_container_factory.ts | 6 +-
.../multi_task_todo_component.tsx | 17 ++--
.../multi_task_todo_embeddable.tsx | 27 +++----
examples/embeddable_examples/public/plugin.ts | 7 +-
.../searchable_list_container.tsx | 16 ++--
.../searchable_list_container_component.tsx | 79 +++++++++++++------
.../searchable_list_container_factory.ts | 6 +-
.../public/todo/todo_component.tsx | 10 ++-
examples/embeddable_explorer/public/app.tsx | 13 +--
.../public/embeddable_panel_example.tsx | 49 ++----------
.../public/list_container_example.tsx | 10 ++-
.../embeddable_child_panel.test.tsx | 2 +-
.../lib/embeddables/with_subscription.tsx | 12 +--
.../lib/panel/embeddable_panel.test.tsx | 4 +-
.../inspect_panel_action.test.tsx | 2 +-
src/plugins/embeddable/public/mocks.ts | 10 ++-
.../public/{plugin.ts => plugin.tsx} | 54 ++++++++++---
.../public/tests/apply_filter_action.test.ts | 2 +-
.../embeddable/public/tests/test_plugin.ts | 11 ++-
test/examples/embeddables/list_container.ts | 9 +--
.../public/np_ready/public/app/app.tsx | 34 ++------
.../app/dashboard_container_example.tsx | 33 ++------
.../public/np_ready/public/plugin.tsx | 15 +---
26 files changed, 234 insertions(+), 303 deletions(-)
delete mode 100644 examples/embeddable_examples/public/list_container/embeddable_list_item.tsx
rename src/plugins/embeddable/public/{plugin.ts => plugin.tsx} (76%)
diff --git a/examples/embeddable_examples/public/list_container/embeddable_list_item.tsx b/examples/embeddable_examples/public/list_container/embeddable_list_item.tsx
deleted file mode 100644
index 2c80cef8a636486..000000000000000
--- a/examples/embeddable_examples/public/list_container/embeddable_list_item.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import React from 'react';
-import { EuiPanel, EuiLoadingSpinner, EuiFlexItem } from '@elastic/eui';
-import { IEmbeddable } from '../../../../src/plugins/embeddable/public';
-
-interface Props {
- embeddable: IEmbeddable;
-}
-
-export class EmbeddableListItem extends React.Component {
- private embeddableRoot: React.RefObject;
- private rendered = false;
-
- constructor(props: Props) {
- super(props);
- this.embeddableRoot = React.createRef();
- }
-
- public componentDidMount() {
- if (this.embeddableRoot.current && this.props.embeddable) {
- this.props.embeddable.render(this.embeddableRoot.current);
- this.rendered = true;
- }
- }
-
- public componentDidUpdate() {
- if (this.embeddableRoot.current && this.props.embeddable && !this.rendered) {
- this.props.embeddable.render(this.embeddableRoot.current);
- this.rendered = true;
- }
- }
-
- public render() {
- return (
-
-
- {this.props.embeddable ? (
-
- ) : (
-
- )}
-
-
- );
- }
-}
diff --git a/examples/embeddable_examples/public/list_container/list_container.tsx b/examples/embeddable_examples/public/list_container/list_container.tsx
index bbbd0d6e3230460..9e7bec7a1c951ec 100644
--- a/examples/embeddable_examples/public/list_container/list_container.tsx
+++ b/examples/embeddable_examples/public/list_container/list_container.tsx
@@ -31,16 +31,14 @@ export class ListContainer extends Container<{}, ContainerInput> {
public readonly type = LIST_CONTAINER;
private node?: HTMLElement;
- constructor(
- input: ContainerInput,
- getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']
- ) {
- super(input, { embeddableLoaded: {} }, getEmbeddableFactory);
+ constructor(input: ContainerInput, private embeddableServices: EmbeddableStart) {
+ super(input, { embeddableLoaded: {} }, embeddableServices.getEmbeddableFactory);
}
- // This container has no input itself.
- getInheritedInput(id: string) {
- return {};
+ getInheritedInput() {
+ return {
+ viewMode: this.input.viewMode,
+ };
}
public render(node: HTMLElement) {
@@ -48,7 +46,10 @@ export class ListContainer extends Container<{}, ContainerInput> {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
- ReactDOM.render(, node);
+ ReactDOM.render(
+ ,
+ node
+ );
}
public destroy() {
diff --git a/examples/embeddable_examples/public/list_container/list_container_component.tsx b/examples/embeddable_examples/public/list_container/list_container_component.tsx
index f6e04933ee8971a..da27889a276036d 100644
--- a/examples/embeddable_examples/public/list_container/list_container_component.tsx
+++ b/examples/embeddable_examples/public/list_container/list_container_component.tsx
@@ -24,30 +24,35 @@ import {
withEmbeddableSubscription,
ContainerInput,
ContainerOutput,
+ EmbeddableStart,
} from '../../../../src/plugins/embeddable/public';
-import { EmbeddableListItem } from './embeddable_list_item';
interface Props {
embeddable: IContainer;
input: ContainerInput;
output: ContainerOutput;
+ embeddableServices: EmbeddableStart;
}
-function renderList(embeddable: IContainer, panels: ContainerInput['panels']) {
+function renderList(
+ embeddable: IContainer,
+ panels: ContainerInput['panels'],
+ embeddableServices: EmbeddableStart
+) {
let number = 0;
const list = Object.values(panels).map(panel => {
const child = embeddable.getChild(panel.explicitInput.id);
number++;
return (
-
+
{number}
-
+
@@ -56,12 +61,12 @@ function renderList(embeddable: IContainer, panels: ContainerInput['panels']) {
return list;
}
-export function ListContainerComponentInner(props: Props) {
+export function ListContainerComponentInner({ embeddable, input, embeddableServices }: Props) {
return (
-
{props.embeddable.getTitle()}
+ {embeddable.getTitle()}
- {renderList(props.embeddable, props.input.panels)}
+ {renderList(embeddable, input.panels, embeddableServices)}
);
}
@@ -71,4 +76,9 @@ export function ListContainerComponentInner(props: Props) {
// anything on input or output state changes. If you don't want that to happen (for example
// if you expect something on input or output state to change frequently that your react
// component does not care about, then you should probably hook this up manually).
-export const ListContainerComponent = withEmbeddableSubscription(ListContainerComponentInner);
+export const ListContainerComponent = withEmbeddableSubscription<
+ ContainerInput,
+ ContainerOutput,
+ IContainer,
+ { embeddableServices: EmbeddableStart }
+>(ListContainerComponentInner);
diff --git a/examples/embeddable_examples/public/list_container/list_container_factory.ts b/examples/embeddable_examples/public/list_container/list_container_factory.ts
index 1fde254110c624f..02a024b95349fa8 100644
--- a/examples/embeddable_examples/public/list_container/list_container_factory.ts
+++ b/examples/embeddable_examples/public/list_container/list_container_factory.ts
@@ -26,7 +26,7 @@ import {
import { LIST_CONTAINER, ListContainer } from './list_container';
interface StartServices {
- getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
+ embeddableServices: EmbeddableStart;
}
export class ListContainerFactory implements EmbeddableFactoryDefinition {
@@ -40,8 +40,8 @@ export class ListContainerFactory implements EmbeddableFactoryDefinition {
}
public create = async (initialInput: ContainerInput) => {
- const { getEmbeddableFactory } = await this.getStartServices();
- return new ListContainer(initialInput, getEmbeddableFactory);
+ const { embeddableServices } = await this.getStartServices();
+ return new ListContainer(initialInput, embeddableServices);
};
public getDisplayName() {
diff --git a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx
index e33dfab0eaf4a2e..b2882c97ef501ec 100644
--- a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx
+++ b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx
@@ -54,7 +54,7 @@ function wrapSearchTerms(task: string, search?: string) {
);
}
-function renderTasks(tasks: MultiTaskTodoOutput['tasks'], search?: string) {
+function renderTasks(tasks: MultiTaskTodoInput['tasks'], search?: string) {
return tasks.map(task => (
+
{icon ? : }
-
+
{wrapSearchTerms(title, search)}
@@ -89,6 +88,8 @@ export function MultiTaskTodoEmbeddableComponentInner({
);
}
-export const MultiTaskTodoEmbeddableComponent = withEmbeddableSubscription(
- MultiTaskTodoEmbeddableComponentInner
-);
+export const MultiTaskTodoEmbeddableComponent = withEmbeddableSubscription<
+ MultiTaskTodoInput,
+ MultiTaskTodoOutput,
+ MultiTaskTodoEmbeddable
+>(MultiTaskTodoEmbeddableComponentInner);
diff --git a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable.tsx b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable.tsx
index a2197c9c06fe946..a9e58c5538107b1 100644
--- a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable.tsx
+++ b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable.tsx
@@ -36,30 +36,27 @@ export interface MultiTaskTodoInput extends EmbeddableInput {
title: string;
}
-// This embeddable has output! It's the tasks list that is filtered.
-// Output state is something only the embeddable itself can update. It
-// can be something completely internal, or it can be state that is
+// This embeddable has output! Output state is something only the embeddable itself
+// can update. It can be something completely internal, or it can be state that is
// derived from input state and updates when input does.
export interface MultiTaskTodoOutput extends EmbeddableOutput {
- tasks: string[];
+ hasMatch: boolean;
}
-function getFilteredTasks(tasks: string[], search?: string) {
- const filteredTasks: string[] = [];
- if (search === undefined) return tasks;
+function getHasMatch(tasks: string[], title?: string, search?: string) {
+ if (search === undefined || search === '') return false;
- tasks.forEach(task => {
- if (task.match(search)) {
- filteredTasks.push(task);
- }
- });
+ if (title && title.match(search)) return true;
+
+ const match = tasks.find(task => task.match(search));
+ if (match) return true;
- return filteredTasks;
+ return false;
}
function getOutput(input: MultiTaskTodoInput) {
- const tasks = getFilteredTasks(input.tasks, input.search);
- return { tasks, hasMatch: tasks.length > 0 || (input.search && input.title.match(input.search)) };
+ const hasMatch = getHasMatch(input.tasks, input.title, input.search);
+ return { hasMatch };
}
export class MultiTaskTodoEmbeddable extends Embeddable {
diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts
index 5c202d96ceb1a25..31a3037332dda9c 100644
--- a/examples/embeddable_examples/public/plugin.ts
+++ b/examples/embeddable_examples/public/plugin.ts
@@ -53,20 +53,17 @@ export class EmbeddableExamplesPlugin
new MultiTaskTodoEmbeddableFactory()
);
- // These are registered in the start method because `getEmbeddableFactory `
- // is only available in start. We could reconsider this I think and make it
- // available in both.
deps.embeddable.registerEmbeddableFactory(
SEARCHABLE_LIST_CONTAINER,
new SearchableListContainerFactory(async () => ({
- getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory,
+ embeddableServices: (await core.getStartServices())[1].embeddable,
}))
);
deps.embeddable.registerEmbeddableFactory(
LIST_CONTAINER,
new ListContainerFactory(async () => ({
- getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory,
+ embeddableServices: (await core.getStartServices())[1].embeddable,
}))
);
diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx
index 06462937c768d79..f6efb0b722c4c4c 100644
--- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx
+++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx
@@ -40,11 +40,8 @@ export class SearchableListContainer extends Container, node);
+ ReactDOM.render(
+ ,
+ node
+ );
}
public destroy() {
diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx
index b79f86e2a0192d9..49dbce74788bfa8 100644
--- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx
+++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx
@@ -34,14 +34,15 @@ import {
withEmbeddableSubscription,
ContainerOutput,
EmbeddableOutput,
+ EmbeddableStart,
} from '../../../../src/plugins/embeddable/public';
-import { EmbeddableListItem } from '../list_container/embeddable_list_item';
import { SearchableListContainer, SearchableContainerInput } from './searchable_list_container';
interface Props {
embeddable: SearchableListContainer;
input: SearchableContainerInput;
output: ContainerOutput;
+ embeddableServices: EmbeddableStart;
}
interface State {
@@ -111,13 +112,27 @@ export class SearchableListContainerComponentInner extends Component {
+ const { input, embeddable } = this.props;
+ const checked: { [key: string]: boolean } = {};
+ Object.values(input.panels).map(panel => {
+ const child = embeddable.getChild(panel.explicitInput.id);
+ const output = child.getOutput();
+ if (hasHasMatchOutput(output) && output.hasMatch) {
+ checked[panel.explicitInput.id] = true;
+ }
+ });
+ this.setState({ checked });
+ };
+
private toggleCheck = (isChecked: boolean, id: string) => {
this.setState(prevState => ({ checked: { ...prevState.checked, [id]: isChecked } }));
};
public renderControls() {
+ const { input } = this.props;
return (
-
+
this.deleteChecked()}>
@@ -125,6 +140,17 @@ export class SearchableListContainerComponentInner extends Component
+
+
+ this.checkMatching()}
+ >
+ Check matching
+
+
+
- {embeddable.getTitle()}
-
- {this.renderControls()}
-
- {this.renderList()}
-
+
+
+ {embeddable.getTitle()}
+
+ {this.renderControls()}
+
+ {this.renderList()}
+
+
);
}
private renderList() {
+ const { embeddableServices, input, embeddable } = this.props;
let id = 0;
- const list = Object.values(this.props.input.panels).map(panel => {
- const embeddable = this.props.embeddable.getChild(panel.explicitInput.id);
- if (this.props.input.search && !this.state.hasMatch[panel.explicitInput.id]) return;
+ const list = Object.values(input.panels).map(panel => {
+ const childEmbeddable = embeddable.getChild(panel.explicitInput.id);
id++;
- return embeddable ? (
-
-
+ return childEmbeddable ? (
+
+
this.toggleCheck(e.target.checked, embeddable.id)}
+ data-test-subj={`todoCheckBox-${childEmbeddable.id}`}
+ disabled={!childEmbeddable}
+ id={childEmbeddable ? childEmbeddable.id : ''}
+ checked={this.state.checked[childEmbeddable.id]}
+ onChange={e => this.toggleCheck(e.target.checked, childEmbeddable.id)}
/>
-
+
@@ -183,6 +211,9 @@ export class SearchableListContainerComponentInner extends Component(SearchableListContainerComponentInner);
diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts
index 382bb65e769ef24..34ea43c29462a8e 100644
--- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts
+++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts
@@ -29,7 +29,7 @@ import {
} from './searchable_list_container';
interface StartServices {
- getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
+ embeddableServices: EmbeddableStart;
}
export class SearchableListContainerFactory implements EmbeddableFactoryDefinition {
@@ -43,8 +43,8 @@ export class SearchableListContainerFactory implements EmbeddableFactoryDefiniti
}
public create = async (initialInput: SearchableContainerInput) => {
- const { getEmbeddableFactory } = await this.getStartServices();
- return new SearchableListContainer(initialInput, getEmbeddableFactory);
+ const { embeddableServices } = await this.getStartServices();
+ return new SearchableListContainer(initialInput, embeddableServices);
};
public getDisplayName() {
diff --git a/examples/embeddable_examples/public/todo/todo_component.tsx b/examples/embeddable_examples/public/todo/todo_component.tsx
index fbebfc98627b583..a4593bea3cc5e5e 100644
--- a/examples/embeddable_examples/public/todo/todo_component.tsx
+++ b/examples/embeddable_examples/public/todo/todo_component.tsx
@@ -51,12 +51,12 @@ function wrapSearchTerms(task: string, search?: string) {
export function TodoEmbeddableComponentInner({ input: { icon, title, task, search } }: Props) {
return (
-
+
{icon ? : }
-
+
{wrapSearchTerms(title || '', search)}
@@ -71,4 +71,8 @@ export function TodoEmbeddableComponentInner({ input: { icon, title, task, searc
);
}
-export const TodoEmbeddableComponent = withEmbeddableSubscription(TodoEmbeddableComponentInner);
+export const TodoEmbeddableComponent = withEmbeddableSubscription<
+ TodoInput,
+ EmbeddableOutput,
+ TodoEmbeddable
+>(TodoEmbeddableComponentInner);
diff --git a/examples/embeddable_explorer/public/app.tsx b/examples/embeddable_explorer/public/app.tsx
index 9c8568454855db0..e18012b4b3d806b 100644
--- a/examples/embeddable_explorer/public/app.tsx
+++ b/examples/embeddable_explorer/public/app.tsx
@@ -117,18 +117,7 @@ const EmbeddableExplorerApp = ({
{
title: 'Dynamically adding children to a container',
id: 'embeddablePanelExamplae',
- component: (
-
- ),
+ component: ,
},
];
diff --git a/examples/embeddable_explorer/public/embeddable_panel_example.tsx b/examples/embeddable_explorer/public/embeddable_panel_example.tsx
index b26111bed7ff23b..54cd7c5b5b2c01e 100644
--- a/examples/embeddable_explorer/public/embeddable_panel_example.tsx
+++ b/examples/embeddable_explorer/public/embeddable_panel_example.tsx
@@ -29,43 +29,19 @@ import {
EuiText,
} from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
-import { OverlayStart, CoreStart, SavedObjectsStart, IUiSettingsClient } from 'kibana/public';
-import {
- EmbeddablePanel,
- EmbeddableStart,
- IEmbeddable,
-} from '../../../src/plugins/embeddable/public';
+import { EmbeddableStart, IEmbeddable } from '../../../src/plugins/embeddable/public';
import {
HELLO_WORLD_EMBEDDABLE,
TODO_EMBEDDABLE,
MULTI_TASK_TODO_EMBEDDABLE,
SEARCHABLE_LIST_CONTAINER,
} from '../../embeddable_examples/public';
-import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
-import { Start as InspectorStartContract } from '../../../src/plugins/inspector/public';
-import { getSavedObjectFinder } from '../../../src/plugins/saved_objects/public';
interface Props {
- getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'];
- getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
- uiActionsApi: UiActionsStart;
- overlays: OverlayStart;
- notifications: CoreStart['notifications'];
- inspector: InspectorStartContract;
- savedObject: SavedObjectsStart;
- uiSettingsClient: IUiSettingsClient;
+ embeddableServices: EmbeddableStart;
}
-export function EmbeddablePanelExample({
- inspector,
- notifications,
- overlays,
- getAllEmbeddableFactories,
- getEmbeddableFactory,
- uiActionsApi,
- savedObject,
- uiSettingsClient,
-}: Props) {
+export function EmbeddablePanelExample({ embeddableServices }: Props) {
const searchableInput = {
id: '1',
title: 'My searchable todo list',
@@ -105,7 +81,7 @@ export function EmbeddablePanelExample({
useEffect(() => {
ref.current = true;
if (!embeddable) {
- const factory = getEmbeddableFactory(SEARCHABLE_LIST_CONTAINER);
+ const factory = embeddableServices.getEmbeddableFactory(SEARCHABLE_LIST_CONTAINER);
const promise = factory?.create(searchableInput);
if (promise) {
promise.then(e => {
@@ -134,22 +110,13 @@ export function EmbeddablePanelExample({
You can render your embeddable inside the EmbeddablePanel component. This adds some
extra rendering and offers a context menu with pluggable actions. Using EmbeddablePanel
- to render your embeddable means you get access to the "e;Add panel flyout"e;.
- Now you can see how to add embeddables to your container, and how
- "e;getExplicitInput"e; is used to grab input not provided by the container.
+ to render your embeddable means you get access to the "Add panel flyout". Now
+ you can see how to add embeddables to your container, and how
+ "getExplicitInput" is used to grab input not provided by the container.
{embeddable ? (
-
+
) : (
Loading...
)}
diff --git a/examples/embeddable_explorer/public/list_container_example.tsx b/examples/embeddable_explorer/public/list_container_example.tsx
index 969fdb0ca46db84..98ad50418d3fea3 100644
--- a/examples/embeddable_explorer/public/list_container_example.tsx
+++ b/examples/embeddable_explorer/public/list_container_example.tsx
@@ -29,7 +29,11 @@ import {
EuiText,
} from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
-import { EmbeddableFactoryRenderer, EmbeddableStart } from '../../../src/plugins/embeddable/public';
+import {
+ EmbeddableFactoryRenderer,
+ EmbeddableStart,
+ ViewMode,
+} from '../../../src/plugins/embeddable/public';
import {
HELLO_WORLD_EMBEDDABLE,
TODO_EMBEDDABLE,
@@ -46,6 +50,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {
const listInput = {
id: 'hello',
title: 'My todo list',
+ viewMode: ViewMode.VIEW,
panels: {
'1': {
type: HELLO_WORLD_EMBEDDABLE,
@@ -76,6 +81,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {
const searchableInput = {
id: '1',
title: 'My searchable todo list',
+ viewMode: ViewMode.VIEW,
panels: {
'1': {
type: HELLO_WORLD_EMBEDDABLE,
@@ -150,7 +156,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {
- Check out the "e;Dynamically adding children"e; section, to see how to add
+ Check out the "Dynamically adding children" section, to see how to add
children to this container, and see it rendered inside an `EmbeddablePanel` component.
diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx
index 9e47da5cea0329e..2a0ffd723850b3a 100644
--- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx
+++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx
@@ -29,7 +29,7 @@ import {
ContactCardEmbeddable,
} from '../test_samples/embeddables/contact_card/contact_card_embeddable';
// eslint-disable-next-line
-import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks';
+import { inspectorPluginMock } from '../../../../inspector/public/mocks';
import { mount } from 'enzyme';
import { embeddablePluginMock } from '../../mocks';
diff --git a/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx b/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx
index 47b8001961cf514..9bc5889715c7605 100644
--- a/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx
@@ -23,18 +23,19 @@ import { IEmbeddable, EmbeddableInput, EmbeddableOutput } from './i_embeddable';
export const withEmbeddableSubscription = <
I extends EmbeddableInput,
O extends EmbeddableOutput,
- E extends IEmbeddable = IEmbeddable
+ E extends IEmbeddable = IEmbeddable,
+ ExtraProps = {}
>(
- WrappedComponent: React.ComponentType<{ input: I; output: O; embeddable: E }>
-): React.ComponentType<{ embeddable: E }> =>
+ WrappedComponent: React.ComponentType<{ input: I; output: O; embeddable: E } & ExtraProps>
+): React.ComponentType<{ embeddable: E } & ExtraProps> =>
class WithEmbeddableSubscription extends React.Component<
- { embeddable: E },
+ { embeddable: E } & ExtraProps,
{ input: I; output: O }
> {
private subscription?: Rx.Subscription;
private mounted: boolean = false;
- constructor(props: { embeddable: E }) {
+ constructor(props: { embeddable: E } & ExtraProps) {
super(props);
this.state = {
input: this.props.embeddable.getInput(),
@@ -71,6 +72,7 @@ export const withEmbeddableSubscription = <
input={this.state.input}
output={this.state.output}
embeddable={this.props.embeddable}
+ {...this.props}
/>
);
}
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
index 649677dc67c7d14..1e7cbb2f3dafcff 100644
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
+++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
@@ -25,7 +25,7 @@ import { nextTick } from 'test_utils/enzyme_helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { I18nProvider } from '@kbn/i18n/react';
import { CONTEXT_MENU_TRIGGER } from '../triggers';
-import { Action, UiActionsStart, ActionType } from 'src/plugins/ui_actions/public';
+import { Action, UiActionsStart, ActionType } from '../../../../ui_actions/public';
import { Trigger, ViewMode } from '../types';
import { isErrorEmbeddable } from '../embeddables';
import { EmbeddablePanel } from './embeddable_panel';
@@ -41,7 +41,7 @@ import {
ContactCardEmbeddableOutput,
} from '../test_samples/embeddables/contact_card/contact_card_embeddable';
// eslint-disable-next-line
-import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks';
+import { inspectorPluginMock } from '../../../../inspector/public/mocks';
import { EuiBadge } from '@elastic/eui';
import { embeddablePluginMock } from '../../mocks';
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx
index ee31127cb5a405b..491eaad9faefa4c 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx
@@ -27,7 +27,7 @@ import {
ContactCardEmbeddable,
} from '../../../test_samples';
// eslint-disable-next-line
-import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks';
+import { inspectorPluginMock } from '../../../../../../../plugins/inspector/public/mocks';
import { EmbeddableOutput, isErrorEmbeddable, ErrorEmbeddable } from '../../../embeddables';
import { of } from '../../../../tests/helpers';
import { esFilters } from '../../../../../../../plugins/data/public';
diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts
index 2ee05d8316aced6..65b15f3a7614fb4 100644
--- a/src/plugins/embeddable/public/mocks.ts
+++ b/src/plugins/embeddable/public/mocks.ts
@@ -16,11 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-
import { EmbeddableStart, EmbeddableSetup } from '.';
import { EmbeddablePublicPlugin } from './plugin';
import { coreMock } from '../../../core/public/mocks';
+// eslint-disable-next-line
+import { inspectorPluginMock } from '../../inspector/public/mocks';
// eslint-disable-next-line
import { uiActionsPluginMock } from '../../ui_actions/public/mocks';
@@ -39,6 +40,7 @@ const createStartContract = (): Start => {
const startContract: Start = {
getEmbeddableFactories: jest.fn(),
getEmbeddableFactory: jest.fn(),
+ EmbeddablePanel: jest.fn(),
};
return startContract;
};
@@ -48,7 +50,11 @@ const createInstance = () => {
const setup = plugin.setup(coreMock.createSetup(), {
uiActions: uiActionsPluginMock.createSetupContract(),
});
- const doStart = () => plugin.start(coreMock.createStart());
+ const doStart = () =>
+ plugin.start(coreMock.createStart(), {
+ uiActions: uiActionsPluginMock.createStartContract(),
+ inspector: inspectorPluginMock.createStartContract(),
+ });
return {
plugin,
setup,
diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.tsx
similarity index 76%
rename from src/plugins/embeddable/public/plugin.ts
rename to src/plugins/embeddable/public/plugin.tsx
index a483f90f76dde79..01fbf52c8018236 100644
--- a/src/plugins/embeddable/public/plugin.ts
+++ b/src/plugins/embeddable/public/plugin.tsx
@@ -16,7 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { UiActionsSetup } from 'src/plugins/ui_actions/public';
+import React from 'react';
+import { getSavedObjectFinder } from '../../saved_objects/public';
+import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public';
+import { Start as InspectorStart } from '../../inspector/public';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public';
import { EmbeddableFactoryRegistry, EmbeddableFactoryProvider } from './types';
import { bootstrap } from './bootstrap';
@@ -26,6 +29,7 @@ import {
EmbeddableOutput,
defaultEmbeddableFactoryProvider,
IEmbeddable,
+ EmbeddablePanel,
} from './lib';
import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
@@ -33,6 +37,11 @@ export interface EmbeddableSetupDependencies {
uiActions: UiActionsSetup;
}
+export interface EmbeddableStartDependencies {
+ uiActions: UiActionsStart;
+ inspector: InspectorStart;
+}
+
export interface EmbeddableSetup {
registerEmbeddableFactory: (
id: string,
@@ -50,6 +59,7 @@ export interface EmbeddableStart {
embeddableFactoryId: string
) => EmbeddableFactory | undefined;
getEmbeddableFactories: () => IterableIterator;
+ EmbeddablePanel: React.FC<{ embeddable: IEmbeddable; hideHeader?: boolean }>;
}
export class EmbeddablePublicPlugin implements Plugin {
@@ -78,7 +88,10 @@ export class EmbeddablePublicPlugin implements Plugin {
this.embeddableFactories.set(
def.type,
@@ -89,15 +102,36 @@ export class EmbeddablePublicPlugin implements Plugin {
- this.ensureFactoriesExist();
- return this.embeddableFactories.values();
- },
+ getEmbeddableFactories: this.getEmbeddableFactories,
+ EmbeddablePanel: ({
+ embeddable,
+ hideHeader,
+ }: {
+ embeddable: IEmbeddable;
+ hideHeader?: boolean;
+ }) => (
+
+ ),
};
}
public stop() {}
+ private getEmbeddableFactories = () => {
+ this.ensureFactoriesExist();
+ return this.embeddableFactories.values();
+ };
+
private registerEmbeddableFactory = (
embeddableFactoryId: string,
factory: EmbeddableFactoryDefinition
@@ -130,11 +164,11 @@ export class EmbeddablePublicPlugin implements Plugin {
this.embeddableFactoryDefinitions.forEach(def => this.ensureFactoryExists(def.type));
- }
+ };
- private ensureFactoryExists(type: string) {
+ private ensureFactoryExists = (type: string) => {
if (!this.embeddableFactories.get(type)) {
const def = this.embeddableFactoryDefinitions.get(type);
if (!def) return;
@@ -145,5 +179,5 @@ export class EmbeddablePublicPlugin implements Plugin {
diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts
index e199ef193aa1cc8..e13a906e30338f4 100644
--- a/src/plugins/embeddable/public/tests/test_plugin.ts
+++ b/src/plugins/embeddable/public/tests/test_plugin.ts
@@ -18,9 +18,11 @@
*/
import { CoreSetup, CoreStart } from 'src/core/public';
+import { UiActionsStart } from '../../../ui_actions/public';
// eslint-disable-next-line
-import { uiActionsPluginMock } from 'src/plugins/ui_actions/public/mocks';
-import { UiActionsStart } from 'src/plugins/ui_actions/public';
+import { uiActionsPluginMock } from '../../../ui_actions/public/mocks';
+// eslint-disable-next-line
+import { inspectorPluginMock } from '../../../inspector/public/mocks';
import { coreMock } from '../../../../core/public/mocks';
import { EmbeddablePublicPlugin, EmbeddableSetup, EmbeddableStart } from '../plugin';
@@ -48,7 +50,10 @@ export const testPlugin = (
coreStart,
setup,
doStart: (anotherCoreStart: CoreStart = coreStart) => {
- const start = plugin.start(anotherCoreStart);
+ const start = plugin.start(anotherCoreStart, {
+ uiActions: uiActionsPluginMock.createStartContract(),
+ inspector: inspectorPluginMock.createStartContract(),
+ });
return start;
},
uiActions: uiActions.doStart(coreStart),
diff --git a/test/examples/embeddables/list_container.ts b/test/examples/embeddables/list_container.ts
index b1b91ad2c37f1b1..9e93d479471e8a3 100644
--- a/test/examples/embeddables/list_container.ts
+++ b/test/examples/embeddables/list_container.ts
@@ -57,13 +57,12 @@ export default function({ getService }: PluginFunctionalProviderContext) {
expect(text).to.eql(['HELLO WORLD!']);
});
- it('searchable container filters multi-task children', async () => {
+ it('searchable container finds matches in multi-task children', async () => {
await testSubjects.setValue('filterTodos', 'earth');
+ await testSubjects.click('checkMatchingTodos');
+ await testSubjects.click('deleteCheckedTodos');
- await retry.try(async () => {
- const tasks = await testSubjects.getVisibleTextAll('multiTaskTodoTask');
- expect(tasks).to.eql(['Watch planet earth']);
- });
+ await testSubjects.missingOrFail('multiTaskTodoTask');
});
});
}
diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx
index 54d13efe4d79097..2ecde823dc4dfc9 100644
--- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx
+++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx
@@ -18,21 +18,11 @@
*/
import { EuiTab } from '@elastic/eui';
import React, { Component } from 'react';
-import { CoreStart } from 'src/core/public';
import { EmbeddableStart } from 'src/plugins/embeddable/public';
-import { UiActionsService } from '../../../../../../../../src/plugins/ui_actions/public';
import { DashboardContainerExample } from './dashboard_container_example';
-import { Start as InspectorStartContract } from '../../../../../../../../src/plugins/inspector/public';
export interface AppProps {
- getActions: UiActionsService['getTriggerCompatibleActions'];
- getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
- getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'];
- overlays: CoreStart['overlays'];
- notifications: CoreStart['notifications'];
- inspector: InspectorStartContract;
- SavedObjectFinder: React.ComponentType;
- I18nContext: CoreStart['i18n']['Context'];
+ embeddableServices: EmbeddableStart;
}
export class App extends Component {
@@ -72,29 +62,17 @@ export class App extends Component {
public render() {
return (
-
-
-
{this.renderTabs()}
- {this.getContentsForTab()}
-
-
+
+
{this.renderTabs()}
+ {this.getContentsForTab()}
+
);
}
private getContentsForTab() {
switch (this.state.selectedTabId) {
case 'dashboardContainer': {
- return (
-
- );
+ return ;
}
}
}
diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx
index fd07416cadbc5d8..16c2840d6a32e8d 100644
--- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx
+++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx
@@ -19,32 +19,17 @@
import React from 'react';
import { EuiButton, EuiLoadingChart } from '@elastic/eui';
import { ContainerOutput } from 'src/plugins/embeddable/public';
-import {
- ErrorEmbeddable,
- ViewMode,
- isErrorEmbeddable,
- EmbeddablePanel,
- EmbeddableStart,
-} from '../embeddable_api';
+import { ErrorEmbeddable, ViewMode, isErrorEmbeddable, EmbeddableStart } from '../embeddable_api';
import {
DASHBOARD_CONTAINER_TYPE,
DashboardContainer,
DashboardContainerInput,
} from '../../../../../../../../src/plugins/dashboard/public';
-import { CoreStart } from '../../../../../../../../src/core/public';
import { dashboardInput } from './dashboard_input';
-import { Start as InspectorStartContract } from '../../../../../../../../src/plugins/inspector/public';
-import { UiActionsService } from '../../../../../../../../src/plugins/ui_actions/public';
interface Props {
- getActions: UiActionsService['getTriggerCompatibleActions'];
- getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
- getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'];
- overlays: CoreStart['overlays'];
- notifications: CoreStart['notifications'];
- inspector: InspectorStartContract;
- SavedObjectFinder: React.ComponentType;
+ embeddableServices: EmbeddableStart;
}
interface State {
@@ -67,7 +52,7 @@ export class DashboardContainerExample extends React.Component {
public async componentDidMount() {
this.mounted = true;
- const dashboardFactory = this.props.getEmbeddableFactory<
+ const dashboardFactory = this.props.embeddableServices.getEmbeddableFactory<
DashboardContainerInput,
ContainerOutput,
DashboardContainer
@@ -99,6 +84,7 @@ export class DashboardContainerExample extends React.Component {
};
public render() {
+ const { embeddableServices } = this.props;
return (
Dashboard Container
@@ -108,16 +94,7 @@ export class DashboardContainerExample extends React.Component
{
{!this.state.loaded || !this.container ? (
) : (
-
+
)}
);
diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx
index 18ceec652392d1c..e5f5faa6ac361df 100644
--- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx
+++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx
@@ -33,7 +33,6 @@ const REACT_ROOT_ID = 'embeddableExplorerRoot';
import { SayHelloAction, createSendMessageAction } from './embeddable_api';
import { App } from './app';
-import { getSavedObjectFinder } from '../../../../../../../src/plugins/saved_objects/public';
import {
EmbeddableStart,
EmbeddableSetup,
@@ -78,19 +77,7 @@ export class EmbeddableExplorerPublicPlugin
plugins.__LEGACY.onRenderComplete(() => {
const root = document.getElementById(REACT_ROOT_ID);
- ReactDOM.render(
- ,
- root
- );
+ ReactDOM.render(, root);
});
}
From 29abe5b81bddd17dcdd671cf3456f99bfe7b08a0 Mon Sep 17 00:00:00 2001
From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com>
Date: Mon, 6 Apr 2020 13:54:21 -0400
Subject: [PATCH 15/15] [Ingest] EMT-146: agent status impl preparation
(#62557)
[Ingest] EMT-146: very light refactor a precursor for endpoint status change
---
x-pack/plugins/endpoint/server/plugin.ts | 2 +-
.../endpoint/server/routes/alerts/details/handlers.ts | 2 +-
.../server/routes/{metadata.ts => metadata/index.ts} | 9 +++------
.../server/routes/{ => metadata}/metadata.test.ts | 10 +++++-----
.../metadata/query_builders.test.ts} | 5 +----
.../metadata/query_builders.ts} | 4 ++--
6 files changed, 13 insertions(+), 19 deletions(-)
rename x-pack/plugins/endpoint/server/routes/{metadata.ts => metadata/index.ts} (93%)
rename x-pack/plugins/endpoint/server/routes/{ => metadata}/metadata.test.ts (96%)
rename x-pack/plugins/endpoint/server/{services/endpoint/metadata_query_builders.test.ts => routes/metadata/query_builders.test.ts} (97%)
rename x-pack/plugins/endpoint/server/{services/endpoint/metadata_query_builders.ts => routes/metadata/query_builders.ts} (100%)
diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts
index 6d2e9e510551ae9..d3a399124124f20 100644
--- a/x-pack/plugins/endpoint/server/plugin.ts
+++ b/x-pack/plugins/endpoint/server/plugin.ts
@@ -9,9 +9,9 @@ import { PluginSetupContract as FeaturesPluginSetupContract } from '../../featur
import { createConfig$, EndpointConfigType } from './config';
import { EndpointAppContext } from './types';
-import { registerEndpointRoutes } from './routes/metadata';
import { registerAlertRoutes } from './routes/alerts';
import { registerResolverRoutes } from './routes/resolver';
+import { registerEndpointRoutes } from './routes/metadata';
export type EndpointPluginStart = void;
export type EndpointPluginSetup = void;
diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts
index b95c1aaf87c1488..725e362f91ec76c 100644
--- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts
+++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts
@@ -9,7 +9,7 @@ import { AlertEvent, EndpointAppConstants } from '../../../../common/types';
import { EndpointAppContext } from '../../../types';
import { AlertDetailsRequestParams } from '../types';
import { AlertDetailsPagination } from './lib';
-import { getHostData } from '../../../routes/metadata';
+import { getHostData } from '../../metadata';
export const alertDetailsHandlerWrapper = function(
endpointAppContext: EndpointAppContext
diff --git a/x-pack/plugins/endpoint/server/routes/metadata.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts
similarity index 93%
rename from x-pack/plugins/endpoint/server/routes/metadata.ts
rename to x-pack/plugins/endpoint/server/routes/metadata/index.ts
index 787ffe58a537298..ef01db9af98c4b3 100644
--- a/x-pack/plugins/endpoint/server/routes/metadata.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts
@@ -8,12 +8,9 @@ import { IRouter, RequestHandlerContext } from 'kibana/server';
import { SearchResponse } from 'elasticsearch';
import { schema } from '@kbn/config-schema';
-import {
- kibanaRequestToMetadataListESQuery,
- getESQueryHostMetadataByID,
-} from '../services/endpoint/metadata_query_builders';
-import { HostMetadata, HostResultList } from '../../common/types';
-import { EndpointAppContext } from '../types';
+import { HostMetadata, HostResultList } from '../../../common/types';
+import { EndpointAppContext } from '../../types';
+import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders';
interface HitSource {
_source: HostMetadata;
diff --git a/x-pack/plugins/endpoint/server/routes/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
similarity index 96%
rename from x-pack/plugins/endpoint/server/routes/metadata.test.ts
rename to x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
index 65e07edbcde249b..e0fd11e737e7dcb 100644
--- a/x-pack/plugins/endpoint/server/routes/metadata.test.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
@@ -17,12 +17,12 @@ import {
httpServerMock,
httpServiceMock,
loggingServiceMock,
-} from '../../../../../src/core/server/mocks';
-import { HostMetadata, HostResultList } from '../../common/types';
+} from '../../../../../../src/core/server/mocks';
+import { HostMetadata, HostResultList } from '../../../common/types';
import { SearchResponse } from 'elasticsearch';
-import { registerEndpointRoutes } from './metadata';
-import { EndpointConfigSchema } from '../config';
-import * as data from '../test_data/all_metadata_data.json';
+import { EndpointConfigSchema } from '../../config';
+import * as data from '../../test_data/all_metadata_data.json';
+import { registerEndpointRoutes } from './index';
describe('test endpoint route', () => {
let routerMock: jest.Mocked;
diff --git a/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts
similarity index 97%
rename from x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.test.ts
rename to x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts
index 0966b52c79f7d05..2514d5aa858111f 100644
--- a/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.test.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts
@@ -5,10 +5,7 @@
*/
import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks';
import { EndpointConfigSchema } from '../../config';
-import {
- kibanaRequestToMetadataListESQuery,
- getESQueryHostMetadataByID,
-} from './metadata_query_builders';
+import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders';
import { EndpointAppConstants } from '../../../common/types';
describe('query builder', () => {
diff --git a/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts
similarity index 100%
rename from x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.ts
rename to x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts
index 57b0a4ef1051947..bd07604fe9ad248 100644
--- a/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts
@@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { KibanaRequest } from 'kibana/server';
-import { EndpointAppConstants } from '../../../common/types';
-import { EndpointAppContext } from '../../types';
import { esKuery } from '../../../../../../src/plugins/data/server';
+import { EndpointAppContext } from '../../types';
+import { EndpointAppConstants } from '../../../common/types';
export const kibanaRequestToMetadataListESQuery = async (
request: KibanaRequest,