- ) {
- this.service = await this.factory(
- coreSetup,
- coreStart,
- canvasSetupPlugins,
- canvasStartPlugins,
- appUpdater
- );
- }
-
- getService(): Service {
- if (!this.service) {
- throw new Error('Service not ready');
- }
-
- return this.service;
- }
-
- stop() {
- this.service = undefined;
- }
+export interface CanvasPluginServices {
+ workpad: CanvasWorkpadService;
}
-export type ServiceFromProvider = P extends CanvasServiceProvider ? T : never;
-
-export const services = {
- embeddables: new CanvasServiceProvider(embeddablesServiceFactory),
- expressions: new CanvasServiceProvider(expressionsServiceFactory),
- notify: new CanvasServiceProvider(notifyServiceFactory),
- platform: new CanvasServiceProvider(platformServiceFactory),
- navLink: new CanvasServiceProvider(navLinkServiceFactory),
- search: new CanvasServiceProvider(searchServiceFactory),
- reporting: new CanvasServiceProvider(reportingServiceFactory),
- labs: new CanvasServiceProvider(labsServiceFactory),
- workpad: new CanvasServiceProvider(workpadServiceFactory),
-};
-
-export type CanvasServiceProviders = typeof services;
-
-export interface CanvasServices {
- embeddables: ServiceFromProvider;
- expressions: ServiceFromProvider;
- notify: ServiceFromProvider;
- platform: ServiceFromProvider;
- navLink: ServiceFromProvider;
- search: ServiceFromProvider;
- reporting: ServiceFromProvider;
- labs: ServiceFromProvider;
- workpad: ServiceFromProvider;
-}
-
-export const startServices = async (
- coreSetup: CoreSetup,
- coreStart: CoreStart,
- canvasSetupPlugins: CanvasSetupDeps,
- canvasStartPlugins: CanvasStartDeps,
- appUpdater: BehaviorSubject
-) => {
- const startPromises = Object.values(services).map((provider) =>
- provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins, appUpdater)
- );
-
- await Promise.all(startPromises);
-};
-
-export const stopServices = () => {
- Object.values(services).forEach((provider) => provider.stop());
-};
+export const pluginServices = new PluginServices();
-export const {
- embeddables: embeddableService,
- notify: notifyService,
- platform: platformService,
- navLink: navLinkService,
- expressions: expressionsService,
- search: searchService,
- reporting: reportingService,
-} = services;
+export const useWorkpadService = () => (() => pluginServices.getHooks().workpad.useService())();
diff --git a/x-pack/plugins/canvas/public/services/kibana/index.ts b/x-pack/plugins/canvas/public/services/kibana/index.ts
new file mode 100644
index 0000000000000..99012003b3a15
--- /dev/null
+++ b/x-pack/plugins/canvas/public/services/kibana/index.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ PluginServiceProviders,
+ PluginServiceProvider,
+ PluginServiceRegistry,
+ KibanaPluginServiceParams,
+} from '../../../../../../src/plugins/presentation_util/public';
+
+import { workpadServiceFactory } from './workpad';
+import { CanvasPluginServices } from '..';
+import { CanvasStartDeps } from '../../plugin';
+
+export { workpadServiceFactory } from './workpad';
+
+export const pluginServiceProviders: PluginServiceProviders<
+ CanvasPluginServices,
+ KibanaPluginServiceParams
+> = {
+ workpad: new PluginServiceProvider(workpadServiceFactory),
+};
+
+export const pluginServiceRegistry = new PluginServiceRegistry<
+ CanvasPluginServices,
+ KibanaPluginServiceParams
+>(pluginServiceProviders);
diff --git a/x-pack/plugins/canvas/public/services/kibana/workpad.ts b/x-pack/plugins/canvas/public/services/kibana/workpad.ts
new file mode 100644
index 0000000000000..36ad1c568f9e6
--- /dev/null
+++ b/x-pack/plugins/canvas/public/services/kibana/workpad.ts
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public';
+
+import { CanvasStartDeps } from '../../plugin';
+import { CanvasWorkpadService } from '../workpad';
+
+import {
+ API_ROUTE_WORKPAD,
+ DEFAULT_WORKPAD_CSS,
+ API_ROUTE_TEMPLATES,
+} from '../../../common/lib/constants';
+import { CanvasWorkpad } from '../../../types';
+
+export type CanvasWorkpadServiceFactory = KibanaPluginServiceFactory<
+ CanvasWorkpadService,
+ CanvasStartDeps
+>;
+
+/*
+ Remove any top level keys from the workpad which will be rejected by validation
+*/
+const validKeys = [
+ '@created',
+ '@timestamp',
+ 'assets',
+ 'colors',
+ 'css',
+ 'variables',
+ 'height',
+ 'id',
+ 'isWriteable',
+ 'name',
+ 'page',
+ 'pages',
+ 'width',
+];
+
+const sanitizeWorkpad = function (workpad: CanvasWorkpad) {
+ const workpadKeys = Object.keys(workpad);
+
+ for (const key of workpadKeys) {
+ if (!validKeys.includes(key)) {
+ delete (workpad as { [key: string]: any })[key];
+ }
+ }
+
+ return workpad;
+};
+
+export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ coreStart, startPlugins }) => {
+ const getApiPath = function () {
+ return `${API_ROUTE_WORKPAD}`;
+ };
+
+ return {
+ get: async (id: string) => {
+ const workpad = await coreStart.http.get(`${getApiPath()}/${id}`);
+
+ return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad };
+ },
+ create: (workpad: CanvasWorkpad) => {
+ return coreStart.http.post(getApiPath(), {
+ body: JSON.stringify({
+ ...sanitizeWorkpad({ ...workpad }),
+ assets: workpad.assets || {},
+ variables: workpad.variables || [],
+ }),
+ });
+ },
+ createFromTemplate: (templateId: string) => {
+ return coreStart.http.post(getApiPath(), {
+ body: JSON.stringify({ templateId }),
+ });
+ },
+ findTemplates: async () => coreStart.http.get(API_ROUTE_TEMPLATES),
+ find: (searchTerm: string) => {
+ // TODO: this shouldn't be necessary. Check for usage.
+ const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0;
+
+ return coreStart.http.get(`${getApiPath()}/find`, {
+ query: {
+ perPage: 10000,
+ name: validSearchTerm ? searchTerm : '',
+ },
+ });
+ },
+ remove: (id: string) => {
+ return coreStart.http.delete(`${getApiPath()}/${id}`);
+ },
+ };
+};
diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/legacy/context.tsx
similarity index 92%
rename from x-pack/plugins/canvas/public/services/context.tsx
rename to x-pack/plugins/canvas/public/services/legacy/context.tsx
index 3a78e314b9635..7a90c6870df4a 100644
--- a/x-pack/plugins/canvas/public/services/context.tsx
+++ b/x-pack/plugins/canvas/public/services/legacy/context.tsx
@@ -26,7 +26,6 @@ const defaultContextValue = {
platform: {},
navLink: {},
search: {},
- workpad: {},
};
const context = createContext(defaultContextValue as CanvasServices);
@@ -38,7 +37,6 @@ export const useExpressionsService = () => useServices().expressions;
export const useNotifyService = () => useServices().notify;
export const useNavLinkService = () => useServices().navLink;
export const useLabsService = () => useServices().labs;
-export const useWorkpadService = () => useServices().workpad;
export const withServices = (type: ComponentType) => {
const EnhancedType: FC = (props) =>
@@ -46,7 +44,7 @@ export const withServices = (type: ComponentTyp
return EnhancedType;
};
-export const ServicesProvider: FC<{
+export const LegacyServicesProvider: FC<{
providers?: Partial;
children: ReactElement;
}> = ({ providers = {}, children }) => {
@@ -60,7 +58,6 @@ export const ServicesProvider: FC<{
search: specifiedProviders.search.getService(),
reporting: specifiedProviders.reporting.getService(),
labs: specifiedProviders.labs.getService(),
- workpad: specifiedProviders.workpad.getService(),
};
return {children};
};
diff --git a/x-pack/plugins/canvas/public/services/embeddables.ts b/x-pack/plugins/canvas/public/services/legacy/embeddables.ts
similarity index 88%
rename from x-pack/plugins/canvas/public/services/embeddables.ts
rename to x-pack/plugins/canvas/public/services/legacy/embeddables.ts
index 1281c60f31782..05a4205c23f9e 100644
--- a/x-pack/plugins/canvas/public/services/embeddables.ts
+++ b/x-pack/plugins/canvas/public/services/legacy/embeddables.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EmbeddableFactory } from '../../../../../src/plugins/embeddable/public';
+import { EmbeddableFactory } from '../../../../../../src/plugins/embeddable/public';
import { CanvasServiceFactory } from '.';
export interface EmbeddablesService {
diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/legacy/expressions.ts
similarity index 93%
rename from x-pack/plugins/canvas/public/services/expressions.ts
rename to x-pack/plugins/canvas/public/services/legacy/expressions.ts
index 219edb667efc6..99915cad745e3 100644
--- a/x-pack/plugins/canvas/public/services/expressions.ts
+++ b/x-pack/plugins/canvas/public/services/legacy/expressions.ts
@@ -9,8 +9,8 @@ import { CanvasServiceFactory } from '.';
import {
ExpressionsService,
serializeProvider,
-} from '../../../../../src/plugins/expressions/common';
-import { API_ROUTE_FUNCTIONS } from '../../common/lib/constants';
+} from '../../../../../../src/plugins/expressions/common';
+import { API_ROUTE_FUNCTIONS } from '../../../common/lib/constants';
export const expressionsServiceFactory: CanvasServiceFactory = async (
coreSetup,
diff --git a/x-pack/plugins/canvas/public/services/legacy/index.ts b/x-pack/plugins/canvas/public/services/legacy/index.ts
new file mode 100644
index 0000000000000..e23057daa7359
--- /dev/null
+++ b/x-pack/plugins/canvas/public/services/legacy/index.ts
@@ -0,0 +1,129 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { BehaviorSubject } from 'rxjs';
+import { CoreSetup, CoreStart, AppUpdater } from '../../../../../../src/core/public';
+import { CanvasSetupDeps, CanvasStartDeps } from '../../plugin';
+import { notifyServiceFactory } from './notify';
+import { platformServiceFactory } from './platform';
+import { navLinkServiceFactory } from './nav_link';
+import { embeddablesServiceFactory } from './embeddables';
+import { expressionsServiceFactory } from './expressions';
+import { searchServiceFactory } from './search';
+import { labsServiceFactory } from './labs';
+import { reportingServiceFactory } from './reporting';
+
+export { NotifyService } from './notify';
+export { SearchService } from './search';
+export { PlatformService } from './platform';
+export { NavLinkService } from './nav_link';
+export { EmbeddablesService } from './embeddables';
+export { ExpressionsService } from '../../../../../../src/plugins/expressions/common';
+export * from './context';
+
+export type CanvasServiceFactory = (
+ coreSetup: CoreSetup,
+ coreStart: CoreStart,
+ canvasSetupPlugins: CanvasSetupDeps,
+ canvasStartPlugins: CanvasStartDeps,
+ appUpdater: BehaviorSubject
+) => Service | Promise;
+
+export class CanvasServiceProvider {
+ private factory: CanvasServiceFactory;
+ private service: Service | undefined;
+
+ constructor(factory: CanvasServiceFactory) {
+ this.factory = factory;
+ }
+
+ setService(service: Service) {
+ this.service = service;
+ }
+
+ async start(
+ coreSetup: CoreSetup,
+ coreStart: CoreStart,
+ canvasSetupPlugins: CanvasSetupDeps,
+ canvasStartPlugins: CanvasStartDeps,
+ appUpdater: BehaviorSubject
+ ) {
+ this.service = await this.factory(
+ coreSetup,
+ coreStart,
+ canvasSetupPlugins,
+ canvasStartPlugins,
+ appUpdater
+ );
+ }
+
+ getService(): Service {
+ if (!this.service) {
+ throw new Error('Service not ready');
+ }
+
+ return this.service;
+ }
+
+ stop() {
+ this.service = undefined;
+ }
+}
+
+export type ServiceFromProvider = P extends CanvasServiceProvider ? T : never;
+
+export const services = {
+ embeddables: new CanvasServiceProvider(embeddablesServiceFactory),
+ expressions: new CanvasServiceProvider(expressionsServiceFactory),
+ notify: new CanvasServiceProvider(notifyServiceFactory),
+ platform: new CanvasServiceProvider(platformServiceFactory),
+ navLink: new CanvasServiceProvider(navLinkServiceFactory),
+ search: new CanvasServiceProvider(searchServiceFactory),
+ reporting: new CanvasServiceProvider(reportingServiceFactory),
+ labs: new CanvasServiceProvider(labsServiceFactory),
+};
+
+export type CanvasServiceProviders = typeof services;
+
+export interface CanvasServices {
+ embeddables: ServiceFromProvider;
+ expressions: ServiceFromProvider;
+ notify: ServiceFromProvider;
+ platform: ServiceFromProvider;
+ navLink: ServiceFromProvider;
+ search: ServiceFromProvider;
+ reporting: ServiceFromProvider;
+ labs: ServiceFromProvider;
+}
+
+export const startServices = async (
+ coreSetup: CoreSetup,
+ coreStart: CoreStart,
+ canvasSetupPlugins: CanvasSetupDeps,
+ canvasStartPlugins: CanvasStartDeps,
+ appUpdater: BehaviorSubject
+) => {
+ const startPromises = Object.values(services).map((provider) =>
+ provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins, appUpdater)
+ );
+
+ await Promise.all(startPromises);
+};
+
+export const stopServices = () => {
+ Object.values(services).forEach((provider) => provider.stop());
+};
+
+export const {
+ embeddables: embeddableService,
+ notify: notifyService,
+ platform: platformService,
+ navLink: navLinkService,
+ expressions: expressionsService,
+ search: searchService,
+ reporting: reportingService,
+} = services;
diff --git a/x-pack/plugins/canvas/public/services/labs.ts b/x-pack/plugins/canvas/public/services/legacy/labs.ts
similarity index 87%
rename from x-pack/plugins/canvas/public/services/labs.ts
rename to x-pack/plugins/canvas/public/services/legacy/labs.ts
index 7f5de8d1e6570..2a506d813bde9 100644
--- a/x-pack/plugins/canvas/public/services/labs.ts
+++ b/x-pack/plugins/canvas/public/services/legacy/labs.ts
@@ -8,10 +8,10 @@
import {
projectIDs,
PresentationLabsService,
-} from '../../../../../src/plugins/presentation_util/public';
+} from '../../../../../../src/plugins/presentation_util/public';
import { CanvasServiceFactory } from '.';
-import { UI_SETTINGS } from '../../common';
+import { UI_SETTINGS } from '../../../common';
export interface CanvasLabsService extends PresentationLabsService {
projectIDs: typeof projectIDs;
isLabsEnabled: () => boolean;
diff --git a/x-pack/plugins/canvas/public/services/nav_link.ts b/x-pack/plugins/canvas/public/services/legacy/nav_link.ts
similarity index 85%
rename from x-pack/plugins/canvas/public/services/nav_link.ts
rename to x-pack/plugins/canvas/public/services/legacy/nav_link.ts
index 068874b745d9d..49088c08a8a71 100644
--- a/x-pack/plugins/canvas/public/services/nav_link.ts
+++ b/x-pack/plugins/canvas/public/services/legacy/nav_link.ts
@@ -6,8 +6,8 @@
*/
import { CanvasServiceFactory } from '.';
-import { SESSIONSTORAGE_LASTPATH } from '../../common/lib/constants';
-import { getSessionStorage } from '../lib/storage';
+import { SESSIONSTORAGE_LASTPATH } from '../../../common/lib/constants';
+import { getSessionStorage } from '../../lib/storage';
export interface NavLinkService {
updatePath: (path: string) => void;
diff --git a/x-pack/plugins/canvas/public/services/notify.ts b/x-pack/plugins/canvas/public/services/legacy/notify.ts
similarity index 92%
rename from x-pack/plugins/canvas/public/services/notify.ts
rename to x-pack/plugins/canvas/public/services/legacy/notify.ts
index 6ee5eec6291ab..22dcfa671d0b5 100644
--- a/x-pack/plugins/canvas/public/services/notify.ts
+++ b/x-pack/plugins/canvas/public/services/legacy/notify.ts
@@ -7,8 +7,8 @@
import { get } from 'lodash';
import { CanvasServiceFactory } from '.';
-import { formatMsg } from '../../../../../src/plugins/kibana_legacy/public';
-import { ToastInputFields } from '../../../../../src/core/public';
+import { formatMsg } from '../../../../../../src/plugins/kibana_legacy/public';
+import { ToastInputFields } from '../../../../../../src/core/public';
const getToast = (err: Error | string, opts: ToastInputFields = {}) => {
const errData = (get(err, 'response') || err) as Error | string;
diff --git a/x-pack/plugins/canvas/public/services/platform.ts b/x-pack/plugins/canvas/public/services/legacy/platform.ts
similarity index 98%
rename from x-pack/plugins/canvas/public/services/platform.ts
rename to x-pack/plugins/canvas/public/services/legacy/platform.ts
index c4be5097a18f0..b867622f5d302 100644
--- a/x-pack/plugins/canvas/public/services/platform.ts
+++ b/x-pack/plugins/canvas/public/services/legacy/platform.ts
@@ -12,7 +12,7 @@ import {
ChromeBreadcrumb,
IBasePath,
ChromeStart,
-} from '../../../../../src/core/public';
+} from '../../../../../../src/core/public';
import { CanvasServiceFactory } from '.';
export interface PlatformService {
diff --git a/x-pack/plugins/canvas/public/services/reporting.ts b/x-pack/plugins/canvas/public/services/legacy/reporting.ts
similarity index 94%
rename from x-pack/plugins/canvas/public/services/reporting.ts
rename to x-pack/plugins/canvas/public/services/legacy/reporting.ts
index 4fa40401472c6..e594475360dff 100644
--- a/x-pack/plugins/canvas/public/services/reporting.ts
+++ b/x-pack/plugins/canvas/public/services/legacy/reporting.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { ReportingStart } from '../../../reporting/public';
+import { ReportingStart } from '../../../../reporting/public';
import { CanvasServiceFactory } from './';
export interface ReportingService {
diff --git a/x-pack/plugins/canvas/public/services/search.ts b/x-pack/plugins/canvas/public/services/legacy/search.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/services/search.ts
rename to x-pack/plugins/canvas/public/services/legacy/search.ts
diff --git a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/embeddables.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/services/stubs/embeddables.ts
rename to x-pack/plugins/canvas/public/services/legacy/stubs/embeddables.ts
diff --git a/x-pack/plugins/canvas/public/services/stubs/expressions.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/expressions.ts
similarity index 75%
rename from x-pack/plugins/canvas/public/services/stubs/expressions.ts
rename to x-pack/plugins/canvas/public/services/legacy/stubs/expressions.ts
index 497ec9b162045..bd1076ab0bf80 100644
--- a/x-pack/plugins/canvas/public/services/stubs/expressions.ts
+++ b/x-pack/plugins/canvas/public/services/legacy/stubs/expressions.ts
@@ -7,9 +7,9 @@
import { AnyExpressionRenderDefinition } from 'src/plugins/expressions';
import { ExpressionsService } from '../';
-import { plugin } from '../../../../../../src/plugins/expressions/public';
-import { functions as functionDefinitions } from '../../../canvas_plugin_src/functions/common';
-import { renderFunctions } from '../../../canvas_plugin_src/renderers/core';
+import { plugin } from '../../../../../../../src/plugins/expressions/public';
+import { functions as functionDefinitions } from '../../../../canvas_plugin_src/functions/common';
+import { renderFunctions } from '../../../../canvas_plugin_src/renderers/core';
const placeholder = {} as any;
const expressionsPlugin = plugin(placeholder);
diff --git a/x-pack/plugins/canvas/public/services/legacy/stubs/index.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/index.ts
new file mode 100644
index 0000000000000..7246a34d7f491
--- /dev/null
+++ b/x-pack/plugins/canvas/public/services/legacy/stubs/index.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { CanvasServices, services } from '../';
+import { embeddablesService } from './embeddables';
+import { expressionsService } from './expressions';
+import { reportingService } from './reporting';
+import { navLinkService } from './nav_link';
+import { notifyService } from './notify';
+import { labsService } from './labs';
+import { platformService } from './platform';
+import { searchService } from './search';
+
+export const stubs: CanvasServices = {
+ embeddables: embeddablesService,
+ expressions: expressionsService,
+ reporting: reportingService,
+ navLink: navLinkService,
+ notify: notifyService,
+ platform: platformService,
+ search: searchService,
+ labs: labsService,
+};
+
+export const startServices = async (providedServices: Partial = {}) => {
+ Object.entries(services).forEach(([key, provider]) => {
+ // @ts-expect-error Object.entries isn't strongly typed
+ const stub = providedServices[key] || stubs[key];
+ provider.setService(stub);
+ });
+};
diff --git a/x-pack/plugins/canvas/public/services/stubs/labs.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/labs.ts
similarity index 86%
rename from x-pack/plugins/canvas/public/services/stubs/labs.ts
rename to x-pack/plugins/canvas/public/services/legacy/stubs/labs.ts
index db89c5c35d5fb..fc65d45d2dd34 100644
--- a/x-pack/plugins/canvas/public/services/stubs/labs.ts
+++ b/x-pack/plugins/canvas/public/services/legacy/stubs/labs.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { projectIDs } from '../../../../../../src/plugins/presentation_util/public';
+import { projectIDs } from '../../../../../../../src/plugins/presentation_util/public';
import { CanvasLabsService } from '../labs';
const noop = (..._args: any[]): any => {};
diff --git a/x-pack/plugins/canvas/public/services/stubs/nav_link.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/nav_link.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/services/stubs/nav_link.ts
rename to x-pack/plugins/canvas/public/services/legacy/stubs/nav_link.ts
diff --git a/x-pack/plugins/canvas/public/services/stubs/notify.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/notify.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/services/stubs/notify.ts
rename to x-pack/plugins/canvas/public/services/legacy/stubs/notify.ts
diff --git a/x-pack/plugins/canvas/public/services/stubs/platform.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/platform.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/services/stubs/platform.ts
rename to x-pack/plugins/canvas/public/services/legacy/stubs/platform.ts
diff --git a/x-pack/plugins/canvas/public/services/stubs/reporting.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/reporting.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/services/stubs/reporting.ts
rename to x-pack/plugins/canvas/public/services/legacy/stubs/reporting.ts
diff --git a/x-pack/plugins/canvas/public/services/stubs/search.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/search.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/services/stubs/search.ts
rename to x-pack/plugins/canvas/public/services/legacy/stubs/search.ts
diff --git a/x-pack/plugins/canvas/public/services/storybook/index.ts b/x-pack/plugins/canvas/public/services/storybook/index.ts
new file mode 100644
index 0000000000000..de231f730faf5
--- /dev/null
+++ b/x-pack/plugins/canvas/public/services/storybook/index.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ PluginServiceProviders,
+ PluginServiceProvider,
+} from '../../../../../../src/plugins/presentation_util/public';
+
+import { CanvasPluginServices } from '..';
+import { pluginServiceProviders as stubProviders } from '../stubs';
+import { workpadServiceFactory } from './workpad';
+
+export interface StorybookParams {
+ hasTemplates?: boolean;
+ useStaticData?: boolean;
+ workpadCount?: number;
+}
+
+export const pluginServiceProviders: PluginServiceProviders<
+ CanvasPluginServices,
+ StorybookParams
+> = {
+ ...stubProviders,
+ workpad: new PluginServiceProvider(workpadServiceFactory),
+};
+
+export const argTypes = {
+ hasTemplates: {
+ name: 'Has templates?',
+ type: {
+ name: 'boolean',
+ },
+ defaultValue: true,
+ control: {
+ type: 'boolean',
+ },
+ },
+ useStaticData: {
+ name: 'Use static data?',
+ type: {
+ name: 'boolean',
+ },
+ defaultValue: false,
+ control: {
+ type: 'boolean',
+ },
+ },
+ workpadCount: {
+ name: 'Number of workpads',
+ type: { name: 'number' },
+ defaultValue: 5,
+ control: {
+ type: 'range',
+ },
+ },
+};
diff --git a/x-pack/plugins/canvas/public/services/storybook/workpad.ts b/x-pack/plugins/canvas/public/services/storybook/workpad.ts
new file mode 100644
index 0000000000000..a494f634141bc
--- /dev/null
+++ b/x-pack/plugins/canvas/public/services/storybook/workpad.ts
@@ -0,0 +1,100 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import moment from 'moment';
+
+import { action } from '@storybook/addon-actions';
+import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public';
+
+import { getId } from '../../lib/get_id';
+// @ts-expect-error
+import { getDefaultWorkpad } from '../../state/defaults';
+
+import { StorybookParams } from '.';
+import { CanvasWorkpadService } from '../workpad';
+
+import * as stubs from '../stubs/workpad';
+
+export {
+ findNoTemplates,
+ findNoWorkpads,
+ findSomeTemplates,
+ getNoTemplates,
+ getSomeTemplates,
+} from '../stubs/workpad';
+
+type CanvasWorkpadServiceFactory = PluginServiceFactory;
+
+const TIMEOUT = 500;
+const promiseTimeout = (time: number) => () => new Promise((resolve) => setTimeout(resolve, time));
+
+const { findNoTemplates, findNoWorkpads, findSomeTemplates } = stubs;
+
+const getRandomName = () => {
+ const lorem = 'Lorem ipsum dolor sit amet consectetur adipiscing elit Fusce lobortis aliquet arcu ut turpis duis'.split(
+ ' '
+ );
+ return [1, 2, 3].map(() => lorem[Math.floor(Math.random() * lorem.length)]).join(' ');
+};
+
+const getRandomDate = (
+ start: Date = moment().toDate(),
+ end: Date = moment().subtract(7, 'days').toDate()
+) => new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString();
+
+export const getSomeWorkpads = (count = 3) =>
+ Array.from({ length: count }, () => ({
+ '@created': getRandomDate(
+ moment().subtract(3, 'days').toDate(),
+ moment().subtract(10, 'days').toDate()
+ ),
+ '@timestamp': getRandomDate(),
+ id: getId('workpad'),
+ name: getRandomName(),
+ }));
+
+export const findSomeWorkpads = (count = 3, useStaticData = false, timeout = TIMEOUT) => (
+ _term: string
+) => {
+ return Promise.resolve()
+ .then(promiseTimeout(timeout))
+ .then(() => ({
+ total: count,
+ workpads: useStaticData ? stubs.getSomeWorkpads(count) : getSomeWorkpads(count),
+ }));
+};
+
+export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({
+ workpadCount,
+ hasTemplates,
+ useStaticData,
+}) => ({
+ get: (id: string) => {
+ action('workpadService.get')(id);
+ return Promise.resolve({ ...getDefaultWorkpad(), id });
+ },
+ findTemplates: () => {
+ action('workpadService.findTemplates')();
+ return (hasTemplates ? findSomeTemplates() : findNoTemplates())();
+ },
+ create: (workpad) => {
+ action('workpadService.create')(workpad);
+ return Promise.resolve(workpad);
+ },
+ createFromTemplate: (templateId: string) => {
+ action('workpadService.createFromTemplate')(templateId);
+ return Promise.resolve(getDefaultWorkpad());
+ },
+ find: (term: string) => {
+ action('workpadService.find')(term);
+ return (workpadCount ? findSomeWorkpads(workpadCount, useStaticData) : findNoWorkpads())(term);
+ },
+ remove: (id: string) => {
+ action('workpadService.remove')(id);
+ return Promise.resolve();
+ },
+});
diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts
index 3b00e0e6195f3..586007201db81 100644
--- a/x-pack/plugins/canvas/public/services/stubs/index.ts
+++ b/x-pack/plugins/canvas/public/services/stubs/index.ts
@@ -5,33 +5,23 @@
* 2.0.
*/
-import { CanvasServices, services } from '../';
-import { embeddablesService } from './embeddables';
-import { expressionsService } from './expressions';
-import { reportingService } from './reporting';
-import { navLinkService } from './nav_link';
-import { notifyService } from './notify';
-import { labsService } from './labs';
-import { platformService } from './platform';
-import { searchService } from './search';
-import { workpadService } from './workpad';
+export * from '../legacy/stubs';
-export const stubs: CanvasServices = {
- embeddables: embeddablesService,
- expressions: expressionsService,
- reporting: reportingService,
- navLink: navLinkService,
- notify: notifyService,
- platform: platformService,
- search: searchService,
- labs: labsService,
- workpad: workpadService,
-};
+import {
+ PluginServiceProviders,
+ PluginServiceProvider,
+ PluginServiceRegistry,
+} from '../../../../../../src/plugins/presentation_util/public';
+
+import { CanvasPluginServices } from '..';
+import { workpadServiceFactory } from './workpad';
-export const startServices = async (providedServices: Partial = {}) => {
- Object.entries(services).forEach(([key, provider]) => {
- // @ts-expect-error Object.entries isn't strongly typed
- const stub = providedServices[key] || stubs[key];
- provider.setService(stub);
- });
+export { workpadServiceFactory } from './workpad';
+
+export const pluginServiceProviders: PluginServiceProviders = {
+ workpad: new PluginServiceProvider(workpadServiceFactory),
};
+
+export const pluginServiceRegistry = new PluginServiceRegistry(
+ pluginServiceProviders
+);
diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts
index 4e3612feb67c8..eef7508e7c1eb 100644
--- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts
+++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts
@@ -7,26 +7,46 @@
import moment from 'moment';
+import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public';
+
// @ts-expect-error
import { getDefaultWorkpad } from '../../state/defaults';
-import { WorkpadService } from '../workpad';
-import { getId } from '../../lib/get_id';
+import { CanvasWorkpadService } from '../workpad';
import { CanvasTemplate } from '../../../types';
-const TIMEOUT = 500;
+type CanvasWorkpadServiceFactory = PluginServiceFactory;
+
+export const TIMEOUT = 500;
+export const promiseTimeout = (time: number) => () =>
+ new Promise((resolve) => setTimeout(resolve, time));
+
+const DAY = 86400000;
+const JAN_1_2000 = 946684800000;
-const promiseTimeout = (time: number) => () => new Promise((resolve) => setTimeout(resolve, time));
-const getName = () => {
- const lorem = 'Lorem ipsum dolor sit amet consectetur adipiscing elit Fusce lobortis aliquet arcu ut turpis duis'.split(
- ' '
- );
- return [1, 2, 3].map(() => lorem[Math.floor(Math.random() * lorem.length)]).join(' ');
+const getWorkpads = (count = 3) => {
+ const workpads = [];
+ for (let i = 0; i < count; i++) {
+ workpads[i] = {
+ ...getDefaultWorkpad(),
+ name: `Workpad ${i}`,
+ id: `workpad-${i}`,
+ '@created': moment(JAN_1_2000 + DAY * i).toDate(),
+ '@timestamp': moment(JAN_1_2000 + DAY * (i + 1)).toDate(),
+ };
+ }
+ return workpads;
};
-const randomDate = (
- start: Date = moment().toDate(),
- end: Date = moment().subtract(7, 'days').toDate()
-) => new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString();
+export const getSomeWorkpads = (count = 3) => getWorkpads(count);
+
+export const findSomeWorkpads = (count = 3, timeout = TIMEOUT) => (_term: string) => {
+ return Promise.resolve()
+ .then(promiseTimeout(timeout))
+ .then(() => ({
+ total: count,
+ workpads: getSomeWorkpads(count),
+ }));
+};
const templates: CanvasTemplate[] = [
{
@@ -45,26 +65,6 @@ const templates: CanvasTemplate[] = [
},
];
-export const getSomeWorkpads = (count = 3) =>
- Array.from({ length: count }, () => ({
- '@created': randomDate(
- moment().subtract(3, 'days').toDate(),
- moment().subtract(10, 'days').toDate()
- ),
- '@timestamp': randomDate(),
- id: getId('workpad'),
- name: getName(),
- }));
-
-export const findSomeWorkpads = (count = 3, timeout = TIMEOUT) => (_term: string) => {
- return Promise.resolve()
- .then(promiseTimeout(timeout))
- .then(() => ({
- total: count,
- workpads: getSomeWorkpads(count),
- }));
-};
-
export const findNoWorkpads = (timeout = TIMEOUT) => (_term: string) => {
return Promise.resolve()
.then(promiseTimeout(timeout))
@@ -89,11 +89,11 @@ export const findNoTemplates = (timeout = TIMEOUT) => () => {
export const getNoTemplates = () => ({ templates: [] });
export const getSomeTemplates = () => ({ templates });
-export const workpadService: WorkpadService = {
+export const workpadServiceFactory: CanvasWorkpadServiceFactory = () => ({
get: (id: string) => Promise.resolve({ ...getDefaultWorkpad(), id }),
findTemplates: findNoTemplates(),
create: (workpad) => Promise.resolve(workpad),
createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()),
find: findNoWorkpads(),
- remove: (id: string) => Promise.resolve(),
-};
+ remove: (_id: string) => Promise.resolve(),
+});
diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts
index 7d2f1550a312f..37664244b2d55 100644
--- a/x-pack/plugins/canvas/public/services/workpad.ts
+++ b/x-pack/plugins/canvas/public/services/workpad.ts
@@ -5,44 +5,7 @@
* 2.0.
*/
-import {
- API_ROUTE_WORKPAD,
- DEFAULT_WORKPAD_CSS,
- API_ROUTE_TEMPLATES,
-} from '../../common/lib/constants';
import { CanvasWorkpad, CanvasTemplate } from '../../types';
-import { CanvasServiceFactory } from './';
-
-/*
- Remove any top level keys from the workpad which will be rejected by validation
-*/
-const validKeys = [
- '@created',
- '@timestamp',
- 'assets',
- 'colors',
- 'css',
- 'variables',
- 'height',
- 'id',
- 'isWriteable',
- 'name',
- 'page',
- 'pages',
- 'width',
-];
-
-const sanitizeWorkpad = function (workpad: CanvasWorkpad) {
- const workpadKeys = Object.keys(workpad);
-
- for (const key of workpadKeys) {
- if (!validKeys.includes(key)) {
- delete (workpad as { [key: string]: any })[key];
- }
- }
-
- return workpad;
-};
export type FoundWorkpads = Array>;
export type FoundWorkpad = FoundWorkpads[number];
@@ -55,7 +18,7 @@ export interface TemplateFindResponse {
templates: CanvasTemplate[];
}
-export interface WorkpadService {
+export interface CanvasWorkpadService {
get: (id: string) => Promise;
create: (workpad: CanvasWorkpad) => Promise;
createFromTemplate: (templateId: string) => Promise;
@@ -63,50 +26,3 @@ export interface WorkpadService {
remove: (id: string) => Promise;
findTemplates: () => Promise;
}
-
-export const workpadServiceFactory: CanvasServiceFactory = (
- _coreSetup,
- coreStart,
- _setupPlugins,
- startPlugins
-): WorkpadService => {
- const getApiPath = function () {
- return `${API_ROUTE_WORKPAD}`;
- };
- return {
- get: async (id: string) => {
- const workpad = await coreStart.http.get(`${getApiPath()}/${id}`);
-
- return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad };
- },
- create: (workpad: CanvasWorkpad) => {
- return coreStart.http.post(getApiPath(), {
- body: JSON.stringify({
- ...sanitizeWorkpad({ ...workpad }),
- assets: workpad.assets || {},
- variables: workpad.variables || [],
- }),
- });
- },
- createFromTemplate: (templateId: string) => {
- return coreStart.http.post(getApiPath(), {
- body: JSON.stringify({ templateId }),
- });
- },
- findTemplates: async () => coreStart.http.get(API_ROUTE_TEMPLATES),
- find: (searchTerm: string) => {
- // TODO: this shouldn't be necessary. Check for usage.
- const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0;
-
- return coreStart.http.get(`${getApiPath()}/find`, {
- query: {
- perPage: 10000,
- name: validSearchTerm ? searchTerm : '',
- },
- });
- },
- remove: (id: string) => {
- return coreStart.http.delete(`${getApiPath()}/${id}`);
- },
- };
-};
diff --git a/x-pack/plugins/canvas/public/store.ts b/x-pack/plugins/canvas/public/store.ts
index a199599d8c0ff..e8821bafbb052 100644
--- a/x-pack/plugins/canvas/public/store.ts
+++ b/x-pack/plugins/canvas/public/store.ts
@@ -17,17 +17,16 @@ import { getInitialState } from './state/initial_state';
import { CoreSetup } from '../../../../src/core/public';
import { API_ROUTE_FUNCTIONS } from '../common/lib/constants';
-import { CanvasSetupDeps } from './plugin';
-export async function createStore(core: CoreSetup, plugins: CanvasSetupDeps) {
+export async function createStore(core: CoreSetup) {
if (getStore()) {
return cloneStore();
}
- return createFreshStore(core, plugins);
+ return createFreshStore(core);
}
-export async function createFreshStore(core: CoreSetup, plugins: CanvasSetupDeps) {
+export async function createFreshStore(core: CoreSetup) {
const initialState = getInitialState();
const basePath = core.http.basePath.get();
diff --git a/x-pack/plugins/canvas/storybook/decorators/index.ts b/x-pack/plugins/canvas/storybook/decorators/index.ts
index 598a2333be554..a4ea3226b7760 100644
--- a/x-pack/plugins/canvas/storybook/decorators/index.ts
+++ b/x-pack/plugins/canvas/storybook/decorators/index.ts
@@ -8,7 +8,7 @@
import { addDecorator } from '@storybook/react';
import { routerContextDecorator } from './router_decorator';
import { kibanaContextDecorator } from './kibana_decorator';
-import { servicesContextDecorator } from './services_decorator';
+import { servicesContextDecorator, legacyContextDecorator } from './services_decorator';
export { reduxDecorator } from './redux_decorator';
export { servicesContextDecorator } from './services_decorator';
@@ -21,5 +21,6 @@ export const addDecorators = () => {
addDecorator(kibanaContextDecorator);
addDecorator(routerContextDecorator);
- addDecorator(servicesContextDecorator());
+ addDecorator(servicesContextDecorator);
+ addDecorator(legacyContextDecorator());
};
diff --git a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
index def5a5681a8c4..fbc3f140bffcc 100644
--- a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
+++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
@@ -7,40 +7,34 @@
import React from 'react';
-import {
- CanvasServiceFactory,
- CanvasServiceProvider,
- ServicesProvider,
-} from '../../public/services';
-import {
- findNoWorkpads,
- findSomeWorkpads,
- workpadService,
- findSomeTemplates,
- findNoTemplates,
-} from '../../public/services/stubs/workpad';
-import { WorkpadService } from '../../public/services/workpad';
-
-interface Params {
- findWorkpads?: number;
- findTemplates?: boolean;
-}
-
-export const servicesContextDecorator = ({
- findWorkpads = 0,
- findTemplates: findTemplatesOption = false,
-}: Params = {}) => {
- const workpadServiceFactory: CanvasServiceFactory = (): WorkpadService => ({
- ...workpadService,
- find: findWorkpads > 0 ? findSomeWorkpads(findWorkpads) : findNoWorkpads(),
- findTemplates: findTemplatesOption ? findSomeTemplates() : findNoTemplates(),
- });
-
- const workpad = new CanvasServiceProvider(workpadServiceFactory);
- // @ts-expect-error This is a hack at the moment, until we can get Canvas moved over to the new services architecture.
- workpad.start();
-
- return (story: Function) => (
- {story()}
+import { DecoratorFn } from '@storybook/react';
+import { I18nProvider } from '@kbn/i18n/react';
+
+import { PluginServiceRegistry } from '../../../../../src/plugins/presentation_util/public';
+import { pluginServices, LegacyServicesProvider } from '../../public/services';
+import { CanvasPluginServices } from '../../public/services';
+import { pluginServiceProviders, StorybookParams } from '../../public/services/storybook';
+
+export const servicesContextDecorator: DecoratorFn = (story: Function, storybook) => {
+ if (process.env.JEST_WORKER_ID !== undefined) {
+ storybook.args.useStaticData = true;
+ }
+
+ const pluginServiceRegistry = new PluginServiceRegistry(
+ pluginServiceProviders
+ );
+
+ pluginServices.setRegistry(pluginServiceRegistry.start(storybook.args));
+
+ const ContextProvider = pluginServices.getContextProvider();
+
+ return (
+
+ {story()}
+
);
};
+
+export const legacyContextDecorator = () => (story: Function) => (
+ {story()}
+);
diff --git a/x-pack/plugins/canvas/storybook/index.ts b/x-pack/plugins/canvas/storybook/index.ts
index ff60b84c88a69..01dda057dac81 100644
--- a/x-pack/plugins/canvas/storybook/index.ts
+++ b/x-pack/plugins/canvas/storybook/index.ts
@@ -9,7 +9,9 @@ import { ACTIONS_PANEL_ID } from './addon/src/constants';
export * from './decorators';
export { ACTIONS_PANEL_ID } from './addon/src/constants';
+
export const getAddonPanelParameters = () => ({ options: { selectedPanel: ACTIONS_PANEL_ID } });
+
export const getDisableStoryshotsParameter = () => ({
storyshots: {
disable: true,
diff --git a/x-pack/plugins/canvas/storybook/preview.ts b/x-pack/plugins/canvas/storybook/preview.ts
index f885a654cdab8..266ff767c566a 100644
--- a/x-pack/plugins/canvas/storybook/preview.ts
+++ b/x-pack/plugins/canvas/storybook/preview.ts
@@ -6,6 +6,7 @@
*/
import { action } from '@storybook/addon-actions';
+import { addParameters } from '@storybook/react';
import { startServices } from '../public/services/stubs';
import { addDecorators } from './decorators';
@@ -23,3 +24,6 @@ startServices({
});
addDecorators();
+addParameters({
+ controls: { hideNoControlsWarning: true },
+});
diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx
index 7f0ea077c7569..84ac1a26281e0 100644
--- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx
+++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx
@@ -118,7 +118,7 @@ addSerializer(styleSheetSerializer);
initStoryshots({
configPath: path.resolve(__dirname),
framework: 'react',
- test: multiSnapshotWithOptions({}),
+ test: multiSnapshotWithOptions(),
// Don't snapshot tests that start with 'redux'
storyNameRegex: /^((?!.*?redux).)*$/,
});
From 54dae304ccfd04d4814655f6bfe51a15ae915831 Mon Sep 17 00:00:00 2001
From: Brandon Kobel
Date: Wed, 30 Jun 2021 14:11:59 -0700
Subject: [PATCH 12/51] Update docs to explicitly state supported upgrade
version (#103774)
* Update docs to explicitly state supported upgrade version
* Update docs/setup/upgrade.asciidoc
Co-authored-by: Kaarina Tungseth
Co-authored-by: Kaarina Tungseth
---
docs/setup/upgrade.asciidoc | 50 +++++++++++++++++++++++++++++++++++--
1 file changed, 48 insertions(+), 2 deletions(-)
diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc
index 92cd6e9ead5a1..bd93517a7a82f 100644
--- a/docs/setup/upgrade.asciidoc
+++ b/docs/setup/upgrade.asciidoc
@@ -1,8 +1,54 @@
[[upgrade]]
== Upgrade {kib}
-Depending on the {kib} version you're upgrading from, the upgrade process to 7.0
-varies.
+Depending on the {kib} version you're upgrading from, the upgrade process to {version}
+varies. The following upgrades are supported:
+
+* Between minor versions
+* From 5.6 to 6.8
+* From 6.8 to {prev-major-version}
+* From {prev-major-version} to {version}
+ifeval::[ "{version}" != "{minor-version}.0" ]
+* From any version since {minor-version}.0 to {version}
+endif::[]
+
+The following table shows the recommended upgrade paths to {version}.
+
+[cols="<1,3",options="header",]
+|====
+|Upgrade from
+|Recommended upgrade path to {version}
+
+ifeval::[ "{version}" != "{minor-version}.0" ]
+|A previous {minor-version} version (e.g., {minor-version}.0)
+|Upgrade to {version}
+endif::[]
+
+|{prev-major-version}
+|Upgrade to {version}
+
+|7.0–7.7
+a|
+. Upgrade to {prev-major-version}
+. Upgrade to {version}
+
+|6.8
+a|
+. Upgrade to {prev-major-version}
+. Upgrade to {version}
+
+|6.0–6.7
+a|
+
+. Upgrade to 6.8
+. Upgrade to {prev-major-version}
+. Upgrade to {version}
+|====
+
+[WARNING]
+====
+The upgrade path from 6.8 to 7.0 is *not* supported.
+====
[float]
[[upgrade-before-you-begin]]
From f65eaa2c49a1c098df171eb210c9569bbe939b55 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Casper=20H=C3=BCbertz?=
Date: Wed, 30 Jun 2021 23:22:14 +0200
Subject: [PATCH 13/51] [APM] Fix prepend form label background (#103983)
---
.../apm/public/components/shared/time_comparison/index.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx
index cbe7b81486a64..ed9d1a15cdbca 100644
--- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx
@@ -26,7 +26,8 @@ const PrependContainer = euiStyled.div`
display: flex;
justify-content: center;
align-items: center;
- background-color: ${({ theme }) => theme.eui.euiGradientMiddle};
+ background-color: ${({ theme }) =>
+ theme.eui.euiFormInputGroupLabelBackground};
padding: 0 ${px(unit)};
`;
From 12e7fe50bb4367893e8fef9b94b3bb28a92bd75a Mon Sep 17 00:00:00 2001
From: Frank Hassanabad
Date: Wed, 30 Jun 2021 15:50:05 -0600
Subject: [PATCH 14/51] [Security Solutions][Detection Engine] Adds a merge
strategy key to kibana.yml and updates docker to have missing keys from
security solutions (#103800)
## Summary
This is a follow up considered critical addition to:
https://github.com/elastic/kibana/pull/102280
This adds a key of `xpack.securitySolution.alertMergeStrategy` to `kibana.yml` which allows users to change their merge strategy between their raw events and the signals/alerts that are generated. This also adds additional security keys to the docker container that were overlooked in the past from security solutions.
The values you can use and add to to `xpack.securitySolution.alertMergeStrategy` are:
* missingFields (The default)
* allFields
* noFields
## missingFields
The default merge strategy we are using starting with 7.14 which will merge any primitive data types from the [fields API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#search-fields-param) into the resulting signal/alert. This will copy over fields such as `constant_keyword`, `copy_to`, `runtime fields`, `field aliases` which previously were not copied over as long as they are primitive data types such as `keyword`, `text`, `numeric` and are not found in your original `_source` document. This will not copy copy `geo points`, `nested objects`, and in some cases if your `_source` contains arrays or top level objects or conflicts/ambiguities it will not merge them. This will _not_ merge existing values between `_source` and `fields` for `runtime fields` as well. It only merges missing primitive data types.
## allFields
A very aggressive merge strategy which should be considered experimental. It will do everything `missingFields` does but in addition to that it will merge existing values between `_source` and `fields` which means if you change values or override values with `runtime fields` this strategy will attempt to merge those values. This will also merge in most instances your nested fields but it will not merge `geo` data types due to ambiguities. If you have multi-fields this will choose your default field and merge that into `_source`. This can change a lot your data between your original `_source` and `fields` when the data is copied into an alert/signal which is why it is considered an aggressive merge strategy.
Both these strategies attempts to unbox single array elements when it makes sense and assumes you only want values in an array when it sees them in `_source` or if it sees multiple elements within an array.
## noFields
The behavior before https://github.com/elastic/kibana/pull/102280 was introduced and is a do nothing strategy. This should only be used if you are seeing problems with alerts/signals being inserted due to conflicts and/or bugs for some reason with `missingFields`. We are not anticipating this, but if you are setting `noFields` please reach out to our [forums](https://discuss.elastic.co/c/security/83) and let us know we have a bug so we can fix it. If you are encountering undesired merge behaviors or have other strategies you want us to implement let us know on the forums as well.
The missing keys added for docker are:
* xpack.securitySolution.alertMergeStrategy
* xpack.securitySolution.alertResultListDefaultDateRange
* xpack.securitySolution.endpointResultListDefaultFirstPageIndex
* xpack.securitySolution.endpointResultListDefaultPageSize
* xpack.securitySolution.maxRuleImportExportSize
* xpack.securitySolution.maxRuleImportPayloadBytes
* xpack.securitySolution.maxTimelineImportExportSize
* xpack.securitySolution.maxTimelineImportPayloadBytes
* xpack.securitySolution.packagerTaskInterval
* xpack.securitySolution.validateArtifactDownloads
I intentionally skipped adding the other `kibana.yml` keys which are considered either experimental flags or are for internal developers and are not documented and not supported in production by us.
## Manual testing of the different strategies
First add this mapping and document in the dev tools for basic tests
```json
# Mapping with two constant_keywords and a runtime field
DELETE frank-test-delme-17
PUT frank-test-delme-17
{
"mappings": {
"dynamic": "strict",
"runtime": {
"host.name": {
"type": "keyword",
"script": {
"source": "emit('changed_hostname')"
}
}
},
"properties": {
"@timestamp": {
"type": "date"
},
"host": {
"properties": {
"name": {
"type": "keyword"
}
}
},
"data_stream": {
"properties": {
"dataset": {
"type": "constant_keyword",
"value": "datastream_dataset_name_1"
},
"module": {
"type": "constant_keyword",
"value": "datastream_module_name_1"
}
}
},
"event": {
"properties": {
"dataset": {
"type": "constant_keyword",
"value": "event_dataset_name_1"
},
"module": {
"type": "constant_keyword",
"value": "event_module_name_1"
}
}
}
}
}
}
# Document without an existing host.name
PUT frank-test-delme-17/_doc/1
{
"@timestamp": "2021-06-30T15:46:31.800Z"
}
# Document with an existing host.name
PUT frank-test-delme-17/_doc/2
{
"@timestamp": "2021-06-30T15:46:31.800Z",
"host": {
"name": "host_name"
}
}
# Query it to ensure the fields is returned with data that does not exist in _soruce
GET frank-test-delme-17/_search
{
"fields": [
{
"field": "*"
}
]
}
```
For all the different key combinations do the following:
Run a single detection rule against the index:
Ensure two signals are created:
If your `kibana.yml` or `kibana.dev.yml` you set this key (or omit it as it is the default):
```yml
xpack.securitySolution.alertMergeStrategy: 'missingFields'
```
When you click on each signal you should see that `event.module` and `event.dataset` were copied over as well as `data_stream.dataset` and `data_stream.module` since they're `constant_keyword`:
However since this only merges missing fields, you should see that in the first record the `host.name` is the runtime field defined since `host.name` does not exist in `_source` and that in the second record it still shows up as `host_name` since we do not override merges right now:
First:
Second:
When you set in your `kibana.yml` or `kibana.dev.yml` this key:
```yml
xpack.securitySolution.alertMergeStrategy: 'noFields'
```
Expect that your `event.module`, `event.dataset`, `data_stream.module`, `data_stream.dataset` are all non-existent since we do not copy anything over from `fields` at all and only use things within `_source`:
Expect that `host.name` is missing in the first record and has the default value in the second:
First:
Second:
When you set in your `kibana.yml` or `kibana.dev.yml` this key:
```yml
xpack.securitySolution.alertMergeStrategy: 'allFields'
```
Expect that `event.module` and `event.dataset` were copied over as well as `data_stream.dataset` and `data_stream.module` since they're `constant_keyword`:
Expect that both the first and second records contain the runtime field since we merge both of them:
### Checklist
Delete any items that are not applicable to this PR.
- [x] If a plugin configuration key changed, check if it needs to be allowlisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker)
---
.../resources/base/bin/kibana-docker | 10 ++++
.../security_solution/server/config.ts | 6 +++
.../routes/__mocks__/index.ts | 1 +
.../signals/build_bulk_body.test.ts | 49 ++++++++++++++++---
.../signals/build_bulk_body.ts | 18 ++++---
.../signals/search_after_bulk_create.test.ts | 1 +
.../signals/signal_rule_alert_type.test.ts | 1 +
.../signals/signal_rule_alert_type.ts | 5 ++
.../strategies/get_strategy.ts | 31 ++++++++++++
.../source_fields_merging/strategies/index.ts | 1 +
.../merge_all_fields_with_source.ts | 6 +--
.../merge_missing_fields_with_source.ts | 10 ++--
.../strategies/merge_no_fields.ts | 15 ++++++
.../signals/source_fields_merging/types.ts | 7 +++
.../signals/wrap_hits_factory.ts | 5 +-
.../signals/wrap_sequences_factory.ts | 5 +-
.../security_solution/server/plugin.ts | 1 +
17 files changed, 145 insertions(+), 27 deletions(-)
create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/get_strategy.ts
create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
index a224793bace3f..643080fda381f 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
@@ -380,6 +380,16 @@ kibana_vars=(
xpack.security.session.idleTimeout
xpack.security.session.lifespan
xpack.security.sessionTimeout
+ xpack.securitySolution.alertMergeStrategy
+ xpack.securitySolution.alertResultListDefaultDateRange
+ xpack.securitySolution.endpointResultListDefaultFirstPageIndex
+ xpack.securitySolution.endpointResultListDefaultPageSize
+ xpack.securitySolution.maxRuleImportExportSize
+ xpack.securitySolution.maxRuleImportPayloadBytes
+ xpack.securitySolution.maxTimelineImportExportSize
+ xpack.securitySolution.maxTimelineImportPayloadBytes
+ xpack.securitySolution.packagerTaskInterval
+ xpack.securitySolution.validateArtifactDownloads
xpack.spaces.enabled
xpack.spaces.maxSpaces
xpack.task_manager.enabled
diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts
index 8dfe56a1a54f4..d19c36ad21eda 100644
--- a/x-pack/plugins/security_solution/server/config.ts
+++ b/x-pack/plugins/security_solution/server/config.ts
@@ -21,6 +21,12 @@ export const configSchema = schema.object({
maxRuleImportPayloadBytes: schema.number({ defaultValue: 10485760 }),
maxTimelineImportExportSize: schema.number({ defaultValue: 10000 }),
maxTimelineImportPayloadBytes: schema.number({ defaultValue: 10485760 }),
+ alertMergeStrategy: schema.oneOf(
+ [schema.literal('allFields'), schema.literal('missingFields'), schema.literal('noFields')],
+ {
+ defaultValue: 'missingFields',
+ }
+ ),
[SIGNALS_INDEX_KEY]: schema.string({ defaultValue: DEFAULT_SIGNALS_INDEX }),
/**
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts
index 2e72ac137adcf..084105b7d1c49 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts
@@ -30,6 +30,7 @@ export const createMockConfig = (): ConfigType => ({
},
packagerTaskInterval: '60s',
validateArtifactDownloads: true,
+ alertMergeStrategy: 'missingFields',
});
export const mockGetCurrentUser = {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts
index 4053d64539c49..117dcdf0c18da 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts
@@ -38,7 +38,11 @@ describe('buildBulkBody', () => {
const ruleSO = sampleRuleSO(getQueryRuleParams());
const doc = sampleDocNoSortId();
delete doc._source.source;
- const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody(ruleSO, doc);
+ const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody(
+ ruleSO,
+ doc,
+ 'missingFields'
+ );
// Timestamp will potentially always be different so remove it for the test
delete fakeSignalSourceHit['@timestamp'];
const expected: Omit & { someKey: 'someValue' } = {
@@ -102,7 +106,11 @@ describe('buildBulkBody', () => {
},
};
delete doc._source.source;
- const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody(ruleSO, doc);
+ const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody(
+ ruleSO,
+ doc,
+ 'missingFields'
+ );
// Timestamp will potentially always be different so remove it for the test
delete fakeSignalSourceHit['@timestamp'];
const expected: Omit & { someKey: 'someValue' } = {
@@ -180,7 +188,11 @@ describe('buildBulkBody', () => {
dataset: 'socket',
kind: 'event',
};
- const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody(ruleSO, doc);
+ const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody(
+ ruleSO,
+ doc,
+ 'missingFields'
+ );
// Timestamp will potentially always be different so remove it for the test
delete fakeSignalSourceHit['@timestamp'];
const expected: Omit & { someKey: 'someValue' } = {
@@ -244,7 +256,11 @@ describe('buildBulkBody', () => {
module: 'system',
dataset: 'socket',
};
- const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody(ruleSO, doc);
+ const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody(
+ ruleSO,
+ doc,
+ 'missingFields'
+ );
// Timestamp will potentially always be different so remove it for the test
delete fakeSignalSourceHit['@timestamp'];
const expected: Omit & { someKey: 'someValue' } = {
@@ -305,7 +321,11 @@ describe('buildBulkBody', () => {
doc._source.event = {
kind: 'event',
};
- const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody(ruleSO, doc);
+ const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody(
+ ruleSO,
+ doc,
+ 'missingFields'
+ );
// Timestamp will potentially always be different so remove it for the test
delete fakeSignalSourceHit['@timestamp'];
const expected: Omit & { someKey: 'someValue' } = {
@@ -365,7 +385,11 @@ describe('buildBulkBody', () => {
signal: 123,
},
} as unknown) as SignalSourceHit;
- const { '@timestamp': timestamp, ...fakeSignalSourceHit } = buildBulkBody(ruleSO, doc);
+ const { '@timestamp': timestamp, ...fakeSignalSourceHit } = buildBulkBody(
+ ruleSO,
+ doc,
+ 'missingFields'
+ );
const expected: Omit & { someKey: string } = {
someKey: 'someValue',
event: {
@@ -421,7 +445,11 @@ describe('buildBulkBody', () => {
signal: { child_1: { child_2: 'nested data' } },
},
} as unknown) as SignalSourceHit;
- const { '@timestamp': timestamp, ...fakeSignalSourceHit } = buildBulkBody(ruleSO, doc);
+ const { '@timestamp': timestamp, ...fakeSignalSourceHit } = buildBulkBody(
+ ruleSO,
+ doc,
+ 'missingFields'
+ );
const expected: Omit & { someKey: string } = {
someKey: 'someValue',
event: {
@@ -645,7 +673,12 @@ describe('buildSignalFromEvent', () => {
const ancestor = sampleDocWithAncestors().hits.hits[0];
delete ancestor._source.source;
const ruleSO = sampleRuleSO(getQueryRuleParams());
- const signal: SignalHitOptionalTimestamp = buildSignalFromEvent(ancestor, ruleSO, true);
+ const signal: SignalHitOptionalTimestamp = buildSignalFromEvent(
+ ancestor,
+ ruleSO,
+ true,
+ 'missingFields'
+ );
// Timestamp will potentially always be different so remove it for the test
delete signal['@timestamp'];
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts
index 819e1f3eb6df1..2e6f4b9303d89 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts
@@ -6,7 +6,7 @@
*/
import { SavedObject } from 'src/core/types';
-import { mergeMissingFieldsWithSource } from './source_fields_merging/strategies/merge_missing_fields_with_source';
+import { getMergeStrategy } from './source_fields_merging/strategies';
import {
AlertAttributes,
SignalSourceHit,
@@ -21,6 +21,7 @@ import { additionalSignalFields, buildSignal } from './build_signal';
import { buildEventTypeSignal } from './build_event_type_signal';
import { EqlSequence } from '../../../../common/detection_engine/types';
import { generateSignalId, wrapBuildingBlocks, wrapSignal } from './utils';
+import type { ConfigType } from '../../../config';
/**
* Formats the search_after result for insertion into the signals index. We first create a
@@ -33,9 +34,10 @@ import { generateSignalId, wrapBuildingBlocks, wrapSignal } from './utils';
*/
export const buildBulkBody = (
ruleSO: SavedObject,
- doc: SignalSourceHit
+ doc: SignalSourceHit,
+ mergeStrategy: ConfigType['alertMergeStrategy']
): SignalHit => {
- const mergedDoc = mergeMissingFieldsWithSource({ doc });
+ const mergedDoc = getMergeStrategy(mergeStrategy)({ doc });
const rule = buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {});
const signal: Signal = {
...buildSignal([mergedDoc], rule),
@@ -65,11 +67,12 @@ export const buildBulkBody = (
export const buildSignalGroupFromSequence = (
sequence: EqlSequence,
ruleSO: SavedObject,
- outputIndex: string
+ outputIndex: string,
+ mergeStrategy: ConfigType['alertMergeStrategy']
): WrappedSignalHit[] => {
const wrappedBuildingBlocks = wrapBuildingBlocks(
sequence.events.map((event) => {
- const signal = buildSignalFromEvent(event, ruleSO, false);
+ const signal = buildSignalFromEvent(event, ruleSO, false, mergeStrategy);
signal.signal.rule.building_block_type = 'default';
return signal;
}),
@@ -130,9 +133,10 @@ export const buildSignalFromSequence = (
export const buildSignalFromEvent = (
event: BaseSignalHit,
ruleSO: SavedObject,
- applyOverrides: boolean
+ applyOverrides: boolean,
+ mergeStrategy: ConfigType['alertMergeStrategy']
): SignalHit => {
- const mergedEvent = mergeMissingFieldsWithSource({ doc: event });
+ const mergedEvent = getMergeStrategy(mergeStrategy)({ doc: event });
const rule = applyOverrides
? buildRuleWithOverrides(ruleSO, mergedEvent._source ?? {})
: buildRuleWithoutOverrides(ruleSO);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
index 21c1402861e6e..dc03f1bc964f2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
@@ -69,6 +69,7 @@ describe('searchAfterAndBulkCreate', () => {
wrapHits = wrapHitsFactory({
ruleSO,
signalsIndex: DEFAULT_SIGNALS_INDEX,
+ mergeStrategy: 'missingFields',
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
index d8c919b50e9db..39aebb4aa4555 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
@@ -192,6 +192,7 @@ describe('signal_rule_alert_type', () => {
version,
ml: mlMock,
lists: listMock.createSetup(),
+ mergeStrategy: 'missingFields',
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index ba665fa43e8b8..6eef97b05b697 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -68,6 +68,7 @@ import {
import { bulkCreateFactory } from './bulk_create_factory';
import { wrapHitsFactory } from './wrap_hits_factory';
import { wrapSequencesFactory } from './wrap_sequences_factory';
+import { ConfigType } from '../../../config';
export const signalRulesAlertType = ({
logger,
@@ -75,12 +76,14 @@ export const signalRulesAlertType = ({
version,
ml,
lists,
+ mergeStrategy,
}: {
logger: Logger;
eventsTelemetry: TelemetryEventsSender | undefined;
version: string;
ml: SetupPlugins['ml'];
lists: SetupPlugins['lists'] | undefined;
+ mergeStrategy: ConfigType['alertMergeStrategy'];
}): SignalRuleAlertTypeDefinition => {
return {
id: SIGNALS_ID,
@@ -233,11 +236,13 @@ export const signalRulesAlertType = ({
const wrapHits = wrapHitsFactory({
ruleSO: savedObject,
signalsIndex: params.outputIndex,
+ mergeStrategy,
});
const wrapSequences = wrapSequencesFactory({
ruleSO: savedObject,
signalsIndex: params.outputIndex,
+ mergeStrategy,
});
if (isMlRule(type)) {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/get_strategy.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/get_strategy.ts
new file mode 100644
index 0000000000000..3c4b1cd0ef373
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/get_strategy.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { assertUnreachable } from '../../../../../../common';
+import type { ConfigType } from '../../../../../config';
+import { MergeStrategyFunction } from '../types';
+import { mergeAllFieldsWithSource } from './merge_all_fields_with_source';
+import { mergeMissingFieldsWithSource } from './merge_missing_fields_with_source';
+import { mergeNoFields } from './merge_no_fields';
+
+export const getMergeStrategy = (
+ mergeStrategy: ConfigType['alertMergeStrategy']
+): MergeStrategyFunction => {
+ switch (mergeStrategy) {
+ case 'allFields': {
+ return mergeAllFieldsWithSource;
+ }
+ case 'missingFields': {
+ return mergeMissingFieldsWithSource;
+ }
+ case 'noFields': {
+ return mergeNoFields;
+ }
+ default:
+ return assertUnreachable(mergeStrategy);
+ }
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/index.ts
index 212eba9c6c3be..60460ad5f2e00 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/index.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/index.ts
@@ -6,3 +6,4 @@
*/
export * from './merge_all_fields_with_source';
export * from './merge_missing_fields_with_source';
+export * from './get_strategy';
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts
index de8d3ba820e23..da2eea9d2c61e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts
@@ -7,9 +7,9 @@
import { get } from 'lodash/fp';
import { set } from '@elastic/safer-lodash-set/fp';
-import { SignalSource, SignalSourceHit } from '../../types';
+import { SignalSource } from '../../types';
import { filterFieldEntries } from '../utils/filter_field_entries';
-import type { FieldsType } from '../types';
+import type { FieldsType, MergeStrategyFunction } from '../types';
import { isObjectLikeOrArrayOfObjectLikes } from '../utils/is_objectlike_or_array_of_objectlikes';
import { isNestedObject } from '../utils/is_nested_object';
import { recursiveUnboxingFields } from '../utils/recursive_unboxing_fields';
@@ -26,7 +26,7 @@ import { isTypeObject } from '../utils/is_type_object';
* @param throwOnFailSafe Defaults to false, but if set to true it will cause a throw if the fail safe is triggered to indicate we need to add a new explicit test condition
* @returns The two merged together in one object where we can
*/
-export const mergeAllFieldsWithSource = ({ doc }: { doc: SignalSourceHit }): SignalSourceHit => {
+export const mergeAllFieldsWithSource: MergeStrategyFunction = ({ doc }) => {
const source = doc._source ?? {};
const fields = doc.fields ?? {};
const fieldEntries = Object.entries(fields);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts
index bf541acbe7e33..b66c46ccbf0ca 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts
@@ -7,9 +7,9 @@
import { get } from 'lodash/fp';
import { set } from '@elastic/safer-lodash-set/fp';
-import { SignalSource, SignalSourceHit } from '../../types';
+import { SignalSource } from '../../types';
import { filterFieldEntries } from '../utils/filter_field_entries';
-import type { FieldsType } from '../types';
+import type { FieldsType, MergeStrategyFunction } from '../types';
import { recursiveUnboxingFields } from '../utils/recursive_unboxing_fields';
import { isTypeObject } from '../utils/is_type_object';
import { arrayInPathExists } from '../utils/array_in_path_exists';
@@ -22,11 +22,7 @@ import { isNestedObject } from '../utils/is_nested_object';
* @param throwOnFailSafe Defaults to false, but if set to true it will cause a throw if the fail safe is triggered to indicate we need to add a new explicit test condition
* @returns The two merged together in one object where we can
*/
-export const mergeMissingFieldsWithSource = ({
- doc,
-}: {
- doc: SignalSourceHit;
-}): SignalSourceHit => {
+export const mergeMissingFieldsWithSource: MergeStrategyFunction = ({ doc }) => {
const source = doc._source ?? {};
const fields = doc.fields ?? {};
const fieldEntries = Object.entries(fields);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts
new file mode 100644
index 0000000000000..6c2daf2526715
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { MergeStrategyFunction } from '../types';
+
+/**
+ * Does nothing and does not merge source with fields
+ * @param doc The doc to return and do nothing
+ * @returns The doc as a no operation and do nothing
+ */
+export const mergeNoFields: MergeStrategyFunction = ({ doc }) => doc;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts
index e8142e41715e2..1438d2844949c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts
@@ -5,7 +5,14 @@
* 2.0.
*/
+import { SignalSourceHit } from '../types';
+
/**
* A bit stricter typing since the default fields type is an "any"
*/
export type FieldsType = string[] | number[] | boolean[] | object[];
+
+/**
+ * The type of the merge strategy functions which must implement to be part of the strategy group
+ */
+export type MergeStrategyFunction = ({ doc }: { doc: SignalSourceHit }) => SignalSourceHit;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts
index d5c05bc890332..b28c46aae8f82 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts
@@ -9,13 +9,16 @@ import { SearchAfterAndBulkCreateParams, WrapHits, WrappedSignalHit } from './ty
import { generateId } from './utils';
import { buildBulkBody } from './build_bulk_body';
import { filterDuplicateSignals } from './filter_duplicate_signals';
+import type { ConfigType } from '../../../config';
export const wrapHitsFactory = ({
ruleSO,
signalsIndex,
+ mergeStrategy,
}: {
ruleSO: SearchAfterAndBulkCreateParams['ruleSO'];
signalsIndex: string;
+ mergeStrategy: ConfigType['alertMergeStrategy'];
}): WrapHits => (events) => {
const wrappedDocs: WrappedSignalHit[] = events.flatMap((doc) => [
{
@@ -26,7 +29,7 @@ export const wrapHitsFactory = ({
String(doc._version),
ruleSO.attributes.params.ruleId ?? ''
),
- _source: buildBulkBody(ruleSO, doc),
+ _source: buildBulkBody(ruleSO, doc, mergeStrategy),
},
]);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts
index c53ea7b7ebe72..f0b9e64047692 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts
@@ -7,18 +7,21 @@
import { SearchAfterAndBulkCreateParams, WrappedSignalHit, WrapSequences } from './types';
import { buildSignalGroupFromSequence } from './build_bulk_body';
+import { ConfigType } from '../../../config';
export const wrapSequencesFactory = ({
ruleSO,
signalsIndex,
+ mergeStrategy,
}: {
ruleSO: SearchAfterAndBulkCreateParams['ruleSO'];
signalsIndex: string;
+ mergeStrategy: ConfigType['alertMergeStrategy'];
}): WrapSequences => (sequences) =>
sequences.reduce(
(acc: WrappedSignalHit[], sequence) => [
...acc,
- ...buildSignalGroupFromSequence(sequence, ruleSO, signalsIndex),
+ ...buildSignalGroupFromSequence(sequence, ruleSO, signalsIndex, mergeStrategy),
],
[]
);
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 2f523d9d9969d..2f3850ff49f4c 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -387,6 +387,7 @@ export class Plugin implements IPlugin
Date: Thu, 1 Jul 2021 00:09:26 +0200
Subject: [PATCH 15/51] [Cloud] Fix sessions stitching across domains (#103964)
---
x-pack/plugins/cloud/public/fullstory.ts | 39 ++++++++++------------
x-pack/plugins/cloud/public/plugin.test.ts | 13 +++-----
x-pack/plugins/cloud/public/plugin.ts | 9 +++--
3 files changed, 28 insertions(+), 33 deletions(-)
diff --git a/x-pack/plugins/cloud/public/fullstory.ts b/x-pack/plugins/cloud/public/fullstory.ts
index 31e5ec128b9a3..b118688f31ae1 100644
--- a/x-pack/plugins/cloud/public/fullstory.ts
+++ b/x-pack/plugins/cloud/public/fullstory.ts
@@ -12,7 +12,7 @@ export interface FullStoryDeps {
basePath: IBasePath;
orgId: string;
packageInfo: PackageInfo;
- userIdPromise: Promise;
+ userId?: string;
}
interface FullStoryApi {
@@ -24,7 +24,7 @@ export const initializeFullStory = async ({
basePath,
orgId,
packageInfo,
- userIdPromise,
+ userId,
}: FullStoryDeps) => {
// @ts-expect-error
window._fs_debug = false;
@@ -73,28 +73,23 @@ export const initializeFullStory = async ({
/* eslint-enable */
// @ts-expect-error
- const fullstory: FullStoryApi = window.FSKibana;
+ const fullStory: FullStoryApi = window.FSKibana;
+
+ try {
+ // This needs to be called syncronously to be sure that we populate the user ID soon enough to make sessions merging
+ // across domains work
+ if (!userId) return;
+ // Do the hashing here to keep it at clear as possible in our source code that we do not send literal user IDs
+ const hashedId = sha256(userId.toString());
+ fullStory.identify(hashedId);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(`[cloud.full_story] Could not call FS.identify due to error: ${e.toString()}`, e);
+ }
// Record an event that Kibana was opened so we can easily search for sessions that use Kibana
- // @ts-expect-error
- window.FSKibana.event('Loaded Kibana', {
+ fullStory.event('Loaded Kibana', {
+ // `str` suffix is required, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234
kibana_version_str: packageInfo.version,
});
-
- // Use a promise here so we don't have to wait to retrieve the user to start recording the session
- userIdPromise
- .then((userId) => {
- if (!userId) return;
- // Do the hashing here to keep it at clear as possible in our source code that we do not send literal user IDs
- const hashedId = sha256(userId.toString());
- // @ts-expect-error
- window.FSKibana.identify(hashedId);
- })
- .catch((e) => {
- // eslint-disable-next-line no-console
- console.error(
- `[cloud.full_story] Could not call FS.identify due to error: ${e.toString()}`,
- e
- );
- });
};
diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts
index af4d3c4c9005d..264ae61c050e8 100644
--- a/x-pack/plugins/cloud/public/plugin.test.ts
+++ b/x-pack/plugins/cloud/public/plugin.test.ts
@@ -63,16 +63,11 @@ describe('Cloud Plugin', () => {
});
expect(initializeFullStoryMock).toHaveBeenCalled();
- const {
- basePath,
- orgId,
- packageInfo,
- userIdPromise,
- } = initializeFullStoryMock.mock.calls[0][0];
+ const { basePath, orgId, packageInfo, userId } = initializeFullStoryMock.mock.calls[0][0];
expect(basePath.prepend).toBeDefined();
expect(orgId).toEqual('foo');
expect(packageInfo).toEqual(initContext.env.packageInfo);
- expect(await userIdPromise).toEqual('1234');
+ expect(userId).toEqual('1234');
});
it('passes undefined user ID when security is not available', async () => {
@@ -82,9 +77,9 @@ describe('Cloud Plugin', () => {
});
expect(initializeFullStoryMock).toHaveBeenCalled();
- const { orgId, userIdPromise } = initializeFullStoryMock.mock.calls[0][0];
+ const { orgId, userId } = initializeFullStoryMock.mock.calls[0][0];
expect(orgId).toEqual('foo');
- expect(await userIdPromise).toEqual(undefined);
+ expect(userId).toEqual(undefined);
});
it('does not call initializeFullStory when enabled=false', async () => {
diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts
index 68dece1bc5d3d..98017d09ef807 100644
--- a/x-pack/plugins/cloud/public/plugin.ts
+++ b/x-pack/plugins/cloud/public/plugin.ts
@@ -166,16 +166,21 @@ export class CloudPlugin implements Plugin {
}
// Keep this import async so that we do not load any FullStory code into the browser when it is disabled.
- const { initializeFullStory } = await import('./fullstory');
+ const fullStoryChunkPromise = import('./fullstory');
const userIdPromise: Promise = security
? loadFullStoryUserId({ getCurrentUser: security.authc.getCurrentUser })
: Promise.resolve(undefined);
+ const [{ initializeFullStory }, userId] = await Promise.all([
+ fullStoryChunkPromise,
+ userIdPromise,
+ ]);
+
initializeFullStory({
basePath,
orgId,
packageInfo: this.initializerContext.env.packageInfo,
- userIdPromise,
+ userId,
});
}
}
From aa5c56c41866fddf95bf0733edec683ddb54a3b0 Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Wed, 30 Jun 2021 18:29:25 -0400
Subject: [PATCH 16/51] [Security Solution][Hosts] Show Fleet Agent status and
Isolation status for Endpoint Hosts when on the Host Details page (#103781)
* Refactor: extract agent status to endpoint host status to reusable utiltiy
* Show Fleet Agent status + isolation status
* Refactor EndpoinAgentStatus component to use `` common component
* Move actions service to `endpoint/services` directory
* Add pending actions to the search strategy for endpoint data
---
.../security_solution/hosts/common/index.ts | 6 +
.../view/components/endpoint_agent_status.tsx | 16 +--
.../endpoint_overview/index.test.tsx | 69 ++++++++----
.../host_overview/endpoint_overview/index.tsx | 21 +++-
.../endpoint_overview/translations.ts | 11 +-
.../routes/actions/audit_log_handler.ts | 2 +-
.../server/endpoint/routes/actions/status.ts | 103 +-----------------
.../endpoint/routes/metadata/handlers.ts | 17 +--
.../service.ts => services/actions.ts} | 86 ++++++++++++++-
.../server/endpoint/services/index.ts | 1 +
...et_agent_status_to_endpoint_host_status.ts | 29 +++++
.../server/endpoint/utils/index.ts | 8 ++
.../factory/hosts/details/helpers.ts | 19 +++-
13 files changed, 233 insertions(+), 155 deletions(-)
rename x-pack/plugins/security_solution/server/endpoint/{routes/actions/service.ts => services/actions.ts} (55%)
create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/fleet_agent_status_to_endpoint_host_status.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/index.ts
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts
index 3175876a8299c..f6f5ad4cd23f1 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts
@@ -8,6 +8,7 @@
import { CloudEcs } from '../../../../ecs/cloud';
import { HostEcs, OsEcs } from '../../../../ecs/host';
import { Hit, Hits, Maybe, SearchHit, StringOrNumber, TotalValue } from '../../../common';
+import { EndpointPendingActions, HostStatus } from '../../../../endpoint/types';
export enum HostPolicyResponseActionStatus {
success = 'success',
@@ -25,6 +26,11 @@ export interface EndpointFields {
endpointPolicy?: Maybe;
sensorVersion?: Maybe;
policyStatus?: Maybe;
+ /** if the host is currently isolated */
+ isolation?: Maybe;
+ /** A count of pending endpoint actions against the host */
+ pendingActions?: Maybe;
+ elasticAgentStatus?: Maybe;
id?: Maybe;
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.tsx
index 94db233972d67..d422fb736965a 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.tsx
@@ -6,14 +6,13 @@
*/
import React, { memo } from 'react';
-import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { HostInfo, HostMetadata } from '../../../../../../common/endpoint/types';
-import { HOST_STATUS_TO_BADGE_COLOR } from '../host_constants';
import { EndpointHostIsolationStatus } from '../../../../../common/components/endpoint/host_isolation';
import { useEndpointSelector } from '../hooks';
import { getEndpointHostIsolationStatusPropsCallback } from '../../store/selectors';
+import { AgentStatus } from '../../../../../common/components/endpoint/agent_status';
const EuiFlexGroupStyled = styled(EuiFlexGroup)`
.isolation-status {
@@ -34,16 +33,7 @@ export const EndpointAgentStatus = memo(
return (
-
-
-
+
{
- test('it renders with endpoint data', () => {
- const endpointData = {
- endpointPolicy: 'demo',
- policyStatus: HostPolicyResponseActionStatus.success,
- sensorVersion: '7.9.0-SNAPSHOT',
- };
- const wrapper = mount(
+ let endpointData: EndpointFields;
+ let wrapper: ReturnType;
+ let findData: ReturnType;
+ const render = (data: EndpointFields | null = endpointData) => {
+ wrapper = mount(
-
+
);
-
- const findData = wrapper.find(
+ findData = wrapper.find(
'dl[data-test-subj="endpoint-overview"] dd.euiDescriptionList__description'
);
+
+ return wrapper;
+ };
+
+ beforeEach(() => {
+ endpointData = {
+ endpointPolicy: 'demo',
+ policyStatus: HostPolicyResponseActionStatus.success,
+ sensorVersion: '7.9.0-SNAPSHOT',
+ isolation: false,
+ elasticAgentStatus: HostStatus.HEALTHY,
+ pendingActions: {},
+ };
+ });
+
+ test('it renders with endpoint data', () => {
+ render();
expect(findData.at(0).text()).toEqual(endpointData.endpointPolicy);
expect(findData.at(1).text()).toEqual(endpointData.policyStatus);
expect(findData.at(2).text()).toContain(endpointData.sensorVersion); // contain because drag adds a space
+ expect(findData.at(3).text()).toEqual('Healthy');
});
- test('it renders with null data', () => {
- const wrapper = mount(
-
-
-
- );
- const findData = wrapper.find(
- 'dl[data-test-subj="endpoint-overview"] dd.euiDescriptionList__description'
- );
+ test('it renders with null data', () => {
+ render(null);
expect(findData.at(0).text()).toEqual('—');
expect(findData.at(1).text()).toEqual('—');
expect(findData.at(2).text()).toContain('—'); // contain because drag adds a space
+ expect(findData.at(3).text()).toEqual('—');
+ });
+
+ test('it shows isolation status', () => {
+ endpointData.isolation = true;
+ render();
+ expect(findData.at(3).text()).toEqual('HealthyIsolated');
+ });
+
+ test.each([
+ ['isolate', 'Isolating'],
+ ['unisolate', 'Releasing'],
+ ])('it shows pending %s status', (action, expectedLabel) => {
+ endpointData.isolation = true;
+ endpointData.pendingActions![action] = 1;
+ render();
+ expect(findData.at(3).text()).toEqual(`Healthy${expectedLabel}`);
});
});
diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx
index 1b05b600c8e3e..568bf30dbe711 100644
--- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx
@@ -18,6 +18,8 @@ import {
EndpointFields,
HostPolicyResponseActionStatus,
} from '../../../../../common/search_strategy/security_solution/hosts';
+import { AgentStatus } from '../../../../common/components/endpoint/agent_status';
+import { EndpointHostIsolationStatus } from '../../../../common/components/endpoint/host_isolation';
interface Props {
contextID?: string;
@@ -73,7 +75,24 @@ export const EndpointOverview = React.memo(({ contextID, data }) => {
: getEmptyTagValue(),
},
],
- [], // needs 4 columns for design
+ [
+ {
+ title: i18n.FLEET_AGENT_STATUS,
+ description:
+ data != null && data.elasticAgentStatus ? (
+ <>
+
+
+ >
+ ) : (
+ getEmptyTagValue()
+ ),
+ },
+ ],
],
[data, getDefaultRenderer]
);
diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.ts b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.ts
index 1a007cd7f0f56..51e1f10e4b927 100644
--- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.ts
+++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/translations.ts
@@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
export const ENDPOINT_POLICY = i18n.translate(
'xpack.securitySolution.host.details.endpoint.endpointPolicy',
{
- defaultMessage: 'Integration',
+ defaultMessage: 'Endpoint integration policy',
}
);
@@ -24,6 +24,13 @@ export const POLICY_STATUS = i18n.translate(
export const SENSORVERSION = i18n.translate(
'xpack.securitySolution.host.details.endpoint.sensorversion',
{
- defaultMessage: 'Sensor Version',
+ defaultMessage: 'Endpoint version',
+ }
+);
+
+export const FLEET_AGENT_STATUS = i18n.translate(
+ 'xpack.securitySolution.host.details.endpoint.fleetAgentStatus',
+ {
+ defaultMessage: 'Agent status',
}
);
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts
index b0cea299af60d..5e9594f478b31 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts
@@ -10,7 +10,7 @@ import {
EndpointActionLogRequestParams,
EndpointActionLogRequestQuery,
} from '../../../../common/endpoint/schema/actions';
-import { getAuditLogResponse } from './service';
+import { getAuditLogResponse } from '../../services';
import { SecuritySolutionRequestHandlerContext } from '../../../types';
import { EndpointAppContext } from '../../types';
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts
index eb2c41ccb3506..ec03acee0335d 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts
@@ -5,15 +5,8 @@
* 2.0.
*/
-import { ElasticsearchClient, RequestHandler } from 'kibana/server';
+import { RequestHandler } from 'kibana/server';
import { TypeOf } from '@kbn/config-schema';
-import { SearchRequest } from '@elastic/elasticsearch/api/types';
-import {
- EndpointAction,
- EndpointActionResponse,
- EndpointPendingActions,
-} from '../../../../common/endpoint/types';
-import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common';
import { ActionStatusRequestSchema } from '../../../../common/endpoint/schema/actions';
import { ACTION_STATUS_ROUTE } from '../../../../common/endpoint/constants';
import {
@@ -21,6 +14,7 @@ import {
SecuritySolutionRequestHandlerContext,
} from '../../../types';
import { EndpointAppContext } from '../../types';
+import { getPendingActionCounts } from '../../services';
/**
* Registers routes for checking status of endpoints based on pending actions
@@ -53,7 +47,7 @@ export const actionStatusRequestHandler = function (
? [...new Set(req.query.agent_ids)]
: [req.query.agent_ids];
- const response = await getPendingActions(esClient, agentIDs);
+ const response = await getPendingActionCounts(esClient, agentIDs);
return res.ok({
body: {
@@ -62,94 +56,3 @@ export const actionStatusRequestHandler = function (
});
};
};
-
-const getPendingActions = async (
- esClient: ElasticsearchClient,
- agentIDs: string[]
-): Promise => {
- // retrieve the unexpired actions for the given hosts
-
- const recentActions = await searchUntilEmpty(esClient, {
- index: AGENT_ACTIONS_INDEX,
- body: {
- query: {
- bool: {
- filter: [
- { term: { type: 'INPUT_ACTION' } }, // actions that are directed at agent children
- { term: { input_type: 'endpoint' } }, // filter for agent->endpoint actions
- { range: { expiration: { gte: 'now' } } }, // that have not expired yet
- { terms: { agents: agentIDs } }, // for the requested agent IDs
- ],
- },
- },
- },
- });
-
- // retrieve any responses to those action IDs from these agents
- const actionIDs = recentActions.map((a) => a.action_id);
- const responses = await searchUntilEmpty(esClient, {
- index: '.fleet-actions-results',
- body: {
- query: {
- bool: {
- filter: [
- { terms: { action_id: actionIDs } }, // get results for these actions
- { terms: { agent_id: agentIDs } }, // ignoring responses from agents we're not looking for
- ],
- },
- },
- },
- });
-
- // respond with action-count per agent
- const pending: EndpointPendingActions[] = agentIDs.map((aid) => {
- const responseIDsFromAgent = responses
- .filter((r) => r.agent_id === aid)
- .map((r) => r.action_id);
- return {
- agent_id: aid,
- pending_actions: recentActions
- .filter((a) => a.agents.includes(aid) && !responseIDsFromAgent.includes(a.action_id))
- .map((a) => a.data.command)
- .reduce((acc, cur) => {
- if (cur in acc) {
- acc[cur] += 1;
- } else {
- acc[cur] = 1;
- }
- return acc;
- }, {} as EndpointPendingActions['pending_actions']),
- };
- });
-
- return pending;
-};
-
-const searchUntilEmpty = async (
- esClient: ElasticsearchClient,
- query: SearchRequest,
- pageSize: number = 1000
-): Promise => {
- const results: T[] = [];
-
- for (let i = 0; ; i++) {
- const result = await esClient.search(
- {
- size: pageSize,
- from: i * pageSize,
- ...query,
- },
- {
- ignore: [404],
- }
- );
- if (!result || !result.body?.hits?.hits || result.body?.hits?.hits?.length === 0) {
- break;
- }
-
- const response = result.body?.hits?.hits?.map((a) => a._source!) || [];
- results.push(...response);
- }
-
- return results;
-};
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts
index 98610c2e84c02..815f30e6e7426 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts
@@ -25,13 +25,14 @@ import {
import type { SecuritySolutionRequestHandlerContext } from '../../../types';
import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders';
-import { Agent, AgentStatus, PackagePolicy } from '../../../../../fleet/common/types/models';
+import { Agent, PackagePolicy } from '../../../../../fleet/common/types/models';
import { AgentNotFoundError } from '../../../../../fleet/server';
import { EndpointAppContext, HostListQueryResult } from '../../types';
import { GetMetadataListRequestSchema, GetMetadataRequestSchema } from './index';
import { findAllUnenrolledAgentIds } from './support/unenroll';
import { findAgentIDsByStatus } from './support/agent_status';
import { EndpointAppContextService } from '../../endpoint_app_context_services';
+import { fleetAgentStatusToEndpointHostStatus } from '../../utils';
export interface MetadataRequestContext {
esClient?: IScopedClusterClient;
@@ -41,18 +42,6 @@ export interface MetadataRequestContext {
savedObjectsClient?: SavedObjectsClientContract;
}
-const HOST_STATUS_MAPPING = new Map([
- ['online', HostStatus.HEALTHY],
- ['offline', HostStatus.OFFLINE],
- ['inactive', HostStatus.INACTIVE],
- ['unenrolling', HostStatus.UPDATING],
- ['enrolling', HostStatus.UPDATING],
- ['updating', HostStatus.UPDATING],
- ['warning', HostStatus.UNHEALTHY],
- ['error', HostStatus.UNHEALTHY],
- ['degraded', HostStatus.UNHEALTHY],
-]);
-
/**
* 00000000-0000-0000-0000-000000000000 is initial Elastic Agent id sent by Endpoint before policy is configured
* 11111111-1111-1111-1111-111111111111 is Elastic Agent id sent by Endpoint when policy does not contain an id
@@ -375,7 +364,7 @@ export async function enrichHostMetadata(
const status = await metadataRequestContext.endpointAppContextService
?.getAgentService()
?.getAgentStatusById(esClient.asCurrentUser, elasticAgentId);
- hostStatus = HOST_STATUS_MAPPING.get(status!) || HostStatus.UNHEALTHY;
+ hostStatus = fleetAgentStatusToEndpointHostStatus(status!);
} catch (e) {
if (e instanceof AgentNotFoundError) {
log.warn(`agent with id ${elasticAgentId} not found`);
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts
similarity index 55%
rename from x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts
rename to x-pack/plugins/security_solution/server/endpoint/services/actions.ts
index 7a82a56b1f19b..9d8db5b9a2154 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts
@@ -6,9 +6,14 @@
*/
import { ElasticsearchClient, Logger } from 'kibana/server';
-import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../../fleet/common';
-import { SecuritySolutionRequestHandlerContext } from '../../../types';
-import { ActivityLog, EndpointAction } from '../../../../common/endpoint/types';
+import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common';
+import { SecuritySolutionRequestHandlerContext } from '../../types';
+import {
+ ActivityLog,
+ EndpointAction,
+ EndpointActionResponse,
+ EndpointPendingActions,
+} from '../../../common/endpoint/types';
export const getAuditLogResponse = async ({
elasticAgentId,
@@ -135,3 +140,78 @@ const getActivityLog = async ({
return sortedData;
};
+
+export const getPendingActionCounts = async (
+ esClient: ElasticsearchClient,
+ agentIDs: string[]
+): Promise => {
+ // retrieve the unexpired actions for the given hosts
+ const recentActions = await esClient
+ .search(
+ {
+ index: AGENT_ACTIONS_INDEX,
+ size: 10000,
+ from: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ { term: { type: 'INPUT_ACTION' } }, // actions that are directed at agent children
+ { term: { input_type: 'endpoint' } }, // filter for agent->endpoint actions
+ { range: { expiration: { gte: 'now' } } }, // that have not expired yet
+ { terms: { agents: agentIDs } }, // for the requested agent IDs
+ ],
+ },
+ },
+ },
+ },
+ { ignore: [404] }
+ )
+ .then((result) => result.body?.hits?.hits?.map((a) => a._source!) || []);
+
+ // retrieve any responses to those action IDs from these agents
+ const actionIDs = recentActions.map((a) => a.action_id);
+ const responses = await esClient
+ .search(
+ {
+ index: AGENT_ACTIONS_RESULTS_INDEX,
+ size: 10000,
+ from: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ { terms: { action_id: actionIDs } }, // get results for these actions
+ { terms: { agent_id: agentIDs } }, // ignoring responses from agents we're not looking for
+ ],
+ },
+ },
+ },
+ },
+ { ignore: [404] }
+ )
+ .then((result) => result.body?.hits?.hits?.map((a) => a._source!) || []);
+
+ // respond with action-count per agent
+ const pending: EndpointPendingActions[] = agentIDs.map((aid) => {
+ const responseIDsFromAgent = responses
+ .filter((r) => r.agent_id === aid)
+ .map((r) => r.action_id);
+ return {
+ agent_id: aid,
+ pending_actions: recentActions
+ .filter((a) => a.agents.includes(aid) && !responseIDsFromAgent.includes(a.action_id))
+ .map((a) => a.data.command)
+ .reduce((acc, cur) => {
+ if (cur in acc) {
+ acc[cur] += 1;
+ } else {
+ acc[cur] = 1;
+ }
+ return acc;
+ }, {} as EndpointPendingActions['pending_actions']),
+ };
+ });
+
+ return pending;
+};
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/index.ts
index 8bf64999c746a..ee6570c4866bd 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/index.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/index.ts
@@ -7,3 +7,4 @@
export * from './artifacts';
export { getMetadataForEndpoints } from './metadata';
+export * from './actions';
diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/fleet_agent_status_to_endpoint_host_status.ts b/x-pack/plugins/security_solution/server/endpoint/utils/fleet_agent_status_to_endpoint_host_status.ts
new file mode 100644
index 0000000000000..3c02222346a44
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/utils/fleet_agent_status_to_endpoint_host_status.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { AgentStatus } from '../../../../fleet/common';
+import { HostStatus } from '../../../common/endpoint/types';
+
+const STATUS_MAPPING: ReadonlyMap = new Map([
+ ['online', HostStatus.HEALTHY],
+ ['offline', HostStatus.OFFLINE],
+ ['inactive', HostStatus.INACTIVE],
+ ['unenrolling', HostStatus.UPDATING],
+ ['enrolling', HostStatus.UPDATING],
+ ['updating', HostStatus.UPDATING],
+ ['warning', HostStatus.UNHEALTHY],
+ ['error', HostStatus.UNHEALTHY],
+ ['degraded', HostStatus.UNHEALTHY],
+]);
+
+/**
+ * A Map of Fleet Agent Status to Endpoint Host Status.
+ * Default status is `HostStatus.UNHEALTHY`
+ */
+export const fleetAgentStatusToEndpointHostStatus = (status: AgentStatus): HostStatus => {
+ return STATUS_MAPPING.get(status) || HostStatus.UNHEALTHY;
+};
diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/index.ts b/x-pack/plugins/security_solution/server/endpoint/utils/index.ts
new file mode 100644
index 0000000000000..5cf23db57be12
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/utils/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './fleet_agent_status_to_endpoint_host_status';
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts
index 1b6e927f33638..f4d942f733c1d 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts
@@ -24,6 +24,8 @@ import {
import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array';
import { getHostMetaData } from '../../../../../endpoint/routes/metadata/handlers';
import { EndpointAppContext } from '../../../../../endpoint/types';
+import { fleetAgentStatusToEndpointHostStatus } from '../../../../../endpoint/utils';
+import { getPendingActionCounts } from '../../../../../endpoint/services';
export const HOST_FIELDS = [
'_id',
@@ -200,15 +202,30 @@ export const getHostEndpoint = async (
? await getHostMetaData(metadataRequestContext, id, undefined)
: null;
+ const fleetAgentId = endpointData?.metadata.elastic.agent.id;
+ const [fleetAgentStatus, pendingActions] = !fleetAgentId
+ ? [undefined, {}]
+ : await Promise.all([
+ // Get Agent Status
+ agentService.getAgentStatusById(esClient.asCurrentUser, fleetAgentId),
+ // Get a list of pending actions (if any)
+ getPendingActionCounts(esClient.asCurrentUser, [fleetAgentId]).then((results) => {
+ return results[0].pending_actions;
+ }),
+ ]);
+
return endpointData != null && endpointData.metadata
? {
endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name,
policyStatus: endpointData.metadata.Endpoint.policy.applied.status,
sensorVersion: endpointData.metadata.agent.version,
+ elasticAgentStatus: fleetAgentStatusToEndpointHostStatus(fleetAgentStatus!),
+ isolation: endpointData.metadata.Endpoint.state?.isolation ?? false,
+ pendingActions,
}
: null;
} catch (err) {
- logger.warn(JSON.stringify(err, null, 2));
+ logger.warn(err);
return null;
}
};
From 58fab48500eed861d7ac963ea9e772ca3b183b42 Mon Sep 17 00:00:00 2001
From: Frank Hassanabad
Date: Wed, 30 Jun 2021 17:34:13 -0600
Subject: [PATCH 17/51] Fixes the unHandledPromise rejections happening from
unit tests (#104017)
## Summary
We had `unHandledPromise` rejections within some of our unit tests which still pass on CI but technically those tests are not running correctly and will not catch bugs.
We were seeing them showing up like so:
```ts
PASS x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts (10.502 s)
(node:21059) UnhandledPromiseRejectionWarning: [object Object]
at emitUnhandledRejectionWarning (internal/process/promises.js:170:15)
at processPromiseRejections (internal/process/promises.js:247:11)
at processTicksAndRejections (internal/process/task_queues.js:96:32)
(node:21059) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 3)
(node:21059) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
at emitDeprecationWarning (internal/process/promises.js:180:11)
at processPromiseRejections (internal/process/promises.js:249:13)
at processTicksAndRejections (internal/process/task_queues.js:96:32)
PASS x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
PASS x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts
PASS x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts
(node:21059) UnhandledPromiseRejectionWarning: Error: bulk failed
at emitUnhandledRejectionWarning (internal/process/promises.js:170:15)
at processPromiseRejections (internal/process/promises.js:247:11)
at processTicksAndRejections (internal/process/task_queues.js:96:32)
(node:21059) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 7)
````
You can narrow down `unHandledPromise` rejections and fix tests one by one by running the following command:
```ts
node --trace-warnings --unhandled-rejections=strict scripts/jest.js --runInBand x-pack/plugins/security_solution
```
You can manually test if I fixed them by running that command and ensuring all tests run without errors and that the process exits with a 0 for detections only by running:
```ts
node --trace-warnings --unhandled-rejections=strict scripts/jest.js --runInBand x-pack/plugins/security_solution/public/detections
```
and
```ts
node --trace-warnings --unhandled-rejections=strict scripts/jest.js --runInBand x-pack/plugins/security_solution/server/lib/detection_engine
```
### Checklist
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or
---
.../components/rules/query_bar/index.test.tsx | 33 ++++++++++++++++---
.../rule_actions_overflow/index.test.tsx | 15 ++++++---
.../signals/search_after_bulk_create.test.ts | 14 ++++++--
.../signals/signal_rule_alert_type.test.ts | 12 +++++--
4 files changed, 60 insertions(+), 14 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.test.tsx
index 8c6f74a01e49a..12923609db266 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.test.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { mount, shallow } from 'enzyme';
+import { mount } from 'enzyme';
import { QueryBarDefineRule } from './index';
import {
@@ -17,7 +17,26 @@ import {
import { useGetAllTimeline, getAllTimeline } from '../../../../timelines/containers/all';
import { mockHistory, Router } from '../../../../common/mock/router';
-jest.mock('../../../../common/lib/kibana');
+jest.mock('../../../../common/lib/kibana', () => {
+ const actual = jest.requireActual('../../../../common/lib/kibana');
+ return {
+ ...actual,
+ KibanaServices: {
+ get: jest.fn(() => ({
+ http: {
+ post: jest.fn().mockReturnValue({
+ success: true,
+ success_count: 0,
+ timelines_installed: 0,
+ timelines_updated: 0,
+ errors: [],
+ }),
+ fetch: jest.fn(),
+ },
+ })),
+ },
+ };
+});
jest.mock('../../../../timelines/containers/all', () => {
const originalModule = jest.requireActual('../../../../timelines/containers/all');
@@ -55,8 +74,14 @@ describe('QueryBarDefineRule', () => {
/>
);
};
- const wrapper = shallow();
- expect(wrapper.dive().find('[data-test-subj="query-bar-define-rule"]')).toHaveLength(1);
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="query-bar-define-rule"]').exists()).toBeTruthy();
});
it('renders import query from saved timeline modal actions hidden correctly', () => {
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx
index c545de7fd8d7d..6a62b05c2e319 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx
@@ -36,11 +36,16 @@ jest.mock('react-router-dom', () => ({
}),
}));
-jest.mock('../../../pages/detection_engine/rules/all/actions', () => ({
- deleteRulesAction: jest.fn(),
- duplicateRulesAction: jest.fn(),
- editRuleAction: jest.fn(),
-}));
+jest.mock('../../../pages/detection_engine/rules/all/actions', () => {
+ const actual = jest.requireActual('../../../../common/lib/kibana');
+ return {
+ ...actual,
+ exportRulesAction: jest.fn(),
+ deleteRulesAction: jest.fn(),
+ duplicateRulesAction: jest.fn(),
+ editRuleAction: jest.fn(),
+ };
+});
const duplicateRulesActionMock = duplicateRulesAction as jest.Mock;
const flushPromises = () => new Promise(setImmediate);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
index dc03f1bc964f2..711db931e9072 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
@@ -31,6 +31,7 @@ import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
import { bulkCreateFactory } from './bulk_create_factory';
import { wrapHitsFactory } from './wrap_hits_factory';
import { mockBuildRuleMessage } from './__mocks__/build_rule_message.mock';
+import { ResponseError } from '@elastic/elasticsearch/lib/errors';
const buildRuleMessage = mockBuildRuleMessage;
@@ -739,9 +740,16 @@ describe('searchAfterAndBulkCreate', () => {
repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))
)
);
- mockService.scopedClusterClient.asCurrentUser.bulk.mockRejectedValue(
- elasticsearchClientMock.createErrorTransportRequestPromise(new Error('bulk failed'))
- ); // Added this recently
+ mockService.scopedClusterClient.asCurrentUser.bulk.mockReturnValue(
+ elasticsearchClientMock.createErrorTransportRequestPromise(
+ new ResponseError(
+ elasticsearchClientMock.createApiResponse({
+ statusCode: 400,
+ body: { error: { type: 'bulk_error_type' } },
+ })
+ )
+ )
+ );
const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({
listClient,
exceptionsList: [exceptionItem],
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
index 39aebb4aa4555..aec8b6c552b1d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
@@ -32,6 +32,7 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mo
import { queryExecutor } from './executors/query';
import { mlExecutor } from './executors/ml';
import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.mock';
+import { ResponseError } from '@elastic/elasticsearch/lib/errors';
jest.mock('./rule_status_saved_objects_client');
jest.mock('./rule_status_service');
@@ -455,8 +456,15 @@ describe('signal_rule_alert_type', () => {
});
it('and call ruleStatusService with the default message', async () => {
- (queryExecutor as jest.Mock).mockRejectedValue(
- elasticsearchClientMock.createErrorTransportRequestPromise({})
+ (queryExecutor as jest.Mock).mockReturnValue(
+ elasticsearchClientMock.createErrorTransportRequestPromise(
+ new ResponseError(
+ elasticsearchClientMock.createApiResponse({
+ statusCode: 400,
+ body: { error: { type: 'some_error_type' } },
+ })
+ )
+ )
);
await alert.executor(payload);
expect(logger.error).toHaveBeenCalled();
From 3cbce69598012782bcfe9202e0715b395736e6fa Mon Sep 17 00:00:00 2001
From: John Dorlus
Date: Wed, 30 Jun 2021 19:52:15 -0400
Subject: [PATCH 18/51] Add CIT for Date Index Processor in Ingest Node
Pipelines (#103416)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Added initial work for date index processor CITs.
* Fixed the tests and added the remaining coverage.
* Fixed message for date rounding error and updated tests to use GMT since that timezone actually works with the API.
* Update Date Index Name processor test name.
Co-authored-by: Yulia Čech <6585477+yuliacech@users.noreply.github.com>
Co-authored-by: Yulia Čech <6585477+yuliacech@users.noreply.github.com>
---
.../__jest__/processors/date_index.test.tsx | 124 ++++++++++++++++++
.../__jest__/processors/processor.helpers.tsx | 6 +
.../processors/date_index_name.tsx | 20 ++-
3 files changed, 147 insertions(+), 3 deletions(-)
create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date_index.test.tsx
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date_index.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date_index.test.tsx
new file mode 100644
index 0000000000000..264db2c5b65c0
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date_index.test.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { act } from 'react-dom/test-utils';
+import { setup, SetupResult, getProcessorValue } from './processor.helpers';
+
+const DATE_INDEX_TYPE = 'date_index_name';
+
+describe('Processor: Date Index Name', () => {
+ let onUpdate: jest.Mock;
+ let testBed: SetupResult;
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ beforeEach(async () => {
+ onUpdate = jest.fn();
+
+ await act(async () => {
+ testBed = await setup({
+ value: {
+ processors: [],
+ },
+ onFlyoutOpen: jest.fn(),
+ onUpdate,
+ });
+ });
+ testBed.component.update();
+ const {
+ actions: { addProcessor, addProcessorType },
+ } = testBed;
+ // Open the processor flyout
+ addProcessor();
+
+ // Add type (the other fields are not visible until a type is selected)
+ await addProcessorType(DATE_INDEX_TYPE);
+ });
+
+ test('prevents form submission if required fields are not provided', async () => {
+ const {
+ actions: { saveNewProcessor },
+ form,
+ } = testBed;
+
+ // Click submit button with only the type defined
+ await saveNewProcessor();
+
+ // Expect form error as "field" and "date rounding" are required parameters
+ expect(form.getErrorsMessages()).toEqual([
+ 'A field value is required.',
+ 'A date rounding value is required.',
+ ]);
+ });
+
+ test('saves with required field and date rounding parameter values', async () => {
+ const {
+ actions: { saveNewProcessor },
+ form,
+ } = testBed;
+
+ // Add "field" value (required)
+ form.setInputValue('fieldNameField.input', '@timestamp');
+
+ // Select second value for date rounding
+ form.setSelectValue('dateRoundingField', 's');
+
+ // Save the field
+ await saveNewProcessor();
+
+ const processors = await getProcessorValue(onUpdate, DATE_INDEX_TYPE);
+ expect(processors[0].date_index_name).toEqual({
+ field: '@timestamp',
+ date_rounding: 's',
+ });
+ });
+
+ test('allows optional parameters to be set', async () => {
+ const {
+ actions: { saveNewProcessor },
+ form,
+ find,
+ component,
+ } = testBed;
+
+ form.setInputValue('fieldNameField.input', 'field_1');
+
+ form.setSelectValue('dateRoundingField', 'd');
+
+ form.setInputValue('indexNamePrefixField.input', 'prefix');
+
+ form.setInputValue('indexNameFormatField.input', 'yyyy-MM');
+
+ await act(async () => {
+ find('dateFormatsField.input').simulate('change', [{ label: 'ISO8601' }]);
+ });
+ component.update();
+
+ form.setInputValue('timezoneField.input', 'GMT');
+
+ form.setInputValue('localeField.input', 'SPANISH');
+ // Save the field with new changes
+ await saveNewProcessor();
+
+ const processors = await getProcessorValue(onUpdate, DATE_INDEX_TYPE);
+ expect(processors[0].date_index_name).toEqual({
+ field: 'field_1',
+ date_rounding: 'd',
+ index_name_format: 'yyyy-MM',
+ index_name_prefix: 'prefix',
+ date_formats: ['ISO8601'],
+ locale: 'SPANISH',
+ timezone: 'GMT',
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx
index 78bc261aed7df..d50189167a2ff 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx
@@ -147,8 +147,14 @@ type TestSubject =
| 'mockCodeEditor'
| 'tagField.input'
| 'typeSelectorField'
+ | 'dateRoundingField'
| 'ignoreMissingSwitch.input'
| 'ignoreFailureSwitch.input'
+ | 'indexNamePrefixField.input'
+ | 'indexNameFormatField.input'
+ | 'dateFormatsField.input'
+ | 'timezoneField.input'
+ | 'localeField.input'
| 'ifField.textarea'
| 'targetField.input'
| 'targetFieldsField.input'
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx
index 5c5b5ff89fd20..d4fb74c73ff0c 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx
@@ -47,7 +47,7 @@ const fieldsConfig: FieldsConfig = {
i18n.translate(
'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingRequiredError',
{
- defaultMessage: 'A field value is required.',
+ defaultMessage: 'A date rounding value is required.',
}
)
),
@@ -160,6 +160,7 @@ export const DateIndexName: FunctionComponent = () => {
component={SelectField}
componentProps={{
euiFieldProps: {
+ 'data-test-subj': 'dateRoundingField',
options: [
{
value: 'y',
@@ -217,26 +218,39 @@ export const DateIndexName: FunctionComponent = () => {
/>
-
+
-
+
>
);
};
From 81b9e73fed9daabb070809ea4bc2bce5d4de57a7 Mon Sep 17 00:00:00 2001
From: Frank Hassanabad
Date: Wed, 30 Jun 2021 17:59:19 -0600
Subject: [PATCH 19/51] Updates the the PR template to remove links to private
repo and fix docker URL with regards to kibana.yml keys (#103901)
## Summary
Updates the Pull Request template to have:
* Removes links to private repo's
* Fixes the docker link to point to the current version within master
Before this, our PR template had this checkbox:
- [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker)
After this, our PR template becomes:
- [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/master/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
---
.github/PULL_REQUEST_TEMPLATE.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 726e4257a5aac..1ea9e5a5a75bc 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -12,7 +12,7 @@ Delete any items that are not applicable to this PR.
- [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
-- [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker)
+- [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/master/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
From fcd16dd87b97c6a1e9b555187d2f2825bd678e79 Mon Sep 17 00:00:00 2001
From: Clint Andrew Hall
Date: Wed, 30 Jun 2021 20:28:21 -0400
Subject: [PATCH 20/51] [canvas] Replace react-beautiful-dnd with EuiDrapDrop
(#102688)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/dom_preview/dom_preview.tsx | 29 ++++-
.../page_manager/page_manager.component.tsx | 112 ++++++++----------
.../toolbar/__stories__/toolbar.stories.tsx | 42 +++----
.../storybook/decorators/redux_decorator.tsx | 9 +-
4 files changed, 104 insertions(+), 88 deletions(-)
diff --git a/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.tsx b/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.tsx
index 636b40c040e1f..5d9998b16a330 100644
--- a/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.tsx
+++ b/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.tsx
@@ -9,15 +9,22 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
-interface Props {
+interface HeightProps {
elementId: string;
height: number;
+ width?: never;
+}
+interface WidthProps {
+ elementId: string;
+ width: number;
+ height?: never;
}
-export class DomPreview extends PureComponent {
+export class DomPreview extends PureComponent {
static propTypes = {
elementId: PropTypes.string.isRequired,
- height: PropTypes.number.isRequired,
+ height: PropTypes.number,
+ width: PropTypes.number,
};
_container: HTMLDivElement | null = null;
@@ -78,9 +85,19 @@ export class DomPreview extends PureComponent {
const originalWidth = parseInt(originalStyle.getPropertyValue('width'), 10);
const originalHeight = parseInt(originalStyle.getPropertyValue('height'), 10);
- const thumbHeight = this.props.height;
- const scale = thumbHeight / originalHeight;
- const thumbWidth = originalWidth * scale;
+ let thumbHeight = 0;
+ let thumbWidth = 0;
+ let scale = 1;
+
+ if (this.props.height) {
+ thumbHeight = this.props.height;
+ scale = thumbHeight / originalHeight;
+ thumbWidth = originalWidth * scale;
+ } else if (this.props.width) {
+ thumbWidth = this.props.width;
+ scale = thumbWidth / originalWidth;
+ thumbHeight = originalHeight * scale;
+ }
if (this._content.firstChild) {
this._content.removeChild(this._content.firstChild);
diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx
index 9d1939db43fd5..c4d1e6fb91a69 100644
--- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx
+++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx
@@ -7,9 +7,18 @@
import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
-import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
+import {
+ EuiIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+ EuiToolTip,
+ EuiDragDropContext,
+ EuiDraggable,
+ EuiDroppable,
+ DragDropContextProps,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { DragDropContext, Droppable, Draggable, DragDropContextProps } from 'react-beautiful-dnd';
// @ts-expect-error untyped dependency
import Style from 'style-it';
@@ -173,46 +182,37 @@ export class PageManager extends Component {
const pageNumber = i + 1;
return (
-
- {(provided) => (
- {
- if (page.id === selectedPage) {
- this._activePageRef = el;
- }
- provided.innerRef(el);
- }}
- {...provided.draggableProps}
- {...provided.dragHandleProps}
- >
-
-
-
- {pageNumber}
-
-
-
-
- {({ getUrl }) => (
-
- {Style.it(
- workpadCSS,
-
- )}
-
+
+
+
+
+ {pageNumber}
+
+
+
+
+ {({ getUrl }) => (
+
+ {Style.it(
+ workpadCSS,
+
)}
-
-
-
-
- )}
-
+
+ )}
+
+
+
+
);
};
@@ -224,25 +224,17 @@ export class PageManager extends Component {
-
-
- {(provided) => (
- {
- this._pageListRef = el;
- provided.innerRef(el);
- }}
- {...provided.droppableProps}
- >
- {pages.map(this.renderPage)}
- {provided.placeholder}
-
- )}
-
-
+
+
+
+ {pages.map(this.renderPage)}
+
+
+
{isWriteable && (
diff --git a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx
index bd47bb52e0030..e571cc12f4425 100644
--- a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx
+++ b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx
@@ -7,26 +7,28 @@
import { storiesOf } from '@storybook/react';
import React from 'react';
-import { Toolbar } from '../toolbar.component';
-// @ts-expect-error untyped local
-import { getDefaultElement } from '../../../state/defaults';
+// @ts-expect-error
+import { getDefaultPage } from '../../../state/defaults';
+import { reduxDecorator } from '../../../../storybook';
+import { Toolbar } from '../toolbar';
+
+const pages = [...new Array(10)].map(() => getDefaultPage());
+
+const Pages = ({ story }: { story: Function }) => (
+
+ {story()}
+
+ {pages.map((page, index) => (
+
+
Page {index}
+
+ ))}
+
+
+);
storiesOf('components/Toolbar', module)
- .add('no element selected', () => (
-
- ))
- .add('element selected', () => (
-
- ));
+ .addDecorator((story) => )
+ .addDecorator(reduxDecorator({ pages }))
+ .add('redux', () => );
diff --git a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx
index 289171f136ab5..e81ae50ac6dd0 100644
--- a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx
+++ b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx
@@ -15,7 +15,7 @@ import { set } from '@elastic/safer-lodash-set';
// @ts-expect-error Untyped local
import { getDefaultWorkpad } from '../../public/state/defaults';
-import { CanvasWorkpad, CanvasElement, CanvasAsset } from '../../types';
+import { CanvasWorkpad, CanvasElement, CanvasAsset, CanvasPage } from '../../types';
// @ts-expect-error untyped local
import { elementsRegistry } from '../../public/lib/elements_registry';
@@ -27,18 +27,23 @@ export { ADDON_ID, ACTIONS_PANEL_ID } from '../addon/src/constants';
export interface Params {
workpad?: CanvasWorkpad;
+ pages?: CanvasPage[];
elements?: CanvasElement[];
assets?: CanvasAsset[];
}
export const reduxDecorator = (params: Params = {}) => {
const state = cloneDeep(getInitialState());
- const { workpad, elements, assets } = params;
+ const { workpad, elements, assets, pages } = params;
if (workpad) {
set(state, 'persistent.workpad', workpad);
}
+ if (pages) {
+ set(state, 'persistent.workpad.pages', pages);
+ }
+
if (elements) {
set(state, 'persistent.workpad.pages.0.elements', elements);
}
From 90db5fd4a4ce59e1703e86ff54a71e31390427ec Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Thu, 1 Jul 2021 01:48:48 +0100
Subject: [PATCH 21/51] chore(NA): upgrades bazel rules nodejs into v3.6.0
(#103895)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
WORKSPACE.bazel | 6 +++---
package.json | 2 +-
yarn.lock | 8 ++++----
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel
index acb62043a15ca..ebf7bbc8488ac 100644
--- a/WORKSPACE.bazel
+++ b/WORKSPACE.bazel
@@ -10,15 +10,15 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# Fetch Node.js rules
http_archive(
name = "build_bazel_rules_nodejs",
- sha256 = "4a5d654a4ccd4a4c24eca5d319d85a88a650edf119601550c95bf400c8cc897e",
- urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.5.1/rules_nodejs-3.5.1.tar.gz"],
+ sha256 = "0fa2d443571c9e02fcb7363a74ae591bdcce2dd76af8677a95965edf329d778a",
+ urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.6.0/rules_nodejs-3.6.0.tar.gz"],
)
# Now that we have the rules let's import from them to complete the work
load("@build_bazel_rules_nodejs//:index.bzl", "check_rules_nodejs_version", "node_repositories", "yarn_install")
# Assure we have at least a given rules_nodejs version
-check_rules_nodejs_version(minimum_version_string = "3.5.1")
+check_rules_nodejs_version(minimum_version_string = "3.6.0")
# Setup the Node.js toolchain for the architectures we want to support
#
diff --git a/package.json b/package.json
index b1d57d54838bc..1cc379fb807d0 100644
--- a/package.json
+++ b/package.json
@@ -447,7 +447,7 @@
"@babel/traverse": "^7.12.12",
"@babel/types": "^7.12.12",
"@bazel/ibazel": "^0.15.10",
- "@bazel/typescript": "^3.5.1",
+ "@bazel/typescript": "^3.6.0",
"@cypress/snapshot": "^2.1.7",
"@cypress/webpack-preprocessor": "^5.6.0",
"@elastic/eslint-config-kibana": "link:bazel-bin/packages/elastic-eslint-config-kibana",
diff --git a/yarn.lock b/yarn.lock
index b95056a78ea8b..8bce932ee9e4e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1197,10 +1197,10 @@
resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.15.10.tgz#cf0cff1aec6d8e7bb23e1fc618d09fbd39b7a13f"
integrity sha512-0v+OwCQ6fsGFa50r6MXWbUkSGuWOoZ22K4pMSdtWiL5LKFIE4kfmMmtQS+M7/ICNwk2EIYob+NRreyi/DGUz5A==
-"@bazel/typescript@^3.5.1":
- version "3.5.1"
- resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.5.1.tgz#c6027d683adeefa2c3cebfa3ed5efa17c405a63b"
- integrity sha512-dU5sGgaGdFWV1dJ1B+9iFbttgcKtmob+BvlM8mY7Nxq4j7/wVbgPjiVLOBeOD7kpzYep8JHXfhAokHt486IG+Q==
+"@bazel/typescript@^3.6.0":
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.6.0.tgz#4dda2e39505cde4a190f51118fbb82ea0e80fde6"
+ integrity sha512-cO58iHmSxM4mRHJLLbb3FfoJJxv0pMiVGFLORoiUy/EhLtyYGZ1e7ntf4GxEovwK/E4h/awjSUlQkzPThcukTg==
dependencies:
protobufjs "6.8.8"
semver "5.6.0"
From 0cba746e7116daa6188e46175a044d40e6bd0842 Mon Sep 17 00:00:00 2001
From: Frank Hassanabad
Date: Wed, 30 Jun 2021 19:43:50 -0600
Subject: [PATCH 22/51] Should make cypress less flake with two of our tests
(#104033)
## Summary
Should reduce flake in two of our Cypress tests.
* Removed skip on a test recently skipped
* Removes a wait() that doesn't seem to have been reducing flake added by a EUI team member
* Adds a `.click()` to give focus to a component in order to improve our chances of typing in the input box
* Adds some `.should('exists')` which will cause Cypress to ensure something exists and a better chance for click handlers to be added
* Adds a pipe as suggested by @yctercero in the flake test
### Checklist
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
---
.../cypress/integration/timelines/row_renderers.spec.ts | 9 +++++++--
.../cypress/integration/urls/state.spec.ts | 1 -
.../security_solution/cypress/tasks/date_picker.ts | 4 ++--
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts
index b3103963284b4..77a1775494e6a 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts
@@ -46,6 +46,7 @@ describe('Row renderers', () => {
loginAndWaitForPage(HOSTS_URL);
openTimelineUsingToggle();
populateTimeline();
+ cy.get(TIMELINE_SHOW_ROW_RENDERERS_GEAR).should('exist');
cy.get(TIMELINE_SHOW_ROW_RENDERERS_GEAR).first().click({ force: true });
});
@@ -59,6 +60,7 @@ describe('Row renderers', () => {
});
it('Selected renderer can be disabled and enabled', () => {
+ cy.get(TIMELINE_ROW_RENDERERS_SEARCHBOX).should('exist');
cy.get(TIMELINE_ROW_RENDERERS_SEARCHBOX).type('flow');
cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first().uncheck();
@@ -75,8 +77,11 @@ describe('Row renderers', () => {
});
});
- it.skip('Selected renderer can be disabled with one click', () => {
- cy.get(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN).click({ force: true });
+ it('Selected renderer can be disabled with one click', () => {
+ cy.get(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN).should('exist');
+ cy.get(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN)
+ .pipe(($el) => $el.trigger('click'))
+ .should('not.be.visible');
cy.intercept('PATCH', '/api/timeline').as('updateTimeline');
cy.wait('@updateTimeline').its('response.statusCode').should('eq', 200);
diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts
index f2b644e8d054c..842dd85b42ef8 100644
--- a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts
@@ -74,7 +74,6 @@ describe('url state', () => {
waitForIpsTableToBeLoaded();
setEndDate(ABSOLUTE_DATE.newEndTimeTyped);
updateDates();
- cy.wait(300);
let startDate: string;
let endDate: string;
diff --git a/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts b/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts
index 5fef4f2f5569b..26512a2fcbc5b 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts
@@ -21,7 +21,7 @@ export const setEndDate = (date: string) => {
cy.get(DATE_PICKER_ABSOLUTE_TAB).first().click({ force: true });
- cy.get(DATE_PICKER_ABSOLUTE_INPUT).clear().type(date);
+ cy.get(DATE_PICKER_ABSOLUTE_INPUT).click().clear().type(date);
};
export const setStartDate = (date: string) => {
@@ -29,7 +29,7 @@ export const setStartDate = (date: string) => {
cy.get(DATE_PICKER_ABSOLUTE_TAB).first().click({ force: true });
- cy.get(DATE_PICKER_ABSOLUTE_INPUT).clear().type(date);
+ cy.get(DATE_PICKER_ABSOLUTE_INPUT).click().clear().type(date);
};
export const setTimelineEndDate = (date: string) => {
From a06c0f1409c138d6c096d37ae85406333a98653c Mon Sep 17 00:00:00 2001
From: spalger
Date: Wed, 30 Jun 2021 21:39:10 -0700
Subject: [PATCH 23/51] skip flaky suite (#104042)
---
x-pack/test/functional/apps/ml/permissions/index.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/functional/apps/ml/permissions/index.ts b/x-pack/test/functional/apps/ml/permissions/index.ts
index e777f241eaf85..af9f8a5f240d1 100644
--- a/x-pack/test/functional/apps/ml/permissions/index.ts
+++ b/x-pack/test/functional/apps/ml/permissions/index.ts
@@ -8,7 +8,8 @@
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
- describe('permissions', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/104042
+ describe.skip('permissions', function () {
this.tags(['skipFirefox']);
loadTestFile(require.resolve('./full_ml_access'));
From de9b62ac4f5fde70db3a8154699e6d9851398e2c Mon Sep 17 00:00:00 2001
From: mgiota
Date: Thu, 1 Jul 2021 09:03:56 +0200
Subject: [PATCH 24/51] [Metrics UI]: add system.cpu.total.norm.pct to default
metrics (#102428)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../metrics_explorer/hooks/use_metrics_explorer_options.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts
index c1e5be94acc03..8bf64edcf8970 100644
--- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts
+++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts
@@ -99,7 +99,7 @@ export const DEFAULT_CHART_OPTIONS: MetricsExplorerChartOptions = {
export const DEFAULT_METRICS: MetricsExplorerOptionsMetric[] = [
{
aggregation: 'avg',
- field: 'system.cpu.user.pct',
+ field: 'system.cpu.total.norm.pct',
color: Color.color0,
},
{
From a3c079bda645b62e0ac92e739c3af123a9ceb160 Mon Sep 17 00:00:00 2001
From: Tim Roes
Date: Thu, 1 Jul 2021 09:26:43 +0200
Subject: [PATCH 25/51] Make (empty) value subdued (#103833)
* Make empty value subdued
* Fix highlighting in values
* Fix test failures
* Add unit tests
---
.../field_formats/converters/string.test.ts | 28 +++++++++++++++++++
.../common/field_formats/converters/string.ts | 17 +++++++++--
.../field_formats/converters/_index.scss | 1 +
.../field_formats/converters/_string.scss | 3 ++
src/plugins/data/public/index.scss | 1 +
5 files changed, 47 insertions(+), 3 deletions(-)
create mode 100644 src/plugins/data/public/field_formats/converters/_index.scss
create mode 100644 src/plugins/data/public/field_formats/converters/_string.scss
diff --git a/src/plugins/data/common/field_formats/converters/string.test.ts b/src/plugins/data/common/field_formats/converters/string.test.ts
index ccb7a58285b20..d691712b674dd 100644
--- a/src/plugins/data/common/field_formats/converters/string.test.ts
+++ b/src/plugins/data/common/field_formats/converters/string.test.ts
@@ -8,6 +8,14 @@
import { StringFormat } from './string';
+/**
+ * Removes a wrapping span, that is created by the field formatter infrastructure
+ * and we're not caring about in these tests.
+ */
+function stripSpan(input: string): string {
+ return input.replace(/^\(.*)\<\/span\>$/, '$1');
+}
+
describe('String Format', () => {
test('convert a string to lower case', () => {
const string = new StringFormat(
@@ -17,6 +25,7 @@ describe('String Format', () => {
jest.fn()
);
expect(string.convert('Kibana')).toBe('kibana');
+ expect(stripSpan(string.convert('Kibana', 'html'))).toBe('kibana');
});
test('convert a string to upper case', () => {
@@ -27,6 +36,7 @@ describe('String Format', () => {
jest.fn()
);
expect(string.convert('Kibana')).toBe('KIBANA');
+ expect(stripSpan(string.convert('Kibana', 'html'))).toBe('KIBANA');
});
test('decode a base64 string', () => {
@@ -37,6 +47,7 @@ describe('String Format', () => {
jest.fn()
);
expect(string.convert('Zm9vYmFy')).toBe('foobar');
+ expect(stripSpan(string.convert('Zm9vYmFy', 'html'))).toBe('foobar');
});
test('convert a string to title case', () => {
@@ -47,10 +58,15 @@ describe('String Format', () => {
jest.fn()
);
expect(string.convert('PLEASE DO NOT SHOUT')).toBe('Please Do Not Shout');
+ expect(stripSpan(string.convert('PLEASE DO NOT SHOUT', 'html'))).toBe('Please Do Not Shout');
expect(string.convert('Mean, variance and standard_deviation.')).toBe(
'Mean, Variance And Standard_deviation.'
);
+ expect(stripSpan(string.convert('Mean, variance and standard_deviation.', 'html'))).toBe(
+ 'Mean, Variance And Standard_deviation.'
+ );
expect(string.convert('Stay CALM!')).toBe('Stay Calm!');
+ expect(stripSpan(string.convert('Stay CALM!', 'html'))).toBe('Stay Calm!');
});
test('convert a string to short case', () => {
@@ -61,6 +77,7 @@ describe('String Format', () => {
jest.fn()
);
expect(string.convert('dot.notated.string')).toBe('d.n.string');
+ expect(stripSpan(string.convert('dot.notated.string', 'html'))).toBe('d.n.string');
});
test('convert a string to unknown transform case', () => {
@@ -82,5 +99,16 @@ describe('String Format', () => {
jest.fn()
);
expect(string.convert('%EC%95%88%EB%85%95%20%ED%82%A4%EB%B0%94%EB%82%98')).toBe('안녕 키바나');
+ expect(
+ stripSpan(string.convert('%EC%95%88%EB%85%95%20%ED%82%A4%EB%B0%94%EB%82%98', 'html'))
+ ).toBe('안녕 키바나');
+ });
+
+ test('outputs specific empty value', () => {
+ const string = new StringFormat();
+ expect(string.convert('')).toBe('(empty)');
+ expect(stripSpan(string.convert('', 'html'))).toBe(
+ '(empty)'
+ );
});
});
diff --git a/src/plugins/data/common/field_formats/converters/string.ts b/src/plugins/data/common/field_formats/converters/string.ts
index 64367df5d90dd..28dd714abaf41 100644
--- a/src/plugins/data/common/field_formats/converters/string.ts
+++ b/src/plugins/data/common/field_formats/converters/string.ts
@@ -6,14 +6,15 @@
* Side Public License, v 1.
*/
+import escape from 'lodash/escape';
import { i18n } from '@kbn/i18n';
-import { asPrettyString } from '../utils';
+import { asPrettyString, getHighlightHtml } from '../utils';
import { KBN_FIELD_TYPES } from '../../kbn_field_types/types';
import { FieldFormat } from '../field_format';
-import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types';
+import { TextContextTypeConvert, FIELD_FORMAT_IDS, HtmlContextTypeConvert } from '../types';
import { shortenDottedString } from '../../utils';
-export const emptyLabel = i18n.translate('data.fieldFormats.string.emptyLabel', {
+const emptyLabel = i18n.translate('data.fieldFormats.string.emptyLabel', {
defaultMessage: '(empty)',
});
@@ -127,4 +128,14 @@ export class StringFormat extends FieldFormat {
return asPrettyString(val);
}
};
+
+ htmlConvert: HtmlContextTypeConvert = (val, { hit, field } = {}) => {
+ if (val === '') {
+ return `${emptyLabel}`;
+ }
+
+ return hit?.highlight?.[field?.name]
+ ? getHighlightHtml(val, hit.highlight[field.name])
+ : escape(this.textConvert(val));
+ };
}
diff --git a/src/plugins/data/public/field_formats/converters/_index.scss b/src/plugins/data/public/field_formats/converters/_index.scss
new file mode 100644
index 0000000000000..cc13062a3ef8b
--- /dev/null
+++ b/src/plugins/data/public/field_formats/converters/_index.scss
@@ -0,0 +1 @@
+@import './string';
diff --git a/src/plugins/data/public/field_formats/converters/_string.scss b/src/plugins/data/public/field_formats/converters/_string.scss
new file mode 100644
index 0000000000000..9d97f0195780c
--- /dev/null
+++ b/src/plugins/data/public/field_formats/converters/_string.scss
@@ -0,0 +1,3 @@
+.ffString__emptyValue {
+ color: $euiColorDarkShade;
+}
diff --git a/src/plugins/data/public/index.scss b/src/plugins/data/public/index.scss
index 467efa98934ec..c0eebf3402771 100644
--- a/src/plugins/data/public/index.scss
+++ b/src/plugins/data/public/index.scss
@@ -1,2 +1,3 @@
@import './ui/index';
@import './utils/table_inspector_view/index';
+@import './field_formats/converters/index';
From 258d33c12029d8833d9d7d328124237e6959e863 Mon Sep 17 00:00:00 2001
From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
Date: Thu, 1 Jul 2021 04:21:46 -0400
Subject: [PATCH 26/51] [Cases] Adding migration tests for owner field added in
7.14 (#102577)
* Adding migration tests for 7.13 to 7.14
* Adding test for connector mapping
* Comments
---
.../tests/common/cases/migrations.ts | 25 +-
.../tests/common/comments/migrations.ts | 54 +-
.../tests/common/configure/migrations.ts | 67 +-
.../tests/common/connectors/migrations.ts | 39 +
.../tests/common/migrations.ts | 2 +
.../tests/common/user_actions/migrations.ts | 86 +-
.../cases/migrations/7.13.2/data.json.gz | Bin 0 -> 1351 bytes
.../cases/migrations/7.13.2/mappings.json | 2909 +++++++++++++++++
8 files changed, 3117 insertions(+), 65 deletions(-)
create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/migrations.ts
create mode 100644 x-pack/test/functional/es_archives/cases/migrations/7.13.2/data.json.gz
create mode 100644 x-pack/test/functional/es_archives/cases/migrations/7.13.2/mappings.json
diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts
index 8d158cc1c4f70..941b71fb925db 100644
--- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts
+++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts
@@ -7,7 +7,11 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
-import { CASES_URL } from '../../../../../../plugins/cases/common/constants';
+import {
+ CASES_URL,
+ SECURITY_SOLUTION_OWNER,
+} from '../../../../../../plugins/cases/common/constants';
+import { getCase } from '../../../../common/lib/utils';
// eslint-disable-next-line import/no-default-export
export default function createGetTests({ getService }: FtrProviderContext) {
@@ -107,5 +111,24 @@ export default function createGetTests({ getService }: FtrProviderContext) {
});
});
});
+
+ describe('7.13.2', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
+ });
+
+ it('adds the owner field', async () => {
+ const theCase = await getCase({
+ supertest,
+ caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
+ });
+
+ expect(theCase.owner).to.be(SECURITY_SOLUTION_OWNER);
+ });
+ });
});
}
diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/migrations.ts
index 357373e7805ee..67e30987fabac 100644
--- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/migrations.ts
+++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/migrations.ts
@@ -7,7 +7,11 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
-import { CASES_URL } from '../../../../../../plugins/cases/common/constants';
+import {
+ CASES_URL,
+ SECURITY_SOLUTION_OWNER,
+} from '../../../../../../plugins/cases/common/constants';
+import { getComment } from '../../../../common/lib/utils';
// eslint-disable-next-line import/no-default-export
export default function createGetTests({ getService }: FtrProviderContext) {
@@ -15,23 +19,45 @@ export default function createGetTests({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
describe('migrations', () => {
- before(async () => {
- await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
- });
+ describe('7.11.0', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
+ });
+
+ it('7.11.0 migrates cases comments', async () => {
+ const { body: comment } = await supertest
+ .get(
+ `${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509/comments/da677740-1ac7-11eb-b5a3-25ee88122510`
+ )
+ .set('kbn-xsrf', 'true')
+ .send();
- after(async () => {
- await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
+ expect(comment.type).to.eql('user');
+ });
});
- it('7.11.0 migrates cases comments', async () => {
- const { body: comment } = await supertest
- .get(
- `${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509/comments/da677740-1ac7-11eb-b5a3-25ee88122510`
- )
- .set('kbn-xsrf', 'true')
- .send();
+ describe('7.13.2', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
+ });
+
+ it('adds the owner field', async () => {
+ const comment = await getComment({
+ supertest,
+ caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
+ commentId: 'ee59cdd0-cf9d-11eb-a603-13e7747d215c',
+ });
- expect(comment.type).to.eql('user');
+ expect(comment.owner).to.be(SECURITY_SOLUTION_OWNER);
+ });
});
});
}
diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts
index c6d892e3435f1..bf64500a88068 100644
--- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts
+++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts
@@ -7,7 +7,11 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
-import { CASE_CONFIGURE_URL } from '../../../../../../plugins/cases/common/constants';
+import {
+ CASE_CONFIGURE_URL,
+ SECURITY_SOLUTION_OWNER,
+} from '../../../../../../plugins/cases/common/constants';
+import { getConfiguration } from '../../../../common/lib/utils';
// eslint-disable-next-line import/no-default-export
export default function createGetTests({ getService }: FtrProviderContext) {
@@ -15,29 +19,50 @@ export default function createGetTests({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
describe('migrations', () => {
- before(async () => {
- await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
- });
+ describe('7.10.0', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
+ });
+
+ it('7.10.0 migrates configure cases connector', async () => {
+ const { body } = await supertest
+ .get(`${CASE_CONFIGURE_URL}`)
+ .set('kbn-xsrf', 'true')
+ .send()
+ .expect(200);
- after(async () => {
- await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
+ expect(body.length).to.be(1);
+ expect(body[0]).key('connector');
+ expect(body[0]).not.key('connector_id');
+ expect(body[0].connector).to.eql({
+ id: 'connector-1',
+ name: 'Connector 1',
+ type: '.none',
+ fields: null,
+ });
+ });
});
- it('7.10.0 migrates configure cases connector', async () => {
- const { body } = await supertest
- .get(`${CASE_CONFIGURE_URL}`)
- .set('kbn-xsrf', 'true')
- .send()
- .expect(200);
-
- expect(body.length).to.be(1);
- expect(body[0]).key('connector');
- expect(body[0]).not.key('connector_id');
- expect(body[0].connector).to.eql({
- id: 'connector-1',
- name: 'Connector 1',
- type: '.none',
- fields: null,
+ describe('7.13.2', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
+ });
+
+ it('adds the owner field', async () => {
+ const configuration = await getConfiguration({
+ supertest,
+ query: { owner: SECURITY_SOLUTION_OWNER },
+ });
+
+ expect(configuration[0].owner).to.be(SECURITY_SOLUTION_OWNER);
});
});
});
diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/migrations.ts
new file mode 100644
index 0000000000000..863c565b4ab08
--- /dev/null
+++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/migrations.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
+import { SECURITY_SOLUTION_OWNER } from '../../../../../../plugins/cases/common/constants';
+import { getConnectorMappingsFromES } from '../../../../common/lib/utils';
+
+// eslint-disable-next-line import/no-default-export
+export default function createGetTests({ getService }: FtrProviderContext) {
+ const es = getService('es');
+ const esArchiver = getService('esArchiver');
+
+ describe('migrations', () => {
+ describe('7.13.2', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
+ });
+
+ it('adds the owner field', async () => {
+ // We don't get the owner field back from the mappings when we retrieve the configuration so the only way to
+ // check that the migration worked is by checking the saved object stored in Elasticsearch directly
+ const mappings = await getConnectorMappingsFromES({ es });
+ expect(mappings.body.hits.hits.length).to.be(1);
+ expect(mappings.body.hits.hits[0]._source?.['cases-connector-mappings'].owner).to.eql(
+ SECURITY_SOLUTION_OWNER
+ );
+ });
+ });
+ });
+}
diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts
index 17d93e76bbdda..810fecc127d08 100644
--- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts
+++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts
@@ -12,7 +12,9 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
describe('Common migrations', function () {
// Migrations
loadTestFile(require.resolve('./cases/migrations'));
+ loadTestFile(require.resolve('./comments/migrations'));
loadTestFile(require.resolve('./configure/migrations'));
loadTestFile(require.resolve('./user_actions/migrations'));
+ loadTestFile(require.resolve('./connectors/migrations'));
});
};
diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts
index 030441028c502..b4c2dca47bf5f 100644
--- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts
+++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts
@@ -7,7 +7,11 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
-import { CASES_URL } from '../../../../../../plugins/cases/common/constants';
+import {
+ CASES_URL,
+ SECURITY_SOLUTION_OWNER,
+} from '../../../../../../plugins/cases/common/constants';
+import { getCaseUserActions } from '../../../../common/lib/utils';
// eslint-disable-next-line import/no-default-export
export default function createGetTests({ getService }: FtrProviderContext) {
@@ -15,38 +19,62 @@ export default function createGetTests({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
describe('migrations', () => {
- before(async () => {
- await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
- });
+ describe('7.10.0', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
+ });
+
+ it('7.10.0 migrates user actions connector', async () => {
+ const { body } = await supertest
+ .get(`${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509/user_actions`)
+ .set('kbn-xsrf', 'true')
+ .send()
+ .expect(200);
+
+ const connectorUserAction = body[1];
+ const oldValue = JSON.parse(connectorUserAction.old_value);
+ const newValue = JSON.parse(connectorUserAction.new_value);
- after(async () => {
- await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.10.0');
+ expect(connectorUserAction.action_field.length).eql(1);
+ expect(connectorUserAction.action_field[0]).eql('connector');
+ expect(oldValue).to.eql({
+ id: 'c1900ac0-017f-11eb-93f8-d161651bf509',
+ name: 'none',
+ type: '.none',
+ fields: null,
+ });
+ expect(newValue).to.eql({
+ id: 'b1900ac0-017f-11eb-93f8-d161651bf509',
+ name: 'none',
+ type: '.none',
+ fields: null,
+ });
+ });
});
- it('7.10.0 migrates user actions connector', async () => {
- const { body } = await supertest
- .get(`${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509/user_actions`)
- .set('kbn-xsrf', 'true')
- .send()
- .expect(200);
-
- const connectorUserAction = body[1];
- const oldValue = JSON.parse(connectorUserAction.old_value);
- const newValue = JSON.parse(connectorUserAction.new_value);
-
- expect(connectorUserAction.action_field.length).eql(1);
- expect(connectorUserAction.action_field[0]).eql('connector');
- expect(oldValue).to.eql({
- id: 'c1900ac0-017f-11eb-93f8-d161651bf509',
- name: 'none',
- type: '.none',
- fields: null,
+ describe('7.13.2', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
});
- expect(newValue).to.eql({
- id: 'b1900ac0-017f-11eb-93f8-d161651bf509',
- name: 'none',
- type: '.none',
- fields: null,
+
+ it('adds the owner field', async () => {
+ const userActions = await getCaseUserActions({
+ supertest,
+ caseID: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
+ });
+
+ expect(userActions.length).to.not.be(0);
+ for (const action of userActions) {
+ expect(action.owner).to.be(SECURITY_SOLUTION_OWNER);
+ }
});
});
});
diff --git a/x-pack/test/functional/es_archives/cases/migrations/7.13.2/data.json.gz b/x-pack/test/functional/es_archives/cases/migrations/7.13.2/data.json.gz
new file mode 100644
index 0000000000000000000000000000000000000000..c86af3f7d2fbaec66f201962574ac6ce500f4688
GIT binary patch
literal 1351
zcmV-N1-SYjiwFqRrpsUe17u-zVJ>QOZ*BnX8BLF)HuRof5pfO-kN_d4?zK{@9!5RP
zW)wN_l3|xXWSg0dM)~ix4F;Q#3{0DDBJB~sPd~qJ-bdrHK@WQ{7GopMBV)o3UQt@`
zlXv#PK9iV3BSe6h1p#cLh$eYP7T|-u@jcs&HeqZ!4y;Y&+f&n-LJ-V?*mb;;BWIGu
z@PqTQz9(yxp_L;cBrkB(h#Pl_QlJc89&%>;g1neCHyP?{!kA3jNGn7+o@={~Y5S%V
zJUa7*7c6F;<+-z`4ubVT6UG3rLrKaGvm#9=DqzD7RvFp}>wU{JL|;&5tqV#sz`sF?
z2u&g@$_?HzdFPX-LpcM!P`ncqVc}0aG5QZ
z#$tKa7)xVPqplAOl~GD%1!1kNz^rdCU>B}0u!RvBCO6`deMfngK}2)BN@Bqv_UE2G
z-&}Mi!KtCFg+BtJM*3S2~rP9ow;e9YPbDR-}LZX@;Q=EpRGeFO=@d;^Cq6
zN6LgG+15go-Hascsu|FSHO}7%JV-W2)uZw>uAhP5d(?Vib`=bNBwLL5Gye^kRBjzu
z&eU>iR>tlWM+t|{aN#^`%QjAAV%PwNECQl=l;-he#KSB^F$?h?;Vu2Ha%*OvJW3FS
z*c8g?STf&~pvLh>El1W7^$XTG#Xjb8F2(uGeJZ6?MyUC&c*YxX$;Z*?ePAkF3^32|
z?ALnXG$-
z{~0V+OgPTqTB5V7D|pXtrXe|TgA0!ESy%Yj8PwoX9fJVnh9K=1Cx{g8KApos$cWVn7@(6tO`?>#go@0E>NmMKd3<7+_I;^
zQy0ZgkI-v2Bo&$4y9YT__j{LmZs@?jMyWTWe79poiq(AZ!YGcfH1S9=t!bxfrkI%@
z6~kyLza*@@xBBQ`L^@pb7K@0Q16!?*n(JC1`OaL>*|2TVXaRcl&Ru!-VG(l=U3-1G
zAz?fZt`f%j^1}1%(EXXqi!#Or>?}@uy*t}ksBNIezbfRq!|@WXpNJCO-Pg7F$+n`i
z)VJ3o+=s-#-xPSU5NNgpWlvBZGPT&**Ja&LERAP&uG~$K@E%v+SCku-q3ez_OO9G+
zF*}qC88OyXQsm55Z9v#`R<3u%v{mh4_;)oW$;}L3%}C>woSIIIee+K2_-n^;V>=Gl
zVu|d3$d~}rHQZCi>}YK6EMv-Mu2&wbkY&b_Uuw4CYW><${+kG%TvVq2eDHxXr;XF8
zRt?T|6ATNZFbJ}sP`CC4~ftg^#ZZ6v-;HP;8TJuM8|rQXT%
z&~+?3bZ&m`yQN93@uFd=>-=~mIra@k*R|$>eXU?Do!s1^{hqggEGq43Gz+g6l`Vm~
z)s<5
zX~L`rS$tlN&Jv^Pa9Uv0Meg&bgROsg|9sk1Dr(h1cBan?*1Ljs`0Rktb$hKg{sHTz
JR$P}M001q&oJ0Ts
literal 0
HcmV?d00001
diff --git a/x-pack/test/functional/es_archives/cases/migrations/7.13.2/mappings.json b/x-pack/test/functional/es_archives/cases/migrations/7.13.2/mappings.json
new file mode 100644
index 0000000000000..e79ebf2b8fc10
--- /dev/null
+++ b/x-pack/test/functional/es_archives/cases/migrations/7.13.2/mappings.json
@@ -0,0 +1,2909 @@
+{
+ "type": "index",
+ "value": {
+ "aliases": {
+ ".kibana": {
+ },
+ ".kibana_7.13.2": {
+ }
+ },
+ "index": ".kibana_1",
+ "mappings": {
+ "_meta": {
+ "migrationMappingPropertyHashes": {
+ "action": "6e96ac5e648f57523879661ea72525b7",
+ "action_task_params": "a9d49f184ee89641044be0ca2950fa3a",
+ "alert": "d75d3b0e95fe394753d73d8f7952cd7d",
+ "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7",
+ "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd",
+ "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724",
+ "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724",
+ "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd",
+ "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724",
+ "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724",
+ "canvas-element": "7390014e1091044523666d97247392fc",
+ "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231",
+ "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715",
+ "cases": "7c28a18fbac7c2a4e79449e9802ef476",
+ "cases-comments": "112cefc2b6737e613a8ef033234755e6",
+ "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69",
+ "cases-connector-mappings": "6bc7e49411d38be4969dc6aa8bd43776",
+ "cases-user-actions": "32277330ec6b721abe3b846cfd939a71",
+ "config": "c63748b75f39d0c54de12d12c1ccbc20",
+ "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724",
+ "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1",
+ "dashboard": "40554caf09725935e2c02e02563a2d07",
+ "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0",
+ "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862",
+ "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724",
+ "epm-packages": "0cbbb16506734d341a96aaed65ec6413",
+ "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b",
+ "exception-list": "baf108c9934dda844921f692a513adae",
+ "exception-list-agnostic": "baf108c9934dda844921f692a513adae",
+ "file-upload-usage-collection-telemetry": "a34fbb8e3263d105044869264860c697",
+ "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9",
+ "fleet-agents": "59fd74f819f028f8555776db198d2562",
+ "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7",
+ "fleet-preconfiguration-deletion-record": "4c36f199189a367e43541f236141204c",
+ "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752",
+ "index-pattern": "45915a1ad866812242df474eb0479052",
+ "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724",
+ "ingest-agent-policies": "cb4dbcc5a695e53f40a359303cb6286f",
+ "ingest-outputs": "1acb789ca37cbee70259ca79e124d9ad",
+ "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85",
+ "ingest_manager_settings": "f159646d76ab261bfbf8ef504d9631e4",
+ "inventory-view": "3d1b76c39bfb2cc8296b024d73854724",
+ "kql-telemetry": "d12a98a6f19a2d273696597547e064ee",
+ "legacy-url-alias": "3d1b76c39bfb2cc8296b024d73854724",
+ "lens": "52346cfec69ff7b47d5f0c12361a2797",
+ "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327",
+ "map": "9134b47593116d7953f6adba096fc463",
+ "maps-telemetry": "5ef305b18111b77789afefbd36b66171",
+ "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724",
+ "migrationVersion": "4a1746014a75ade3a714e1db5763276f",
+ "ml-job": "3bb64c31915acf93fc724af137a0891b",
+ "ml-module": "46ef4f0d6682636f0fff9799d6a2d7ac",
+ "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68",
+ "namespace": "2f4316de49999235636386fe51dc06c1",
+ "namespaces": "2f4316de49999235636386fe51dc06c1",
+ "originId": "2f4316de49999235636386fe51dc06c1",
+ "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9",
+ "references": "7997cf5a56cc02bdc9c93361bde732b0",
+ "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4",
+ "search": "db2c00e39b36f40930a3b9fc71c823e1",
+ "search-session": "4e238afeeaa2550adef326e140454265",
+ "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724",
+ "security-rule": "8ae39a88fc70af3375b7050e8d8d5cc7",
+ "security-solution-signals-migration": "72761fd374ca11122ac8025a92b84fca",
+ "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18",
+ "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0",
+ "siem-ui-timeline": "3e97beae13cdfc6d62bc1846119f7276",
+ "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084",
+ "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29",
+ "space": "c5ca8acafa0beaa4d08d014a97b6bc6b",
+ "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724",
+ "tag": "83d55da58f6530f7055415717ec06474",
+ "telemetry": "36a616f7026dfa617d6655df850fe16d",
+ "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf",
+ "type": "2f4316de49999235636386fe51dc06c1",
+ "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3",
+ "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3",
+ "updated_at": "00da57df13e94e9d98437d13ace4bfe0",
+ "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763",
+ "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b",
+ "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724",
+ "url": "c7f66a0df8b1b52f17c28c4adb111105",
+ "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4",
+ "visualization": "f819cf6636b75c9e76ba733a0c6ef355",
+ "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724"
+ }
+ },
+ "dynamic": "strict",
+ "properties": {
+ "action": {
+ "properties": {
+ "actionTypeId": {
+ "type": "keyword"
+ },
+ "config": {
+ "enabled": false,
+ "type": "object"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "secrets": {
+ "type": "binary"
+ }
+ }
+ },
+ "action_task_params": {
+ "properties": {
+ "actionId": {
+ "type": "keyword"
+ },
+ "apiKey": {
+ "type": "binary"
+ },
+ "params": {
+ "enabled": false,
+ "type": "object"
+ }
+ }
+ },
+ "alert": {
+ "properties": {
+ "actions": {
+ "properties": {
+ "actionRef": {
+ "type": "keyword"
+ },
+ "actionTypeId": {
+ "type": "keyword"
+ },
+ "group": {
+ "type": "keyword"
+ },
+ "params": {
+ "enabled": false,
+ "type": "object"
+ }
+ },
+ "type": "nested"
+ },
+ "alertTypeId": {
+ "type": "keyword"
+ },
+ "apiKey": {
+ "type": "binary"
+ },
+ "apiKeyOwner": {
+ "type": "keyword"
+ },
+ "consumer": {
+ "type": "keyword"
+ },
+ "createdAt": {
+ "type": "date"
+ },
+ "createdBy": {
+ "type": "keyword"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "executionStatus": {
+ "properties": {
+ "error": {
+ "properties": {
+ "message": {
+ "type": "keyword"
+ },
+ "reason": {
+ "type": "keyword"
+ }
+ }
+ },
+ "lastExecutionDate": {
+ "type": "date"
+ },
+ "status": {
+ "type": "keyword"
+ }
+ }
+ },
+ "meta": {
+ "properties": {
+ "versionApiKeyLastmodified": {
+ "type": "keyword"
+ }
+ }
+ },
+ "muteAll": {
+ "type": "boolean"
+ },
+ "mutedInstanceIds": {
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "notifyWhen": {
+ "type": "keyword"
+ },
+ "params": {
+ "ignore_above": 4096,
+ "type": "flattened"
+ },
+ "schedule": {
+ "properties": {
+ "interval": {
+ "type": "keyword"
+ }
+ }
+ },
+ "scheduledTaskId": {
+ "type": "keyword"
+ },
+ "tags": {
+ "type": "keyword"
+ },
+ "throttle": {
+ "type": "keyword"
+ },
+ "updatedAt": {
+ "type": "date"
+ },
+ "updatedBy": {
+ "type": "keyword"
+ }
+ }
+ },
+ "api_key_pending_invalidation": {
+ "properties": {
+ "apiKeyId": {
+ "type": "keyword"
+ },
+ "createdAt": {
+ "type": "date"
+ }
+ }
+ },
+ "apm-indices": {
+ "properties": {
+ "apm_oss": {
+ "properties": {
+ "errorIndices": {
+ "type": "keyword"
+ },
+ "metricsIndices": {
+ "type": "keyword"
+ },
+ "onboardingIndices": {
+ "type": "keyword"
+ },
+ "sourcemapIndices": {
+ "type": "keyword"
+ },
+ "spanIndices": {
+ "type": "keyword"
+ },
+ "transactionIndices": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "apm-telemetry": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "app_search_telemetry": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "application_usage_daily": {
+ "dynamic": "false",
+ "properties": {
+ "timestamp": {
+ "type": "date"
+ }
+ }
+ },
+ "application_usage_totals": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "application_usage_transactional": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "canvas-element": {
+ "dynamic": "false",
+ "properties": {
+ "@created": {
+ "type": "date"
+ },
+ "@timestamp": {
+ "type": "date"
+ },
+ "content": {
+ "type": "text"
+ },
+ "help": {
+ "type": "text"
+ },
+ "image": {
+ "type": "text"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "canvas-workpad": {
+ "dynamic": "false",
+ "properties": {
+ "@created": {
+ "type": "date"
+ },
+ "@timestamp": {
+ "type": "date"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "canvas-workpad-template": {
+ "dynamic": "false",
+ "properties": {
+ "help": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "tags": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "template_key": {
+ "type": "keyword"
+ }
+ }
+ },
+ "cases": {
+ "properties": {
+ "closed_at": {
+ "type": "date"
+ },
+ "closed_by": {
+ "properties": {
+ "email": {
+ "type": "keyword"
+ },
+ "full_name": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ }
+ }
+ },
+ "connector": {
+ "properties": {
+ "fields": {
+ "properties": {
+ "key": {
+ "type": "text"
+ },
+ "value": {
+ "type": "text"
+ }
+ }
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "text"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "created_at": {
+ "type": "date"
+ },
+ "created_by": {
+ "properties": {
+ "email": {
+ "type": "keyword"
+ },
+ "full_name": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ }
+ }
+ },
+ "description": {
+ "type": "text"
+ },
+ "external_service": {
+ "properties": {
+ "connector_id": {
+ "type": "keyword"
+ },
+ "connector_name": {
+ "type": "keyword"
+ },
+ "external_id": {
+ "type": "keyword"
+ },
+ "external_title": {
+ "type": "text"
+ },
+ "external_url": {
+ "type": "text"
+ },
+ "pushed_at": {
+ "type": "date"
+ },
+ "pushed_by": {
+ "properties": {
+ "email": {
+ "type": "keyword"
+ },
+ "full_name": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "settings": {
+ "properties": {
+ "syncAlerts": {
+ "type": "boolean"
+ }
+ }
+ },
+ "status": {
+ "type": "keyword"
+ },
+ "tags": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "updated_by": {
+ "properties": {
+ "email": {
+ "type": "keyword"
+ },
+ "full_name": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "cases-comments": {
+ "properties": {
+ "alertId": {
+ "type": "keyword"
+ },
+ "associationType": {
+ "type": "keyword"
+ },
+ "comment": {
+ "type": "text"
+ },
+ "created_at": {
+ "type": "date"
+ },
+ "created_by": {
+ "properties": {
+ "email": {
+ "type": "keyword"
+ },
+ "full_name": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ }
+ }
+ },
+ "index": {
+ "type": "keyword"
+ },
+ "pushed_at": {
+ "type": "date"
+ },
+ "pushed_by": {
+ "properties": {
+ "email": {
+ "type": "keyword"
+ },
+ "full_name": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ }
+ }
+ },
+ "rule": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ }
+ }
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "updated_by": {
+ "properties": {
+ "email": {
+ "type": "keyword"
+ },
+ "full_name": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "cases-configure": {
+ "properties": {
+ "closure_type": {
+ "type": "keyword"
+ },
+ "connector": {
+ "properties": {
+ "fields": {
+ "properties": {
+ "key": {
+ "type": "text"
+ },
+ "value": {
+ "type": "text"
+ }
+ }
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "text"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "created_at": {
+ "type": "date"
+ },
+ "created_by": {
+ "properties": {
+ "email": {
+ "type": "keyword"
+ },
+ "full_name": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ }
+ }
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "updated_by": {
+ "properties": {
+ "email": {
+ "type": "keyword"
+ },
+ "full_name": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "cases-connector-mappings": {
+ "properties": {
+ "mappings": {
+ "properties": {
+ "action_type": {
+ "type": "keyword"
+ },
+ "source": {
+ "type": "keyword"
+ },
+ "target": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "cases-user-actions": {
+ "properties": {
+ "action": {
+ "type": "keyword"
+ },
+ "action_at": {
+ "type": "date"
+ },
+ "action_by": {
+ "properties": {
+ "email": {
+ "type": "keyword"
+ },
+ "full_name": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ }
+ }
+ },
+ "action_field": {
+ "type": "keyword"
+ },
+ "new_value": {
+ "type": "text"
+ },
+ "old_value": {
+ "type": "text"
+ }
+ }
+ },
+ "config": {
+ "dynamic": "false",
+ "properties": {
+ "buildNum": {
+ "type": "keyword"
+ }
+ }
+ },
+ "core-usage-stats": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "coreMigrationVersion": {
+ "type": "keyword"
+ },
+ "dashboard": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "doc_values": false,
+ "index": false,
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "index": false,
+ "type": "text"
+ }
+ }
+ },
+ "optionsJSON": {
+ "index": false,
+ "type": "text"
+ },
+ "panelsJSON": {
+ "index": false,
+ "type": "text"
+ },
+ "refreshInterval": {
+ "properties": {
+ "display": {
+ "doc_values": false,
+ "index": false,
+ "type": "keyword"
+ },
+ "pause": {
+ "doc_values": false,
+ "index": false,
+ "type": "boolean"
+ },
+ "section": {
+ "doc_values": false,
+ "index": false,
+ "type": "integer"
+ },
+ "value": {
+ "doc_values": false,
+ "index": false,
+ "type": "integer"
+ }
+ }
+ },
+ "timeFrom": {
+ "doc_values": false,
+ "index": false,
+ "type": "keyword"
+ },
+ "timeRestore": {
+ "doc_values": false,
+ "index": false,
+ "type": "boolean"
+ },
+ "timeTo": {
+ "doc_values": false,
+ "index": false,
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "endpoint:user-artifact": {
+ "properties": {
+ "body": {
+ "type": "binary"
+ },
+ "compressionAlgorithm": {
+ "index": false,
+ "type": "keyword"
+ },
+ "created": {
+ "index": false,
+ "type": "date"
+ },
+ "decodedSha256": {
+ "index": false,
+ "type": "keyword"
+ },
+ "decodedSize": {
+ "index": false,
+ "type": "long"
+ },
+ "encodedSha256": {
+ "type": "keyword"
+ },
+ "encodedSize": {
+ "index": false,
+ "type": "long"
+ },
+ "encryptionAlgorithm": {
+ "index": false,
+ "type": "keyword"
+ },
+ "identifier": {
+ "type": "keyword"
+ }
+ }
+ },
+ "endpoint:user-artifact-manifest": {
+ "properties": {
+ "artifacts": {
+ "properties": {
+ "artifactId": {
+ "index": false,
+ "type": "keyword"
+ },
+ "policyId": {
+ "index": false,
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "created": {
+ "index": false,
+ "type": "date"
+ },
+ "schemaVersion": {
+ "type": "keyword"
+ },
+ "semanticVersion": {
+ "index": false,
+ "type": "keyword"
+ }
+ }
+ },
+ "enterprise_search_telemetry": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "epm-packages": {
+ "properties": {
+ "es_index_patterns": {
+ "enabled": false,
+ "type": "object"
+ },
+ "install_source": {
+ "type": "keyword"
+ },
+ "install_started_at": {
+ "type": "date"
+ },
+ "install_status": {
+ "type": "keyword"
+ },
+ "install_version": {
+ "type": "keyword"
+ },
+ "installed_es": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "installed_kibana": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "internal": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "package_assets": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "removable": {
+ "type": "boolean"
+ },
+ "version": {
+ "type": "keyword"
+ }
+ }
+ },
+ "epm-packages-assets": {
+ "properties": {
+ "asset_path": {
+ "type": "keyword"
+ },
+ "data_base64": {
+ "type": "binary"
+ },
+ "data_utf8": {
+ "index": false,
+ "type": "text"
+ },
+ "install_source": {
+ "type": "keyword"
+ },
+ "media_type": {
+ "type": "keyword"
+ },
+ "package_name": {
+ "type": "keyword"
+ },
+ "package_version": {
+ "type": "keyword"
+ }
+ }
+ },
+ "exception-list": {
+ "properties": {
+ "_tags": {
+ "type": "keyword"
+ },
+ "comments": {
+ "properties": {
+ "comment": {
+ "type": "keyword"
+ },
+ "created_at": {
+ "type": "keyword"
+ },
+ "created_by": {
+ "type": "keyword"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "keyword"
+ },
+ "updated_by": {
+ "type": "keyword"
+ }
+ }
+ },
+ "created_at": {
+ "type": "keyword"
+ },
+ "created_by": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "keyword"
+ },
+ "entries": {
+ "properties": {
+ "entries": {
+ "properties": {
+ "field": {
+ "type": "keyword"
+ },
+ "operator": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "value": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ }
+ }
+ },
+ "field": {
+ "type": "keyword"
+ },
+ "list": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "operator": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "value": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ }
+ }
+ },
+ "immutable": {
+ "type": "boolean"
+ },
+ "item_id": {
+ "type": "keyword"
+ },
+ "list_id": {
+ "type": "keyword"
+ },
+ "list_type": {
+ "type": "keyword"
+ },
+ "meta": {
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ },
+ "os_types": {
+ "type": "keyword"
+ },
+ "tags": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ },
+ "tie_breaker_id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_by": {
+ "type": "keyword"
+ },
+ "version": {
+ "type": "keyword"
+ }
+ }
+ },
+ "exception-list-agnostic": {
+ "properties": {
+ "_tags": {
+ "type": "keyword"
+ },
+ "comments": {
+ "properties": {
+ "comment": {
+ "type": "keyword"
+ },
+ "created_at": {
+ "type": "keyword"
+ },
+ "created_by": {
+ "type": "keyword"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "keyword"
+ },
+ "updated_by": {
+ "type": "keyword"
+ }
+ }
+ },
+ "created_at": {
+ "type": "keyword"
+ },
+ "created_by": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "keyword"
+ },
+ "entries": {
+ "properties": {
+ "entries": {
+ "properties": {
+ "field": {
+ "type": "keyword"
+ },
+ "operator": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "value": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ }
+ }
+ },
+ "field": {
+ "type": "keyword"
+ },
+ "list": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "operator": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "value": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ }
+ }
+ },
+ "immutable": {
+ "type": "boolean"
+ },
+ "item_id": {
+ "type": "keyword"
+ },
+ "list_id": {
+ "type": "keyword"
+ },
+ "list_type": {
+ "type": "keyword"
+ },
+ "meta": {
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ },
+ "os_types": {
+ "type": "keyword"
+ },
+ "tags": {
+ "fields": {
+ "text": {
+ "type": "text"
+ }
+ },
+ "type": "keyword"
+ },
+ "tie_breaker_id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_by": {
+ "type": "keyword"
+ },
+ "version": {
+ "type": "keyword"
+ }
+ }
+ },
+ "file-upload-usage-collection-telemetry": {
+ "properties": {
+ "file_upload": {
+ "properties": {
+ "index_creation_count": {
+ "type": "long"
+ }
+ }
+ }
+ }
+ },
+ "fleet-agent-actions": {
+ "properties": {
+ "ack_data": {
+ "type": "text"
+ },
+ "agent_id": {
+ "type": "keyword"
+ },
+ "created_at": {
+ "type": "date"
+ },
+ "data": {
+ "type": "binary"
+ },
+ "policy_id": {
+ "type": "keyword"
+ },
+ "policy_revision": {
+ "type": "integer"
+ },
+ "sent_at": {
+ "type": "date"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "fleet-agents": {
+ "properties": {
+ "access_api_key_id": {
+ "type": "keyword"
+ },
+ "active": {
+ "type": "boolean"
+ },
+ "current_error_events": {
+ "index": false,
+ "type": "text"
+ },
+ "default_api_key": {
+ "type": "binary"
+ },
+ "default_api_key_id": {
+ "type": "keyword"
+ },
+ "enrolled_at": {
+ "type": "date"
+ },
+ "last_checkin": {
+ "type": "date"
+ },
+ "last_checkin_status": {
+ "type": "keyword"
+ },
+ "last_updated": {
+ "type": "date"
+ },
+ "local_metadata": {
+ "type": "flattened"
+ },
+ "packages": {
+ "type": "keyword"
+ },
+ "policy_id": {
+ "type": "keyword"
+ },
+ "policy_revision": {
+ "type": "integer"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "unenrolled_at": {
+ "type": "date"
+ },
+ "unenrollment_started_at": {
+ "type": "date"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "upgrade_started_at": {
+ "type": "date"
+ },
+ "upgraded_at": {
+ "type": "date"
+ },
+ "user_provided_metadata": {
+ "type": "flattened"
+ },
+ "version": {
+ "type": "keyword"
+ }
+ }
+ },
+ "fleet-enrollment-api-keys": {
+ "properties": {
+ "active": {
+ "type": "boolean"
+ },
+ "api_key": {
+ "type": "binary"
+ },
+ "api_key_id": {
+ "type": "keyword"
+ },
+ "created_at": {
+ "type": "date"
+ },
+ "expire_at": {
+ "type": "date"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "policy_id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ }
+ }
+ },
+ "fleet-preconfiguration-deletion-record": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ }
+ }
+ },
+ "graph-workspace": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "legacyIndexPatternRef": {
+ "index": false,
+ "type": "text"
+ },
+ "numLinks": {
+ "type": "integer"
+ },
+ "numVertices": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "wsState": {
+ "type": "text"
+ }
+ }
+ },
+ "index-pattern": {
+ "dynamic": "false",
+ "properties": {
+ "title": {
+ "type": "text"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "infrastructure-ui-source": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "ingest-agent-policies": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "is_default": {
+ "type": "boolean"
+ },
+ "is_default_fleet_server": {
+ "type": "boolean"
+ },
+ "is_managed": {
+ "type": "boolean"
+ },
+ "is_preconfigured": {
+ "type": "keyword"
+ },
+ "monitoring_enabled": {
+ "index": false,
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "namespace": {
+ "type": "keyword"
+ },
+ "package_policies": {
+ "type": "keyword"
+ },
+ "revision": {
+ "type": "integer"
+ },
+ "status": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "updated_by": {
+ "type": "keyword"
+ }
+ }
+ },
+ "ingest-outputs": {
+ "properties": {
+ "ca_sha256": {
+ "index": false,
+ "type": "keyword"
+ },
+ "config": {
+ "type": "flattened"
+ },
+ "config_yaml": {
+ "type": "text"
+ },
+ "hosts": {
+ "type": "keyword"
+ },
+ "is_default": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "ingest-package-policies": {
+ "properties": {
+ "created_at": {
+ "type": "date"
+ },
+ "created_by": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "inputs": {
+ "enabled": false,
+ "properties": {
+ "compiled_input": {
+ "type": "flattened"
+ },
+ "config": {
+ "type": "flattened"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "streams": {
+ "properties": {
+ "compiled_stream": {
+ "type": "flattened"
+ },
+ "config": {
+ "type": "flattened"
+ },
+ "data_stream": {
+ "properties": {
+ "dataset": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "vars": {
+ "type": "flattened"
+ }
+ },
+ "type": "nested"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "vars": {
+ "type": "flattened"
+ }
+ },
+ "type": "nested"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "namespace": {
+ "type": "keyword"
+ },
+ "output_id": {
+ "type": "keyword"
+ },
+ "package": {
+ "properties": {
+ "name": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "keyword"
+ },
+ "version": {
+ "type": "keyword"
+ }
+ }
+ },
+ "policy_id": {
+ "type": "keyword"
+ },
+ "revision": {
+ "type": "integer"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "updated_by": {
+ "type": "keyword"
+ }
+ }
+ },
+ "ingest_manager_settings": {
+ "properties": {
+ "fleet_server_hosts": {
+ "type": "keyword"
+ },
+ "has_seen_add_data_notice": {
+ "index": false,
+ "type": "boolean"
+ },
+ "has_seen_fleet_migration_notice": {
+ "index": false,
+ "type": "boolean"
+ }
+ }
+ },
+ "inventory-view": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "kql-telemetry": {
+ "properties": {
+ "optInCount": {
+ "type": "long"
+ },
+ "optOutCount": {
+ "type": "long"
+ }
+ }
+ },
+ "legacy-url-alias": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "lens": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "expression": {
+ "doc_values": false,
+ "index": false,
+ "type": "keyword"
+ },
+ "state": {
+ "type": "flattened"
+ },
+ "title": {
+ "type": "text"
+ },
+ "visualizationType": {
+ "type": "keyword"
+ }
+ }
+ },
+ "lens-ui-telemetry": {
+ "properties": {
+ "count": {
+ "type": "integer"
+ },
+ "date": {
+ "type": "date"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "map": {
+ "properties": {
+ "bounds": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "description": {
+ "type": "text"
+ },
+ "layerListJSON": {
+ "type": "text"
+ },
+ "mapStateJSON": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "maps-telemetry": {
+ "enabled": false,
+ "type": "object"
+ },
+ "metrics-explorer-view": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "migrationVersion": {
+ "dynamic": "true",
+ "properties": {
+ "action": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "cases": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "cases-comments": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "cases-configure": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "cases-user-actions": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "config": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "dashboard": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "index-pattern": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "ingest-agent-policies": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "ingest-outputs": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "ingest-package-policies": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "ingest_manager_settings": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "search": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "space": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "visualization": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "ml-job": {
+ "properties": {
+ "datafeed_id": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "job_id": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "ml-module": {
+ "dynamic": "false",
+ "properties": {
+ "datafeeds": {
+ "type": "object"
+ },
+ "defaultIndexPattern": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "description": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "id": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "jobs": {
+ "type": "object"
+ },
+ "logo": {
+ "type": "object"
+ },
+ "query": {
+ "type": "object"
+ },
+ "title": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "type": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "monitoring-telemetry": {
+ "properties": {
+ "reportedClusterUuids": {
+ "type": "keyword"
+ }
+ }
+ },
+ "namespace": {
+ "type": "keyword"
+ },
+ "namespaces": {
+ "type": "keyword"
+ },
+ "originId": {
+ "type": "keyword"
+ },
+ "query": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "filters": {
+ "enabled": false,
+ "type": "object"
+ },
+ "query": {
+ "properties": {
+ "language": {
+ "type": "keyword"
+ },
+ "query": {
+ "index": false,
+ "type": "keyword"
+ }
+ }
+ },
+ "timefilter": {
+ "enabled": false,
+ "type": "object"
+ },
+ "title": {
+ "type": "text"
+ }
+ }
+ },
+ "references": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "sample-data-telemetry": {
+ "properties": {
+ "installCount": {
+ "type": "long"
+ },
+ "unInstallCount": {
+ "type": "long"
+ }
+ }
+ },
+ "search": {
+ "properties": {
+ "columns": {
+ "doc_values": false,
+ "index": false,
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "grid": {
+ "enabled": false,
+ "type": "object"
+ },
+ "hideChart": {
+ "doc_values": false,
+ "index": false,
+ "type": "boolean"
+ },
+ "hits": {
+ "doc_values": false,
+ "index": false,
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "index": false,
+ "type": "text"
+ }
+ }
+ },
+ "sort": {
+ "doc_values": false,
+ "index": false,
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "search-session": {
+ "properties": {
+ "appId": {
+ "type": "keyword"
+ },
+ "completed": {
+ "type": "date"
+ },
+ "created": {
+ "type": "date"
+ },
+ "expires": {
+ "type": "date"
+ },
+ "idMapping": {
+ "enabled": false,
+ "type": "object"
+ },
+ "initialState": {
+ "enabled": false,
+ "type": "object"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "persisted": {
+ "type": "boolean"
+ },
+ "realmName": {
+ "type": "keyword"
+ },
+ "realmType": {
+ "type": "keyword"
+ },
+ "restoreState": {
+ "enabled": false,
+ "type": "object"
+ },
+ "sessionId": {
+ "type": "keyword"
+ },
+ "status": {
+ "type": "keyword"
+ },
+ "touched": {
+ "type": "date"
+ },
+ "urlGeneratorId": {
+ "type": "keyword"
+ },
+ "username": {
+ "type": "keyword"
+ }
+ }
+ },
+ "search-telemetry": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "security-rule": {
+ "dynamic": "false",
+ "properties": {
+ "name": {
+ "type": "keyword"
+ },
+ "rule_id": {
+ "type": "keyword"
+ },
+ "version": {
+ "type": "long"
+ }
+ }
+ },
+ "security-solution-signals-migration": {
+ "properties": {
+ "created": {
+ "index": false,
+ "type": "date"
+ },
+ "createdBy": {
+ "index": false,
+ "type": "text"
+ },
+ "destinationIndex": {
+ "index": false,
+ "type": "keyword"
+ },
+ "error": {
+ "index": false,
+ "type": "text"
+ },
+ "sourceIndex": {
+ "type": "keyword"
+ },
+ "status": {
+ "index": false,
+ "type": "keyword"
+ },
+ "taskId": {
+ "index": false,
+ "type": "keyword"
+ },
+ "updated": {
+ "index": false,
+ "type": "date"
+ },
+ "updatedBy": {
+ "index": false,
+ "type": "text"
+ },
+ "version": {
+ "type": "long"
+ }
+ }
+ },
+ "siem-detection-engine-rule-actions": {
+ "properties": {
+ "actions": {
+ "properties": {
+ "action_type_id": {
+ "type": "keyword"
+ },
+ "group": {
+ "type": "keyword"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "params": {
+ "enabled": false,
+ "type": "object"
+ }
+ }
+ },
+ "alertThrottle": {
+ "type": "keyword"
+ },
+ "ruleAlertId": {
+ "type": "keyword"
+ },
+ "ruleThrottle": {
+ "type": "keyword"
+ }
+ }
+ },
+ "siem-detection-engine-rule-status": {
+ "properties": {
+ "alertId": {
+ "type": "keyword"
+ },
+ "bulkCreateTimeDurations": {
+ "type": "float"
+ },
+ "gap": {
+ "type": "text"
+ },
+ "lastFailureAt": {
+ "type": "date"
+ },
+ "lastFailureMessage": {
+ "type": "text"
+ },
+ "lastLookBackDate": {
+ "type": "date"
+ },
+ "lastSuccessAt": {
+ "type": "date"
+ },
+ "lastSuccessMessage": {
+ "type": "text"
+ },
+ "searchAfterTimeDurations": {
+ "type": "float"
+ },
+ "status": {
+ "type": "keyword"
+ },
+ "statusDate": {
+ "type": "date"
+ }
+ }
+ },
+ "siem-ui-timeline": {
+ "properties": {
+ "columns": {
+ "properties": {
+ "aggregatable": {
+ "type": "boolean"
+ },
+ "category": {
+ "type": "keyword"
+ },
+ "columnHeaderType": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "example": {
+ "type": "text"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "indexes": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "text"
+ },
+ "placeholder": {
+ "type": "text"
+ },
+ "searchable": {
+ "type": "boolean"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ }
+ },
+ "created": {
+ "type": "date"
+ },
+ "createdBy": {
+ "type": "text"
+ },
+ "dataProviders": {
+ "properties": {
+ "and": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "excluded": {
+ "type": "boolean"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "kqlQuery": {
+ "type": "text"
+ },
+ "name": {
+ "type": "text"
+ },
+ "queryMatch": {
+ "properties": {
+ "displayField": {
+ "type": "text"
+ },
+ "displayValue": {
+ "type": "text"
+ },
+ "field": {
+ "type": "text"
+ },
+ "operator": {
+ "type": "text"
+ },
+ "value": {
+ "type": "text"
+ }
+ }
+ },
+ "type": {
+ "type": "text"
+ }
+ }
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "excluded": {
+ "type": "boolean"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "kqlQuery": {
+ "type": "text"
+ },
+ "name": {
+ "type": "text"
+ },
+ "queryMatch": {
+ "properties": {
+ "displayField": {
+ "type": "text"
+ },
+ "displayValue": {
+ "type": "text"
+ },
+ "field": {
+ "type": "text"
+ },
+ "operator": {
+ "type": "text"
+ },
+ "value": {
+ "type": "text"
+ }
+ }
+ },
+ "type": {
+ "type": "text"
+ }
+ }
+ },
+ "dateRange": {
+ "properties": {
+ "end": {
+ "type": "date"
+ },
+ "start": {
+ "type": "date"
+ }
+ }
+ },
+ "description": {
+ "type": "text"
+ },
+ "eqlOptions": {
+ "properties": {
+ "eventCategoryField": {
+ "type": "text"
+ },
+ "query": {
+ "type": "text"
+ },
+ "size": {
+ "type": "text"
+ },
+ "tiebreakerField": {
+ "type": "text"
+ },
+ "timestampField": {
+ "type": "text"
+ }
+ }
+ },
+ "eventType": {
+ "type": "keyword"
+ },
+ "excludedRowRendererIds": {
+ "type": "text"
+ },
+ "favorite": {
+ "properties": {
+ "favoriteDate": {
+ "type": "date"
+ },
+ "fullName": {
+ "type": "text"
+ },
+ "keySearch": {
+ "type": "text"
+ },
+ "userName": {
+ "type": "text"
+ }
+ }
+ },
+ "filters": {
+ "properties": {
+ "exists": {
+ "type": "text"
+ },
+ "match_all": {
+ "type": "text"
+ },
+ "meta": {
+ "properties": {
+ "alias": {
+ "type": "text"
+ },
+ "controlledBy": {
+ "type": "text"
+ },
+ "disabled": {
+ "type": "boolean"
+ },
+ "field": {
+ "type": "text"
+ },
+ "formattedValue": {
+ "type": "text"
+ },
+ "index": {
+ "type": "keyword"
+ },
+ "key": {
+ "type": "keyword"
+ },
+ "negate": {
+ "type": "boolean"
+ },
+ "params": {
+ "type": "text"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "value": {
+ "type": "text"
+ }
+ }
+ },
+ "missing": {
+ "type": "text"
+ },
+ "query": {
+ "type": "text"
+ },
+ "range": {
+ "type": "text"
+ },
+ "script": {
+ "type": "text"
+ }
+ }
+ },
+ "indexNames": {
+ "type": "text"
+ },
+ "kqlMode": {
+ "type": "keyword"
+ },
+ "kqlQuery": {
+ "properties": {
+ "filterQuery": {
+ "properties": {
+ "kuery": {
+ "properties": {
+ "expression": {
+ "type": "text"
+ },
+ "kind": {
+ "type": "keyword"
+ }
+ }
+ },
+ "serializedQuery": {
+ "type": "text"
+ }
+ }
+ }
+ }
+ },
+ "savedQueryId": {
+ "type": "keyword"
+ },
+ "sort": {
+ "dynamic": "false",
+ "properties": {
+ "columnId": {
+ "type": "keyword"
+ },
+ "columnType": {
+ "type": "keyword"
+ },
+ "sortDirection": {
+ "type": "keyword"
+ }
+ }
+ },
+ "status": {
+ "type": "keyword"
+ },
+ "templateTimelineId": {
+ "type": "text"
+ },
+ "templateTimelineVersion": {
+ "type": "integer"
+ },
+ "timelineType": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "updated": {
+ "type": "date"
+ },
+ "updatedBy": {
+ "type": "text"
+ }
+ }
+ },
+ "siem-ui-timeline-note": {
+ "properties": {
+ "created": {
+ "type": "date"
+ },
+ "createdBy": {
+ "type": "text"
+ },
+ "eventId": {
+ "type": "keyword"
+ },
+ "note": {
+ "type": "text"
+ },
+ "timelineId": {
+ "type": "keyword"
+ },
+ "updated": {
+ "type": "date"
+ },
+ "updatedBy": {
+ "type": "text"
+ }
+ }
+ },
+ "siem-ui-timeline-pinned-event": {
+ "properties": {
+ "created": {
+ "type": "date"
+ },
+ "createdBy": {
+ "type": "text"
+ },
+ "eventId": {
+ "type": "keyword"
+ },
+ "timelineId": {
+ "type": "keyword"
+ },
+ "updated": {
+ "type": "date"
+ },
+ "updatedBy": {
+ "type": "text"
+ }
+ }
+ },
+ "space": {
+ "properties": {
+ "_reserved": {
+ "type": "boolean"
+ },
+ "color": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "disabledFeatures": {
+ "type": "keyword"
+ },
+ "imageUrl": {
+ "index": false,
+ "type": "text"
+ },
+ "initials": {
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 2048,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "spaces-usage-stats": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "tag": {
+ "properties": {
+ "color": {
+ "type": "text"
+ },
+ "description": {
+ "type": "text"
+ },
+ "name": {
+ "type": "text"
+ }
+ }
+ },
+ "telemetry": {
+ "properties": {
+ "allowChangingOptInStatus": {
+ "type": "boolean"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "lastReported": {
+ "type": "date"
+ },
+ "lastVersionChecked": {
+ "type": "keyword"
+ },
+ "reportFailureCount": {
+ "type": "integer"
+ },
+ "reportFailureVersion": {
+ "type": "keyword"
+ },
+ "sendUsageFrom": {
+ "type": "keyword"
+ },
+ "userHasSeenNotice": {
+ "type": "boolean"
+ }
+ }
+ },
+ "timelion-sheet": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "timelion_chart_height": {
+ "type": "integer"
+ },
+ "timelion_columns": {
+ "type": "integer"
+ },
+ "timelion_interval": {
+ "type": "keyword"
+ },
+ "timelion_other_interval": {
+ "type": "keyword"
+ },
+ "timelion_rows": {
+ "type": "integer"
+ },
+ "timelion_sheet": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "ui-counter": {
+ "properties": {
+ "count": {
+ "type": "integer"
+ }
+ }
+ },
+ "ui-metric": {
+ "properties": {
+ "count": {
+ "type": "integer"
+ }
+ }
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "upgrade-assistant-reindex-operation": {
+ "properties": {
+ "errorMessage": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "indexName": {
+ "type": "keyword"
+ },
+ "lastCompletedStep": {
+ "type": "long"
+ },
+ "locked": {
+ "type": "date"
+ },
+ "newIndexName": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "reindexOptions": {
+ "properties": {
+ "openAndClose": {
+ "type": "boolean"
+ },
+ "queueSettings": {
+ "properties": {
+ "queuedAt": {
+ "type": "long"
+ },
+ "startedAt": {
+ "type": "long"
+ }
+ }
+ }
+ }
+ },
+ "reindexTaskId": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 256,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ },
+ "reindexTaskPercComplete": {
+ "type": "float"
+ },
+ "runningReindexCount": {
+ "type": "integer"
+ },
+ "status": {
+ "type": "integer"
+ }
+ }
+ },
+ "upgrade-assistant-telemetry": {
+ "properties": {
+ "features": {
+ "properties": {
+ "deprecation_logging": {
+ "properties": {
+ "enabled": {
+ "null_value": true,
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "ui_open": {
+ "properties": {
+ "cluster": {
+ "null_value": 0,
+ "type": "long"
+ },
+ "indices": {
+ "null_value": 0,
+ "type": "long"
+ },
+ "overview": {
+ "null_value": 0,
+ "type": "long"
+ }
+ }
+ },
+ "ui_reindex": {
+ "properties": {
+ "close": {
+ "null_value": 0,
+ "type": "long"
+ },
+ "open": {
+ "null_value": 0,
+ "type": "long"
+ },
+ "start": {
+ "null_value": 0,
+ "type": "long"
+ },
+ "stop": {
+ "null_value": 0,
+ "type": "long"
+ }
+ }
+ }
+ }
+ },
+ "uptime-dynamic-settings": {
+ "dynamic": "false",
+ "type": "object"
+ },
+ "url": {
+ "properties": {
+ "accessCount": {
+ "type": "long"
+ },
+ "accessDate": {
+ "type": "date"
+ },
+ "createDate": {
+ "type": "date"
+ },
+ "url": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 2048,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "usage-counters": {
+ "dynamic": "false",
+ "properties": {
+ "domainId": {
+ "type": "keyword"
+ }
+ }
+ },
+ "visualization": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "index": false,
+ "type": "text"
+ }
+ }
+ },
+ "savedSearchRefName": {
+ "doc_values": false,
+ "index": false,
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "index": false,
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "visState": {
+ "index": false,
+ "type": "text"
+ }
+ }
+ },
+ "workplace_search_telemetry": {
+ "dynamic": "false",
+ "type": "object"
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "auto_expand_replicas": "0-1",
+ "number_of_replicas": "1",
+ "number_of_shards": "1",
+ "priority": "10",
+ "refresh_interval": "1s"
+ }
+ }
+ }
+}
\ No newline at end of file
From 65ff74ff5a3c90129f3128b0e6dd431f4cdfcb8a Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Thu, 1 Jul 2021 10:48:34 +0200
Subject: [PATCH 27/51] [Lens] Add functional test for example integration
(#103460)
---
.../embedded_lens_example/public/app.tsx | 47 +++++++++++++-
x-pack/test/examples/config.ts | 2 +-
.../embedded_lens/embedded_example.ts | 65 +++++++++++++++++++
x-pack/test/examples/embedded_lens/index.ts | 34 ++++++++++
4 files changed, 145 insertions(+), 3 deletions(-)
create mode 100644 x-pack/test/examples/embedded_lens/embedded_example.ts
create mode 100644 x-pack/test/examples/embedded_lens/index.ts
diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx
index a13ddbbd79ef0..913836a244b8a 100644
--- a/x-pack/examples/embedded_lens_example/public/app.tsx
+++ b/x-pack/examples/embedded_lens_example/public/app.tsx
@@ -17,6 +17,7 @@ import {
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
+ EuiCallOut,
} from '@elastic/eui';
import { IndexPattern } from 'src/plugins/data/public';
import { CoreStart } from 'kibana/public';
@@ -149,6 +150,7 @@ export const App = (props: {
{
// eslint-disable-next-line no-bitwise
@@ -177,12 +179,32 @@ export const App = (props: {
setColor(newColor);
}}
>
- Edit in Lens
+ Edit in Lens (new tab)
+
+
+
+ {
+ props.plugins.lens.navigateToPrefilledEditor(
+ {
+ id: '',
+ timeRange: time,
+ attributes: getLensAttributes(props.defaultIndexPattern!, color),
+ },
+ false
+ );
+ }}
+ >
+ Edit in Lens (same tab)
{
setIsSaveModalVisible(true);
@@ -191,6 +213,21 @@ export const App = (props: {
Save Visualization
+
+ {
+ setTime({
+ from: '2015-09-18T06:31:44.000Z',
+ to: '2015-09-23T18:31:44.000Z',
+ });
+ }}
+ >
+ Change time range
+
+
) : (
- This demo only works if your default index pattern is set and time based
+
+ This demo only works if your default index pattern is set and time based
+
)}
diff --git a/x-pack/test/examples/config.ts b/x-pack/test/examples/config.ts
index 491c23a33a3ef..606f97f9c3de7 100644
--- a/x-pack/test/examples/config.ts
+++ b/x-pack/test/examples/config.ts
@@ -33,7 +33,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
reportName: 'X-Pack Example plugin functional tests',
},
- testFiles: [require.resolve('./search_examples')],
+ testFiles: [require.resolve('./search_examples'), require.resolve('./embedded_lens')],
kbnTestServer: {
...xpackFunctionalConfig.get('kbnTestServer'),
diff --git a/x-pack/test/examples/embedded_lens/embedded_example.ts b/x-pack/test/examples/embedded_lens/embedded_example.ts
new file mode 100644
index 0000000000000..3a0891079f24e
--- /dev/null
+++ b/x-pack/test/examples/embedded_lens/embedded_example.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../functional/ftr_provider_context';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const PageObjects = getPageObjects(['lens', 'common', 'dashboard', 'timeToVisualize']);
+ const elasticChart = getService('elasticChart');
+ const testSubjects = getService('testSubjects');
+ const retry = getService('retry');
+
+ async function checkData() {
+ const data = await elasticChart.getChartDebugData();
+ expect(data!.bars![0].bars.length).to.eql(24);
+ }
+
+ describe('show and save', () => {
+ beforeEach(async () => {
+ await PageObjects.common.navigateToApp('embedded_lens_example');
+ await elasticChart.setNewChartUiDebugFlag(true);
+ await testSubjects.click('lns-example-change-time-range');
+ await PageObjects.lens.waitForVisualization();
+ });
+
+ it('should show chart', async () => {
+ await testSubjects.click('lns-example-change-color');
+ await PageObjects.lens.waitForVisualization();
+ await checkData();
+ });
+
+ it('should save to dashboard', async () => {
+ await testSubjects.click('lns-example-save');
+ await PageObjects.timeToVisualize.setSaveModalValues('From example', {
+ saveAsNew: true,
+ redirectToOrigin: false,
+ addToDashboard: 'new',
+ dashboardId: undefined,
+ saveToLibrary: false,
+ });
+
+ await testSubjects.click('confirmSaveSavedObjectButton');
+ await retry.waitForWithTimeout('Save modal to disappear', 1000, () =>
+ testSubjects
+ .missingOrFail('confirmSaveSavedObjectButton')
+ .then(() => true)
+ .catch(() => false)
+ );
+ await PageObjects.lens.goToTimeRange();
+ await PageObjects.dashboard.waitForRenderComplete();
+ await checkData();
+ });
+
+ it('should load Lens editor', async () => {
+ await testSubjects.click('lns-example-open-editor');
+ await PageObjects.lens.waitForVisualization();
+ await checkData();
+ });
+ });
+}
diff --git a/x-pack/test/examples/embedded_lens/index.ts b/x-pack/test/examples/embedded_lens/index.ts
new file mode 100644
index 0000000000000..3bd4ea31cc89b
--- /dev/null
+++ b/x-pack/test/examples/embedded_lens/index.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { PluginFunctionalProviderContext } from 'test/plugin_functional/services';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getService, loadTestFile }: PluginFunctionalProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
+
+ describe('embedded Lens examples', function () {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional');
+ await esArchiver.load('x-pack/test/functional/es_archives/lens/basic'); // need at least one index pattern
+ await kibanaServer.uiSettings.update({
+ defaultIndex: 'logstash-*',
+ });
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/lens/basic');
+ });
+
+ describe('', function () {
+ this.tags(['ciGroup4', 'skipFirefox']);
+
+ loadTestFile(require.resolve('./embedded_example'));
+ });
+ });
+}
From 6e3df60abaf1bcf1cc0ea902dde3913820fc32c8 Mon Sep 17 00:00:00 2001
From: Marta Bondyra
Date: Thu, 1 Jul 2021 11:00:56 +0200
Subject: [PATCH 28/51] [Lens] Move editorFrame state to redux (#100858)
Co-authored-by: Joe Reuter
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: dej611
---
.../embedded_lens_example/public/app.tsx | 1 -
.../lens/public/app_plugin/app.test.tsx | 418 ++++------
x-pack/plugins/lens/public/app_plugin/app.tsx | 256 +++---
.../lens/public/app_plugin/lens_top_nav.tsx | 61 +-
.../lens/public/app_plugin/mounter.test.tsx | 188 ++++-
.../lens/public/app_plugin/mounter.tsx | 236 +++++-
.../lens/public/app_plugin/save_modal.tsx | 27 +-
.../app_plugin/save_modal_container.tsx | 60 +-
.../plugins/lens/public/app_plugin/types.ts | 7 +-
.../components/dimension_editor.test.tsx | 2 +-
.../visualization.test.tsx | 8 +-
.../datatable_visualization/visualization.tsx | 4 +-
.../config_panel/config_panel.test.tsx | 165 ++--
.../config_panel/config_panel.tsx | 181 ++--
.../config_panel/layer_actions.test.ts | 2 +
.../config_panel/layer_actions.ts | 11 +-
.../config_panel/layer_panel.test.tsx | 2 +-
.../editor_frame/config_panel/types.ts | 3 -
.../editor_frame/data_panel_wrapper.tsx | 89 +-
.../editor_frame/editor_frame.test.tsx | 782 ++++--------------
.../editor_frame/editor_frame.tsx | 394 ++-------
.../editor_frame/index.ts | 1 -
.../editor_frame/save.test.ts | 116 ---
.../editor_frame_service/editor_frame/save.ts | 79 --
.../editor_frame/state_helpers.ts | 2 +-
.../editor_frame/state_management.test.ts | 415 ----------
.../editor_frame/state_management.ts | 293 -------
.../editor_frame/suggestion_helpers.test.ts | 2 +-
.../editor_frame/suggestion_helpers.ts | 23 +-
.../editor_frame/suggestion_panel.test.tsx | 187 ++---
.../editor_frame/suggestion_panel.tsx | 25 +-
.../workspace_panel/chart_switch.test.tsx | 498 ++++++-----
.../workspace_panel/chart_switch.tsx | 159 ++--
.../editor_frame/workspace_panel/title.tsx | 27 +
.../workspace_panel/workspace_panel.test.tsx | 96 ++-
.../workspace_panel/workspace_panel.tsx | 69 +-
.../workspace_panel_wrapper.test.tsx | 22 +-
.../workspace_panel_wrapper.tsx | 40 +-
.../public/editor_frame_service/mocks.tsx | 117 +--
.../public/editor_frame_service/service.tsx | 14 +-
.../visualization.test.ts | 10 +-
.../heatmap_visualization/visualization.tsx | 4 +-
.../public/indexpattern_datasource/loader.ts | 22 +-
.../visualization.test.ts | 17 +-
.../metric_visualization/visualization.tsx | 4 +-
x-pack/plugins/lens/public/mocks.tsx | 195 ++++-
.../pie_visualization/visualization.tsx | 4 +-
.../lens/public/state_management/app_slice.ts | 55 --
.../external_context_middleware.ts | 6 +-
.../lens/public/state_management/index.ts | 31 +-
.../state_management/lens_slice.test.ts | 148 ++++
.../public/state_management/lens_slice.ts | 262 ++++++
.../state_management/optimizing_middleware.ts | 22 +
.../time_range_middleware.test.ts | 51 +-
.../state_management/time_range_middleware.ts | 13 +-
.../lens/public/state_management/types.ts | 23 +-
x-pack/plugins/lens/public/types.ts | 18 +-
x-pack/plugins/lens/public/utils.ts | 115 ++-
.../xy_visualization/to_expression.test.ts | 2 +-
.../visual_options_popover.test.tsx | 2 +-
.../xy_visualization/visualization.test.ts | 11 +-
.../public/xy_visualization/visualization.tsx | 4 +-
.../xy_visualization/xy_config_panel.test.tsx | 2 +-
.../shared/exploratory_view/header/header.tsx | 1 -
x-pack/test/accessibility/apps/lens.ts | 4 +-
x-pack/test/functional/apps/lens/dashboard.ts | 5 +-
.../test/functional/page_objects/lens_page.ts | 6 +
67 files changed, 2747 insertions(+), 3372 deletions(-)
delete mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts
delete mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts
delete mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts
delete mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts
create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/title.tsx
delete mode 100644 x-pack/plugins/lens/public/state_management/app_slice.ts
create mode 100644 x-pack/plugins/lens/public/state_management/lens_slice.test.ts
create mode 100644 x-pack/plugins/lens/public/state_management/lens_slice.ts
create mode 100644 x-pack/plugins/lens/public/state_management/optimizing_middleware.ts
diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx
index 913836a244b8a..bf43e200b902d 100644
--- a/x-pack/examples/embedded_lens_example/public/app.tsx
+++ b/x-pack/examples/embedded_lens_example/public/app.tsx
@@ -260,7 +260,6 @@ export const App = (props: {
color
) as unknown) as LensEmbeddableInput
}
- isVisible={isSaveModalVisible}
onSave={() => {}}
onClose={() => setIsSaveModalVisible(false)}
/>
diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
index bced8bf7c04fe..1c49527d9eca8 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
@@ -13,7 +13,13 @@ import { App } from './app';
import { LensAppProps, LensAppServices } from './types';
import { EditorFrameInstance, EditorFrameProps } from '../types';
import { Document } from '../persistence';
-import { makeDefaultServices, mountWithProvider } from '../mocks';
+import {
+ createMockDatasource,
+ createMockVisualization,
+ DatasourceMock,
+ makeDefaultServices,
+ mountWithProvider,
+} from '../mocks';
import { I18nProvider } from '@kbn/i18n/react';
import {
SavedObjectSaveModal,
@@ -25,7 +31,6 @@ import {
FilterManager,
IFieldType,
IIndexPattern,
- IndexPattern,
Query,
} from '../../../../../src/plugins/data/public';
import { TopNavMenuData } from '../../../../../src/plugins/navigation/public';
@@ -60,17 +65,41 @@ jest.mock('lodash', () => {
// const navigationStartMock = navigationPluginMock.createStartContract();
-function createMockFrame(): jest.Mocked {
- return {
- EditorFrameContainer: jest.fn((props: EditorFrameProps) => ),
- };
-}
-
const sessionIdSubject = new Subject();
describe('Lens App', () => {
let defaultDoc: Document;
let defaultSavedObjectId: string;
+ const mockDatasource: DatasourceMock = createMockDatasource('testDatasource');
+ const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2');
+ const datasourceMap = {
+ testDatasource2: mockDatasource2,
+ testDatasource: mockDatasource,
+ };
+
+ const mockVisualization = {
+ ...createMockVisualization(),
+ id: 'testVis',
+ visualizationTypes: [
+ {
+ icon: 'empty',
+ id: 'testVis',
+ label: 'TEST1',
+ groupLabel: 'testVisGroup',
+ },
+ ],
+ };
+ const visualizationMap = {
+ testVis: mockVisualization,
+ };
+
+ function createMockFrame(): jest.Mocked {
+ return {
+ EditorFrameContainer: jest.fn((props: EditorFrameProps) => ),
+ datasourceMap,
+ visualizationMap,
+ };
+ }
const navMenuItems = {
expectedSaveButton: { emphasize: true, testId: 'lnsApp_saveButton' },
@@ -86,17 +115,19 @@ describe('Lens App', () => {
redirectToOrigin: jest.fn(),
onAppLeave: jest.fn(),
setHeaderActionMenu: jest.fn(),
+ datasourceMap,
+ visualizationMap,
};
}
async function mountWith({
props = makeDefaultProps(),
services = makeDefaultServices(sessionIdSubject),
- storePreloadedState,
+ preloadedState,
}: {
props?: jest.Mocked;
services?: jest.Mocked;
- storePreloadedState?: Partial;
+ preloadedState?: Partial;
}) {
const wrappingComponent: React.FC<{
children: React.ReactNode;
@@ -110,9 +141,11 @@ describe('Lens App', () => {
const { instance, lensStore } = await mountWithProvider(
,
- services.data,
- storePreloadedState,
- wrappingComponent
+ {
+ data: services.data,
+ preloadedState,
+ },
+ { wrappingComponent }
);
const frame = props.editorFrame as ReturnType;
@@ -139,8 +172,6 @@ describe('Lens App', () => {
Array [
Array [
Object {
- "initialContext": undefined,
- "onError": [Function],
"showNoDataPopover": [Function],
},
Object {},
@@ -164,7 +195,7 @@ describe('Lens App', () => {
instance.update();
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
query: { query: '', language: 'lucene' },
filters: [pinnedFilter],
resolvedDateRange: {
@@ -177,14 +208,6 @@ describe('Lens App', () => {
expect(services.data.query.filterManager.getFilters).not.toHaveBeenCalled();
});
- it('displays errors from the frame in a toast', async () => {
- const { instance, frame, services } = await mountWith({});
- const onError = frame.EditorFrameContainer.mock.calls[0][0].onError;
- onError({ message: 'error' });
- instance.update();
- expect(services.notifications.toasts.addDanger).toHaveBeenCalled();
- });
-
describe('breadcrumbs', () => {
const breadcrumbDocSavedObjectId = defaultSavedObjectId;
const breadcrumbDoc = ({
@@ -237,7 +260,7 @@ describe('Lens App', () => {
const { instance, lensStore } = await mountWith({
props,
services,
- storePreloadedState: {
+ preloadedState: {
isLinkedToOriginatingApp: true,
},
});
@@ -275,8 +298,8 @@ describe('Lens App', () => {
});
describe('persistence', () => {
- it('loads a document and uses query and filters if initial input is provided', async () => {
- const { instance, lensStore, services } = await mountWith({});
+ it('passes query and indexPatterns to TopNavMenu', async () => {
+ const { instance, lensStore, services } = await mountWith({ preloadedState: {} });
const document = ({
savedObjectId: defaultSavedObjectId,
state: {
@@ -290,8 +313,6 @@ describe('Lens App', () => {
lensStore.dispatch(
setState({
query: ('fake query' as unknown) as Query,
- indexPatternsForTopNav: ([{ id: '1' }] as unknown) as IndexPattern[],
- lastKnownDoc: document,
persistedDoc: document,
})
);
@@ -301,7 +322,7 @@ describe('Lens App', () => {
expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith(
expect.objectContaining({
query: 'fake query',
- indexPatterns: [{ id: '1' }],
+ indexPatterns: [{ id: 'mockip' }],
}),
{}
);
@@ -332,16 +353,11 @@ describe('Lens App', () => {
}
async function save({
- lastKnownDoc = {
- references: [],
- state: {
- filters: [],
- },
- },
+ preloadedState,
initialSavedObjectId,
...saveProps
}: SaveProps & {
- lastKnownDoc?: object;
+ preloadedState?: Partial;
initialSavedObjectId?: string;
}) {
const props = {
@@ -366,18 +382,14 @@ describe('Lens App', () => {
},
} as jest.ResolvedValue);
- const { frame, instance, lensStore } = await mountWith({ services, props });
-
- act(() => {
- lensStore.dispatch(
- setState({
- isSaveable: true,
- lastKnownDoc: { savedObjectId: initialSavedObjectId, ...lastKnownDoc } as Document,
- })
- );
+ const { frame, instance, lensStore } = await mountWith({
+ services,
+ props,
+ preloadedState: {
+ isSaveable: true,
+ ...preloadedState,
+ },
});
-
- instance.update();
expect(getButton(instance).disableButton).toEqual(false);
await act(async () => {
testSave(instance, { ...saveProps });
@@ -399,7 +411,6 @@ describe('Lens App', () => {
act(() => {
lensStore.dispatch(
setState({
- lastKnownDoc: ({ savedObjectId: 'will save this' } as unknown) as Document,
isSaveable: true,
})
);
@@ -415,7 +426,6 @@ describe('Lens App', () => {
lensStore.dispatch(
setState({
isSaveable: true,
- lastKnownDoc: ({ savedObjectId: 'will save this' } as unknown) as Document,
})
);
});
@@ -455,7 +465,7 @@ describe('Lens App', () => {
const { instance } = await mountWith({
props,
services,
- storePreloadedState: {
+ preloadedState: {
isLinkedToOriginatingApp: true,
},
});
@@ -483,7 +493,7 @@ describe('Lens App', () => {
const { instance, services } = await mountWith({
props,
- storePreloadedState: {
+ preloadedState: {
isLinkedToOriginatingApp: true,
},
});
@@ -540,6 +550,7 @@ describe('Lens App', () => {
initialSavedObjectId: defaultSavedObjectId,
newCopyOnSave: true,
newTitle: 'hello there',
+ preloadedState: { persistedDoc: defaultDoc },
});
expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith(
expect.objectContaining({
@@ -559,10 +570,11 @@ describe('Lens App', () => {
});
it('saves existing docs', async () => {
- const { props, services, instance, lensStore } = await save({
+ const { props, services, instance } = await save({
initialSavedObjectId: defaultSavedObjectId,
newCopyOnSave: false,
newTitle: 'hello there',
+ preloadedState: { persistedDoc: defaultDoc },
});
expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith(
expect.objectContaining({
@@ -576,22 +588,6 @@ describe('Lens App', () => {
await act(async () => {
instance.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } });
});
-
- expect(lensStore.dispatch).toHaveBeenCalledWith({
- payload: {
- lastKnownDoc: expect.objectContaining({
- savedObjectId: defaultSavedObjectId,
- title: 'hello there',
- }),
- persistedDoc: expect.objectContaining({
- savedObjectId: defaultSavedObjectId,
- title: 'hello there',
- }),
- isLinkedToOriginatingApp: false,
- },
- type: 'app/setState',
- });
-
expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith(
"Saved 'hello there'"
);
@@ -602,18 +598,13 @@ describe('Lens App', () => {
services.attributeService.wrapAttributes = jest
.fn()
.mockRejectedValue({ message: 'failed' });
- const { instance, props, lensStore } = await mountWith({ services });
- act(() => {
- lensStore.dispatch(
- setState({
- isSaveable: true,
- lastKnownDoc: ({ id: undefined } as unknown) as Document,
- })
- );
+ const { instance, props } = await mountWith({
+ services,
+ preloadedState: {
+ isSaveable: true,
+ },
});
- instance.update();
-
await act(async () => {
testSave(instance, { newCopyOnSave: false, newTitle: 'hello there' });
});
@@ -655,22 +646,19 @@ describe('Lens App', () => {
initialSavedObjectId: defaultSavedObjectId,
newCopyOnSave: false,
newTitle: 'hello there2',
- lastKnownDoc: {
- expression: 'kibana 3',
- state: {
- filters: [pinned, unpinned],
- },
+ preloadedState: {
+ persistedDoc: defaultDoc,
+ filters: [pinned, unpinned],
},
});
expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith(
- {
+ expect.objectContaining({
savedObjectId: defaultSavedObjectId,
title: 'hello there2',
- expression: 'kibana 3',
- state: {
+ state: expect.objectContaining({
filters: [unpinned],
- },
- },
+ }),
+ }),
true,
{ id: '5678', savedObjectId: defaultSavedObjectId }
);
@@ -681,17 +669,13 @@ describe('Lens App', () => {
services.attributeService.wrapAttributes = jest
.fn()
.mockReturnValue(Promise.resolve({ savedObjectId: '123' }));
- const { instance, lensStore } = await mountWith({ services });
- await act(async () => {
- lensStore.dispatch(
- setState({
- isSaveable: true,
- lastKnownDoc: ({ savedObjectId: '123' } as unknown) as Document,
- })
- );
+ const { instance } = await mountWith({
+ services,
+ preloadedState: {
+ isSaveable: true,
+ persistedDoc: ({ savedObjectId: '123' } as unknown) as Document,
+ },
});
-
- instance.update();
await act(async () => {
instance.setProps({ initialInput: { savedObjectId: '123' } });
getButton(instance).run(instance.getDOMNode());
@@ -716,17 +700,7 @@ describe('Lens App', () => {
});
it('does not show the copy button on first save', async () => {
- const { instance, lensStore } = await mountWith({});
- await act(async () => {
- lensStore.dispatch(
- setState({
- isSaveable: true,
- lastKnownDoc: ({} as unknown) as Document,
- })
- );
- });
-
- instance.update();
+ const { instance } = await mountWith({ preloadedState: { isSaveable: true } });
await act(async () => getButton(instance).run(instance.getDOMNode()));
instance.update();
expect(instance.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false);
@@ -744,33 +718,18 @@ describe('Lens App', () => {
}
it('should be disabled when no data is available', async () => {
- const { instance, lensStore } = await mountWith({});
- await act(async () => {
- lensStore.dispatch(
- setState({
- isSaveable: true,
- lastKnownDoc: ({} as unknown) as Document,
- })
- );
- });
- instance.update();
+ const { instance } = await mountWith({ preloadedState: { isSaveable: true } });
expect(getButton(instance).disableButton).toEqual(true);
});
it('should disable download when not saveable', async () => {
- const { instance, lensStore } = await mountWith({});
-
- await act(async () => {
- lensStore.dispatch(
- setState({
- lastKnownDoc: ({} as unknown) as Document,
- isSaveable: false,
- activeData: { layer1: { type: 'datatable', columns: [], rows: [] } },
- })
- );
+ const { instance } = await mountWith({
+ preloadedState: {
+ isSaveable: false,
+ activeData: { layer1: { type: 'datatable', columns: [], rows: [] } },
+ },
});
- instance.update();
expect(getButton(instance).disableButton).toEqual(true);
});
@@ -784,17 +743,13 @@ describe('Lens App', () => {
},
};
- const { instance, lensStore } = await mountWith({ services });
- await act(async () => {
- lensStore.dispatch(
- setState({
- lastKnownDoc: ({} as unknown) as Document,
- isSaveable: true,
- activeData: { layer1: { type: 'datatable', columns: [], rows: [] } },
- })
- );
+ const { instance } = await mountWith({
+ services,
+ preloadedState: {
+ isSaveable: true,
+ activeData: { layer1: { type: 'datatable', columns: [], rows: [] } },
+ },
});
- instance.update();
expect(getButton(instance).disableButton).toEqual(false);
});
});
@@ -812,7 +767,7 @@ describe('Lens App', () => {
);
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
query: { query: '', language: 'lucene' },
resolvedDateRange: {
fromDate: '2021-01-10T04:00:00.000Z',
@@ -822,49 +777,6 @@ describe('Lens App', () => {
});
});
- it('updates the index patterns when the editor frame is changed', async () => {
- const { instance, lensStore, services } = await mountWith({});
- expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith(
- expect.objectContaining({
- indexPatterns: [],
- }),
- {}
- );
- await act(async () => {
- lensStore.dispatch(
- setState({
- indexPatternsForTopNav: [{ id: '1' }] as IndexPattern[],
- lastKnownDoc: ({} as unknown) as Document,
- isSaveable: true,
- })
- );
- });
- instance.update();
- expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith(
- expect.objectContaining({
- indexPatterns: [{ id: '1' }],
- }),
- {}
- );
- // Do it again to verify that the dirty checking is done right
- await act(async () => {
- lensStore.dispatch(
- setState({
- indexPatternsForTopNav: [{ id: '2' }] as IndexPattern[],
- lastKnownDoc: ({} as unknown) as Document,
- isSaveable: true,
- })
- );
- });
- instance.update();
- expect(services.navigation.ui.TopNavMenu).toHaveBeenLastCalledWith(
- expect.objectContaining({
- indexPatterns: [{ id: '2' }],
- }),
- {}
- );
- });
-
it('updates the editor frame when the user changes query or time in the search bar', async () => {
const { instance, services, lensStore } = await mountWith({});
(services.data.query.timefilter.timefilter.calculateBounds as jest.Mock).mockReturnValue({
@@ -892,7 +804,7 @@ describe('Lens App', () => {
});
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
query: { query: 'new', language: 'lucene' },
resolvedDateRange: {
fromDate: '2021-01-09T04:00:00.000Z',
@@ -907,7 +819,7 @@ describe('Lens App', () => {
const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
const field = ({ name: 'myfield' } as unknown) as IFieldType;
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
filters: [],
}),
});
@@ -918,7 +830,7 @@ describe('Lens App', () => {
);
instance.update();
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
filters: [esFilters.buildExistsFilter(field, indexPattern)],
}),
});
@@ -928,7 +840,7 @@ describe('Lens App', () => {
const { instance, services, lensStore } = await mountWith({});
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
searchSessionId: `sessionId-1`,
}),
});
@@ -942,7 +854,7 @@ describe('Lens App', () => {
instance.update();
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
searchSessionId: `sessionId-2`,
}),
});
@@ -955,7 +867,7 @@ describe('Lens App', () => {
);
instance.update();
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
searchSessionId: `sessionId-3`,
}),
});
@@ -968,7 +880,7 @@ describe('Lens App', () => {
);
instance.update();
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
searchSessionId: `sessionId-4`,
}),
});
@@ -1105,7 +1017,7 @@ describe('Lens App', () => {
act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!());
instance.update();
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
filters: [pinned],
}),
});
@@ -1137,7 +1049,7 @@ describe('Lens App', () => {
});
instance.update();
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
searchSessionId: `sessionId-2`,
}),
});
@@ -1162,30 +1074,12 @@ describe('Lens App', () => {
act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!());
instance.update();
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
searchSessionId: `sessionId-4`,
}),
});
});
- const mockUpdate = {
- filterableIndexPatterns: [],
- doc: {
- title: '',
- description: '',
- visualizationType: '',
- state: {
- datasourceStates: {},
- visualization: {},
- filters: [],
- query: { query: '', language: 'lucene' },
- },
- references: [],
- },
- isSaveable: true,
- activeData: undefined,
- };
-
it('updates the state if session id changes from the outside', async () => {
const services = makeDefaultServices(sessionIdSubject);
const { lensStore } = await mountWith({ props: undefined, services });
@@ -1197,25 +1091,16 @@ describe('Lens App', () => {
await new Promise((r) => setTimeout(r, 0));
});
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
searchSessionId: `new-session-id`,
}),
});
});
it('does not update the searchSessionId when the state changes', async () => {
- const { lensStore } = await mountWith({});
- act(() => {
- lensStore.dispatch(
- setState({
- indexPatternsForTopNav: [],
- lastKnownDoc: mockUpdate.doc,
- isSaveable: true,
- })
- );
- });
+ const { lensStore } = await mountWith({ preloadedState: { isSaveable: true } });
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
+ lens: expect.objectContaining({
searchSessionId: `sessionId-1`,
}),
});
@@ -1248,20 +1133,7 @@ describe('Lens App', () => {
visualize: { save: false, saveQuery: false, show: true },
},
};
- const { instance, props, lensStore } = await mountWith({ services });
- act(() => {
- lensStore.dispatch(
- setState({
- indexPatternsForTopNav: [] as IndexPattern[],
- lastKnownDoc: ({
- savedObjectId: undefined,
- references: [],
- } as unknown) as Document,
- isSaveable: true,
- })
- );
- });
- instance.update();
+ const { props } = await mountWith({ services, preloadedState: { isSaveable: true } });
const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0];
lastCall({ default: defaultLeave, confirm: confirmLeave });
expect(defaultLeave).toHaveBeenCalled();
@@ -1269,14 +1141,14 @@ describe('Lens App', () => {
});
it('should confirm when leaving with an unsaved doc', async () => {
- const { lensStore, props } = await mountWith({});
- act(() => {
- lensStore.dispatch(
- setState({
- lastKnownDoc: ({ savedObjectId: undefined, state: {} } as unknown) as Document,
- isSaveable: true,
- })
- );
+ const { props } = await mountWith({
+ preloadedState: {
+ visualization: {
+ activeId: 'testVis',
+ state: {},
+ },
+ isSaveable: true,
+ },
});
const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0];
lastCall({ default: defaultLeave, confirm: confirmLeave });
@@ -1285,18 +1157,15 @@ describe('Lens App', () => {
});
it('should confirm when leaving with unsaved changes to an existing doc', async () => {
- const { lensStore, props } = await mountWith({});
- act(() => {
- lensStore.dispatch(
- setState({
- persistedDoc: defaultDoc,
- lastKnownDoc: ({
- savedObjectId: defaultSavedObjectId,
- references: [],
- } as unknown) as Document,
- isSaveable: true,
- })
- );
+ const { props } = await mountWith({
+ preloadedState: {
+ persistedDoc: defaultDoc,
+ visualization: {
+ activeId: 'testVis',
+ state: {},
+ },
+ isSaveable: true,
+ },
});
const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0];
lastCall({ default: defaultLeave, confirm: confirmLeave });
@@ -1305,15 +1174,23 @@ describe('Lens App', () => {
});
it('should not confirm when changes are saved', async () => {
- const { lensStore, props } = await mountWith({});
- act(() => {
- lensStore.dispatch(
- setState({
- lastKnownDoc: defaultDoc,
- persistedDoc: defaultDoc,
- isSaveable: true,
- })
- );
+ const { props } = await mountWith({
+ preloadedState: {
+ persistedDoc: {
+ ...defaultDoc,
+ state: {
+ ...defaultDoc.state,
+ datasourceStates: { testDatasource: '' },
+ visualization: {},
+ },
+ },
+ isSaveable: true,
+ ...(defaultDoc.state as Partial),
+ visualization: {
+ activeId: 'testVis',
+ state: {},
+ },
+ },
});
const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0];
lastCall({ default: defaultLeave, confirm: confirmLeave });
@@ -1321,16 +1198,13 @@ describe('Lens App', () => {
expect(confirmLeave).not.toHaveBeenCalled();
});
+ // not sure how to test it
it('should confirm when the latest doc is invalid', async () => {
const { lensStore, props } = await mountWith({});
act(() => {
lensStore.dispatch(
setState({
persistedDoc: defaultDoc,
- lastKnownDoc: ({
- savedObjectId: defaultSavedObjectId,
- references: [],
- } as unknown) as Document,
isSaveable: true,
})
);
diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx
index fee64a532553d..8faee830d52bb 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.tsx
@@ -10,8 +10,6 @@ import './app.scss';
import { isEqual } from 'lodash';
import React, { useState, useEffect, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
-import { Toast } from 'kibana/public';
-import { VisualizeFieldContext } from 'src/plugins/ui_actions/public';
import { EuiBreadcrumb } from '@elastic/eui';
import {
createKbnUrlStateStorage,
@@ -24,8 +22,9 @@ import { LensAppProps, LensAppServices } from './types';
import { LensTopNavMenu } from './lens_top_nav';
import { LensByReferenceInput } from '../embeddable';
import { EditorFrameInstance } from '../types';
+import { Document } from '../persistence/saved_object_store';
import {
- setState as setAppState,
+ setState,
useLensSelector,
useLensDispatch,
LensAppState,
@@ -36,6 +35,7 @@ import {
getLastKnownDocWithoutPinnedFilters,
runSaveLensVisualization,
} from './save_modal_container';
+import { getSavedObjectFormat } from '../utils';
export type SaveProps = Omit & {
returnToOrigin: boolean;
@@ -54,7 +54,8 @@ export function App({
incomingState,
redirectToOrigin,
setHeaderActionMenu,
- initialContext,
+ datasourceMap,
+ visualizationMap,
}: LensAppProps) {
const lensAppServices = useKibana().services;
@@ -73,16 +74,69 @@ export function App({
const dispatch = useLensDispatch();
const dispatchSetState: DispatchSetState = useCallback(
- (state: Partial) => dispatch(setAppState(state)),
+ (state: Partial) => dispatch(setState(state)),
[dispatch]
);
- const appState = useLensSelector((state) => state.app);
+ const {
+ datasourceStates,
+ visualization,
+ filters,
+ query,
+ activeDatasourceId,
+ persistedDoc,
+ isLinkedToOriginatingApp,
+ searchSessionId,
+ isLoading,
+ isSaveable,
+ } = useLensSelector((state) => state.lens);
// Used to show a popover that guides the user towards changing the date range when no data is available.
const [indicateNoData, setIndicateNoData] = useState(false);
const [isSaveModalVisible, setIsSaveModalVisible] = useState(false);
- const { lastKnownDoc } = appState;
+ const [lastKnownDoc, setLastKnownDoc] = useState(undefined);
+
+ useEffect(() => {
+ const activeVisualization = visualization.activeId && visualizationMap[visualization.activeId];
+ const activeDatasource =
+ activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading
+ ? datasourceMap[activeDatasourceId]
+ : undefined;
+
+ if (!activeDatasource || !activeVisualization || !visualization.state) {
+ return;
+ }
+ setLastKnownDoc(
+ // todo: that should be redux store selector
+ getSavedObjectFormat({
+ activeDatasources: Object.keys(datasourceStates).reduce(
+ (acc, datasourceId) => ({
+ ...acc,
+ [datasourceId]: datasourceMap[datasourceId],
+ }),
+ {}
+ ),
+ datasourceStates,
+ visualization,
+ filters,
+ query,
+ title: persistedDoc?.title || '',
+ description: persistedDoc?.description,
+ persistedId: persistedDoc?.savedObjectId,
+ })
+ );
+ }, [
+ persistedDoc?.title,
+ persistedDoc?.description,
+ persistedDoc?.savedObjectId,
+ datasourceStates,
+ visualization,
+ filters,
+ query,
+ activeDatasourceId,
+ datasourceMap,
+ visualizationMap,
+ ]);
const showNoDataPopover = useCallback(() => {
setIndicateNoData(true);
@@ -92,30 +146,17 @@ export function App({
if (indicateNoData) {
setIndicateNoData(false);
}
- }, [
- setIndicateNoData,
- indicateNoData,
- appState.indexPatternsForTopNav,
- appState.searchSessionId,
- ]);
-
- const onError = useCallback(
- (e: { message: string }) =>
- notifications.toasts.addDanger({
- title: e.message,
- }),
- [notifications.toasts]
- );
+ }, [setIndicateNoData, indicateNoData, searchSessionId]);
const getIsByValueMode = useCallback(
() =>
Boolean(
// Temporarily required until the 'by value' paradigm is default.
dashboardFeatureFlag.allowByValueEmbeddables &&
- appState.isLinkedToOriginatingApp &&
+ isLinkedToOriginatingApp &&
!(initialInput as LensByReferenceInput)?.savedObjectId
),
- [dashboardFeatureFlag.allowByValueEmbeddables, appState.isLinkedToOriginatingApp, initialInput]
+ [dashboardFeatureFlag.allowByValueEmbeddables, isLinkedToOriginatingApp, initialInput]
);
useEffect(() => {
@@ -138,13 +179,11 @@ export function App({
onAppLeave((actions) => {
// Confirm when the user has made any changes to an existing doc
// or when the user has configured something without saving
+
if (
application.capabilities.visualize.save &&
- !isEqual(
- appState.persistedDoc?.state,
- getLastKnownDocWithoutPinnedFilters(lastKnownDoc)?.state
- ) &&
- (appState.isSaveable || appState.persistedDoc)
+ !isEqual(persistedDoc?.state, getLastKnownDocWithoutPinnedFilters(lastKnownDoc)?.state) &&
+ (isSaveable || persistedDoc)
) {
return actions.confirm(
i18n.translate('xpack.lens.app.unsavedWorkMessage', {
@@ -158,19 +197,13 @@ export function App({
return actions.default();
}
});
- }, [
- onAppLeave,
- lastKnownDoc,
- appState.isSaveable,
- appState.persistedDoc,
- application.capabilities.visualize.save,
- ]);
+ }, [onAppLeave, lastKnownDoc, isSaveable, persistedDoc, application.capabilities.visualize.save]);
// Sync Kibana breadcrumbs any time the saved document's title changes
useEffect(() => {
const isByValueMode = getIsByValueMode();
const breadcrumbs: EuiBreadcrumb[] = [];
- if (appState.isLinkedToOriginatingApp && getOriginatingAppName() && redirectToOrigin) {
+ if (isLinkedToOriginatingApp && getOriginatingAppName() && redirectToOrigin) {
breadcrumbs.push({
onClick: () => {
redirectToOrigin();
@@ -193,10 +226,10 @@ export function App({
let currentDocTitle = i18n.translate('xpack.lens.breadcrumbsCreate', {
defaultMessage: 'Create',
});
- if (appState.persistedDoc) {
+ if (persistedDoc) {
currentDocTitle = isByValueMode
? i18n.translate('xpack.lens.breadcrumbsByValue', { defaultMessage: 'Edit visualization' })
- : appState.persistedDoc.title;
+ : persistedDoc.title;
}
breadcrumbs.push({ text: currentDocTitle });
chrome.setBreadcrumbs(breadcrumbs);
@@ -207,39 +240,55 @@ export function App({
getIsByValueMode,
application,
chrome,
- appState.isLinkedToOriginatingApp,
- appState.persistedDoc,
+ isLinkedToOriginatingApp,
+ persistedDoc,
]);
- const runSave = (saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
- return runSaveLensVisualization(
- {
- lastKnownDoc,
- getIsByValueMode,
- savedObjectsTagging,
- initialInput,
- redirectToOrigin,
- persistedDoc: appState.persistedDoc,
- onAppLeave,
- redirectTo,
- originatingApp: incomingState?.originatingApp,
- ...lensAppServices,
- },
- saveProps,
- options
- ).then(
- (newState) => {
- if (newState) {
- dispatchSetState(newState);
- setIsSaveModalVisible(false);
+ const runSave = useCallback(
+ (saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
+ return runSaveLensVisualization(
+ {
+ lastKnownDoc,
+ getIsByValueMode,
+ savedObjectsTagging,
+ initialInput,
+ redirectToOrigin,
+ persistedDoc,
+ onAppLeave,
+ redirectTo,
+ originatingApp: incomingState?.originatingApp,
+ ...lensAppServices,
+ },
+ saveProps,
+ options
+ ).then(
+ (newState) => {
+ if (newState) {
+ dispatchSetState(newState);
+ setIsSaveModalVisible(false);
+ }
+ },
+ () => {
+ // error is handled inside the modal
+ // so ignoring it here
}
- },
- () => {
- // error is handled inside the modal
- // so ignoring it here
- }
- );
- };
+ );
+ },
+ [
+ incomingState?.originatingApp,
+ lastKnownDoc,
+ persistedDoc,
+ getIsByValueMode,
+ savedObjectsTagging,
+ initialInput,
+ redirectToOrigin,
+ onAppLeave,
+ redirectTo,
+ lensAppServices,
+ dispatchSetState,
+ setIsSaveModalVisible,
+ ]
+ );
return (
<>
@@ -253,64 +302,53 @@ export function App({
setIsSaveModalVisible={setIsSaveModalVisible}
setHeaderActionMenu={setHeaderActionMenu}
indicateNoData={indicateNoData}
+ datasourceMap={datasourceMap}
+ title={persistedDoc?.title}
/>
- {(!appState.isAppLoading || appState.persistedDoc) && (
+ {(!isLoading || persistedDoc) && (
)}
- {
- setIsSaveModalVisible(false);
- }}
- getAppNameFromId={() => getOriginatingAppName()}
- lastKnownDoc={lastKnownDoc}
- onAppLeave={onAppLeave}
- persistedDoc={appState.persistedDoc}
- initialInput={initialInput}
- redirectTo={redirectTo}
- redirectToOrigin={redirectToOrigin}
- returnToOriginSwitchLabel={
- getIsByValueMode() && initialInput
- ? i18n.translate('xpack.lens.app.updatePanel', {
- defaultMessage: 'Update panel on {originatingAppName}',
- values: { originatingAppName: getOriginatingAppName() },
- })
- : undefined
- }
- />
+ {isSaveModalVisible && (
+ {
+ setIsSaveModalVisible(false);
+ }}
+ getAppNameFromId={() => getOriginatingAppName()}
+ lastKnownDoc={lastKnownDoc}
+ onAppLeave={onAppLeave}
+ persistedDoc={persistedDoc}
+ initialInput={initialInput}
+ redirectTo={redirectTo}
+ redirectToOrigin={redirectToOrigin}
+ returnToOriginSwitchLabel={
+ getIsByValueMode() && initialInput
+ ? i18n.translate('xpack.lens.app.updatePanel', {
+ defaultMessage: 'Update panel on {originatingAppName}',
+ values: { originatingAppName: getOriginatingAppName() },
+ })
+ : undefined
+ }
+ />
+ )}
>
);
}
const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({
editorFrame,
- onError,
showNoDataPopover,
- initialContext,
}: {
editorFrame: EditorFrameInstance;
- onError: (e: { message: string }) => Toast;
showNoDataPopover: () => void;
- initialContext: VisualizeFieldContext | undefined;
}) {
const { EditorFrameContainer } = editorFrame;
- return (
-
- );
+ return ;
});
diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx
index ecaae04232f8a..5034069b448af 100644
--- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx
@@ -7,21 +7,21 @@
import { isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
-import React, { useCallback, useMemo } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { TopNavMenuData } from '../../../../../src/plugins/navigation/public';
import { LensAppServices, LensTopNavActions, LensTopNavMenuProps } from './types';
import { downloadMultipleAs } from '../../../../../src/plugins/share/public';
import { trackUiEvent } from '../lens_ui_telemetry';
-import { exporters } from '../../../../../src/plugins/data/public';
-
+import { exporters, IndexPattern } from '../../../../../src/plugins/data/public';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import {
- setState as setAppState,
+ setState,
useLensSelector,
useLensDispatch,
LensAppState,
DispatchSetState,
} from '../state_management';
+import { getIndexPatternsObjects, getIndexPatternsIds } from '../utils';
function getLensTopNavConfig(options: {
showSaveAndReturn: boolean;
@@ -127,6 +127,8 @@ export const LensTopNavMenu = ({
runSave,
onAppLeave,
redirectToOrigin,
+ datasourceMap,
+ title,
}: LensTopNavMenuProps) => {
const {
data,
@@ -139,19 +141,52 @@ export const LensTopNavMenu = ({
const dispatch = useLensDispatch();
const dispatchSetState: DispatchSetState = React.useCallback(
- (state: Partial) => dispatch(setAppState(state)),
+ (state: Partial) => dispatch(setState(state)),
[dispatch]
);
+ const [indexPatterns, setIndexPatterns] = useState([]);
+
const {
isSaveable,
isLinkedToOriginatingApp,
- indexPatternsForTopNav,
query,
- lastKnownDoc,
activeData,
savedQuery,
- } = useLensSelector((state) => state.app);
+ activeDatasourceId,
+ datasourceStates,
+ } = useLensSelector((state) => state.lens);
+
+ useEffect(() => {
+ const activeDatasource =
+ datasourceMap && activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading
+ ? datasourceMap[activeDatasourceId]
+ : undefined;
+ if (!activeDatasource) {
+ return;
+ }
+ const indexPatternIds = getIndexPatternsIds({
+ activeDatasources: Object.keys(datasourceStates).reduce(
+ (acc, datasourceId) => ({
+ ...acc,
+ [datasourceId]: datasourceMap[datasourceId],
+ }),
+ {}
+ ),
+ datasourceStates,
+ });
+ const hasIndexPatternsChanged =
+ indexPatterns.length !== indexPatternIds.length ||
+ indexPatternIds.some((id) => !indexPatterns.find((indexPattern) => indexPattern.id === id));
+ // Update the cached index patterns if the user made a change to any of them
+ if (hasIndexPatternsChanged) {
+ getIndexPatternsObjects(indexPatternIds, data.indexPatterns).then(
+ ({ indexPatterns: indexPatternObjects }) => {
+ setIndexPatterns(indexPatternObjects);
+ }
+ );
+ }
+ }, [datasourceStates, activeDatasourceId, data.indexPatterns, datasourceMap, indexPatterns]);
const { TopNavMenu } = navigation.ui;
const { from, to } = data.query.timefilter.timefilter.getTime();
@@ -190,7 +225,7 @@ export const LensTopNavMenu = ({
if (datatable) {
const postFix = datatables.length > 1 ? `-${i + 1}` : '';
- memo[`${lastKnownDoc?.title || unsavedTitle}${postFix}.csv`] = {
+ memo[`${title || unsavedTitle}${postFix}.csv`] = {
content: exporters.datatableToCSV(datatable, {
csvSeparator: uiSettings.get('csv:separator', ','),
quoteValues: uiSettings.get('csv:quoteValues', true),
@@ -208,14 +243,14 @@ export const LensTopNavMenu = ({
}
},
saveAndReturn: () => {
- if (savingToDashboardPermitted && lastKnownDoc) {
+ if (savingToDashboardPermitted) {
// disabling the validation on app leave because the document has been saved.
onAppLeave((actions) => {
return actions.default();
});
runSave(
{
- newTitle: lastKnownDoc.title,
+ newTitle: title || '',
newCopyOnSave: false,
isTitleDuplicateConfirmed: false,
returnToOrigin: true,
@@ -248,7 +283,7 @@ export const LensTopNavMenu = ({
initialInput,
isLinkedToOriginatingApp,
isSaveable,
- lastKnownDoc,
+ title,
onAppLeave,
redirectToOrigin,
runSave,
@@ -321,7 +356,7 @@ export const LensTopNavMenu = ({
onSaved={onSavedWrapped}
onSavedQueryUpdated={onSavedQueryUpdatedWrapped}
onClearSavedQuery={onClearSavedQueryWrapped}
- indexPatterns={indexPatternsForTopNav}
+ indexPatterns={indexPatterns}
query={query}
dateRangeFrom={from}
dateRangeTo={to}
diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx
index 4f890a51f9b6a..03eec4f617cfc 100644
--- a/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx
@@ -4,45 +4,150 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { makeDefaultServices, mockLensStore } from '../mocks';
+import { makeDefaultServices, makeLensStore, defaultDoc, createMockVisualization } from '../mocks';
+import { createMockDatasource, DatasourceMock } from '../mocks';
import { act } from 'react-dom/test-utils';
-import { loadDocument } from './mounter';
+import { loadInitialStore } from './mounter';
import { LensEmbeddableInput } from '../embeddable/embeddable';
const defaultSavedObjectId = '1234';
+const preloadedState = {
+ isLoading: true,
+ visualization: {
+ state: null,
+ activeId: 'testVis',
+ },
+};
describe('Mounter', () => {
const byValueFlag = { allowByValueEmbeddables: true };
- describe('loadDocument', () => {
+ const mockDatasource: DatasourceMock = createMockDatasource('testDatasource');
+ const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2');
+ const datasourceMap = {
+ testDatasource2: mockDatasource2,
+ testDatasource: mockDatasource,
+ };
+ const mockVisualization = {
+ ...createMockVisualization(),
+ id: 'testVis',
+ visualizationTypes: [
+ {
+ icon: 'empty',
+ id: 'testVis',
+ label: 'TEST1',
+ groupLabel: 'testVisGroup',
+ },
+ ],
+ };
+ const mockVisualization2 = {
+ ...createMockVisualization(),
+ id: 'testVis2',
+ visualizationTypes: [
+ {
+ icon: 'empty',
+ id: 'testVis2',
+ label: 'TEST2',
+ groupLabel: 'testVis2Group',
+ },
+ ],
+ };
+ const visualizationMap = {
+ testVis: mockVisualization,
+ testVis2: mockVisualization2,
+ };
+
+ it('should initialize initial datasource', async () => {
+ const services = makeDefaultServices();
+ const redirectCallback = jest.fn();
+ services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc);
+
+ const lensStore = await makeLensStore({
+ data: services.data,
+ preloadedState,
+ });
+ await act(async () => {
+ await loadInitialStore(
+ redirectCallback,
+ undefined,
+ services,
+ lensStore,
+ undefined,
+ byValueFlag,
+ datasourceMap,
+ visualizationMap
+ );
+ });
+ expect(mockDatasource.initialize).toHaveBeenCalled();
+ });
+
+ it('should have initialized only the initial datasource and visualization', async () => {
+ const services = makeDefaultServices();
+ const redirectCallback = jest.fn();
+ services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc);
+
+ const lensStore = await makeLensStore({ data: services.data, preloadedState });
+ await act(async () => {
+ await loadInitialStore(
+ redirectCallback,
+ undefined,
+ services,
+ lensStore,
+ undefined,
+ byValueFlag,
+ datasourceMap,
+ visualizationMap
+ );
+ });
+ expect(mockDatasource.initialize).toHaveBeenCalled();
+ expect(mockDatasource2.initialize).not.toHaveBeenCalled();
+
+ expect(mockVisualization.initialize).toHaveBeenCalled();
+ expect(mockVisualization2.initialize).not.toHaveBeenCalled();
+ });
+
+ // it('should initialize all datasources with state from doc', async () => {})
+ // it('should pass the datasource api for each layer to the visualization', async () => {})
+ // it('should create a separate datasource public api for each layer', async () => {})
+ // it('should not initialize visualization before datasource is initialized', async () => {})
+ // it('should pass the public frame api into visualization initialize', async () => {})
+ // it('should fetch suggestions of currently active datasource when initializes from visualization trigger', async () => {})
+ // it.skip('should pass the datasource api for each layer to the visualization', async () => {})
+ // it('displays errors from the frame in a toast', async () => {
+
+ describe('loadInitialStore', () => {
it('does not load a document if there is no initial input', async () => {
const services = makeDefaultServices();
const redirectCallback = jest.fn();
- const lensStore = mockLensStore({ data: services.data });
- await loadDocument(redirectCallback, undefined, services, lensStore, undefined, byValueFlag);
+ const lensStore = makeLensStore({ data: services.data, preloadedState });
+ await loadInitialStore(
+ redirectCallback,
+ undefined,
+ services,
+ lensStore,
+ undefined,
+ byValueFlag,
+ datasourceMap,
+ visualizationMap
+ );
expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled();
});
it('loads a document and uses query and filters if initial input is provided', async () => {
const services = makeDefaultServices();
const redirectCallback = jest.fn();
- services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({
- savedObjectId: defaultSavedObjectId,
- state: {
- query: 'fake query',
- filters: [{ query: { match_phrase: { src: 'test' } } }],
- },
- references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
- });
+ services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc);
- const lensStore = await mockLensStore({ data: services.data });
+ const lensStore = await makeLensStore({ data: services.data, preloadedState });
await act(async () => {
- await loadDocument(
+ await loadInitialStore(
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
lensStore,
undefined,
- byValueFlag
+ byValueFlag,
+ datasourceMap,
+ visualizationMap
);
});
@@ -50,21 +155,16 @@ describe('Mounter', () => {
savedObjectId: defaultSavedObjectId,
});
- expect(services.data.indexPatterns.get).toHaveBeenCalledWith('1');
-
expect(services.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([
{ query: { match_phrase: { src: 'test' } } },
]);
expect(lensStore.getState()).toEqual({
- app: expect.objectContaining({
- persistedDoc: expect.objectContaining({
- savedObjectId: defaultSavedObjectId,
- state: expect.objectContaining({
- query: 'fake query',
- filters: [{ query: { match_phrase: { src: 'test' } } }],
- }),
- }),
+ lens: expect.objectContaining({
+ persistedDoc: { ...defaultDoc, type: 'lens' },
+ query: 'kuery',
+ isLoading: false,
+ activeDatasourceId: 'testDatasource',
}),
});
});
@@ -72,40 +172,46 @@ describe('Mounter', () => {
it('does not load documents on sequential renders unless the id changes', async () => {
const redirectCallback = jest.fn();
const services = makeDefaultServices();
- const lensStore = mockLensStore({ data: services.data });
+ const lensStore = makeLensStore({ data: services.data, preloadedState });
await act(async () => {
- await loadDocument(
+ await loadInitialStore(
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
lensStore,
undefined,
- byValueFlag
+ byValueFlag,
+ datasourceMap,
+ visualizationMap
);
});
await act(async () => {
- await loadDocument(
+ await loadInitialStore(
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
lensStore,
undefined,
- byValueFlag
+ byValueFlag,
+ datasourceMap,
+ visualizationMap
);
});
expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1);
await act(async () => {
- await loadDocument(
+ await loadInitialStore(
redirectCallback,
{ savedObjectId: '5678' } as LensEmbeddableInput,
services,
lensStore,
undefined,
- byValueFlag
+ byValueFlag,
+ datasourceMap,
+ visualizationMap
);
});
@@ -116,18 +222,20 @@ describe('Mounter', () => {
const services = makeDefaultServices();
const redirectCallback = jest.fn();
- const lensStore = mockLensStore({ data: services.data });
+ const lensStore = makeLensStore({ data: services.data, preloadedState });
services.attributeService.unwrapAttributes = jest.fn().mockRejectedValue('failed to load');
await act(async () => {
- await loadDocument(
+ await loadInitialStore(
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
lensStore,
undefined,
- byValueFlag
+ byValueFlag,
+ datasourceMap,
+ visualizationMap
);
});
expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({
@@ -141,15 +249,17 @@ describe('Mounter', () => {
const redirectCallback = jest.fn();
const services = makeDefaultServices();
- const lensStore = mockLensStore({ data: services.data });
+ const lensStore = makeLensStore({ data: services.data, preloadedState });
await act(async () => {
- await loadDocument(
+ await loadInitialStore(
redirectCallback,
({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput,
services,
lensStore,
undefined,
- byValueFlag
+ byValueFlag,
+ datasourceMap,
+ visualizationMap
);
});
diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx
index 7f27b06c51ba4..1fd12460ba3b6 100644
--- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx
@@ -23,7 +23,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry';
import { App } from './app';
-import { EditorFrameStart } from '../types';
+import { Datasource, EditorFrameStart, Visualization } from '../types';
import { addHelpMenuToAppChrome } from '../help_menu_util';
import { LensPluginStartDependencies } from '../plugin';
import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common';
@@ -32,7 +32,10 @@ import {
LensByReferenceInput,
LensByValueInput,
} from '../embeddable/embeddable';
-import { ACTION_VISUALIZE_LENS_FIELD } from '../../../../../src/plugins/ui_actions/public';
+import {
+ ACTION_VISUALIZE_LENS_FIELD,
+ VisualizeFieldContext,
+} from '../../../../../src/plugins/ui_actions/public';
import { LensAttributeService } from '../lens_attribute_service';
import { LensAppServices, RedirectToOriginProps, HistoryLocationState } from './types';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
@@ -43,9 +46,18 @@ import {
getPreloadedState,
LensRootStore,
setState,
+ LensAppState,
+ updateLayer,
+ updateVisualizationState,
} from '../state_management';
-import { getResolvedDateRange } from '../utils';
-import { getLastKnownDoc } from './save_modal_container';
+import { getPersistedDoc } from './save_modal_container';
+import { getResolvedDateRange, getInitialDatasourceId } from '../utils';
+import { initializeDatasources } from '../editor_frame_service/editor_frame';
+import { generateId } from '../id_generator';
+import {
+ getVisualizeFieldSuggestions,
+ switchToSuggestion,
+} from '../editor_frame_service/editor_frame/suggestion_helpers';
export async function getLensServices(
coreStart: CoreStart,
@@ -166,7 +178,19 @@ export async function mountApp(
if (!initialContext) {
data.query.filterManager.setAppFilters([]);
}
+ const { datasourceMap, visualizationMap } = instance;
+
+ const initialDatasourceId = getInitialDatasourceId(datasourceMap);
+ const datasourceStates: LensAppState['datasourceStates'] = {};
+ if (initialDatasourceId) {
+ datasourceStates[initialDatasourceId] = {
+ state: null,
+ isLoading: true,
+ };
+ }
+
const preloadedState = getPreloadedState({
+ isLoading: true,
query: data.query.queryString.getQuery(),
// Do not use app-specific filters from previous app,
// only if Lens was opened with the intention to visualize a field (e.g. coming from Discover)
@@ -176,10 +200,15 @@ export async function mountApp(
searchSessionId: data.search.session.getSessionId(),
resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
isLinkedToOriginatingApp: Boolean(embeddableEditorIncomingState?.originatingApp),
+ activeDatasourceId: initialDatasourceId,
+ datasourceStates,
+ visualization: {
+ state: null,
+ activeId: Object.keys(visualizationMap)[0] || null,
+ },
});
const lensStore: LensRootStore = makeConfigureStore(preloadedState, { data });
-
const EditorRenderer = React.memo(
(props: { id?: string; history: History; editByValue?: boolean }) => {
const redirectCallback = useCallback(
@@ -190,14 +219,18 @@ export async function mountApp(
);
trackUiEvent('loaded');
const initialInput = getInitialInput(props.id, props.editByValue);
- loadDocument(
+ loadInitialStore(
redirectCallback,
initialInput,
lensServices,
lensStore,
embeddableEditorIncomingState,
- dashboardFeatureFlag
+ dashboardFeatureFlag,
+ datasourceMap,
+ visualizationMap,
+ initialContext
);
+
return (
);
@@ -270,64 +304,180 @@ export async function mountApp(
};
}
-export function loadDocument(
+export function loadInitialStore(
redirectCallback: (savedObjectId?: string) => void,
initialInput: LensEmbeddableInput | undefined,
lensServices: LensAppServices,
lensStore: LensRootStore,
embeddableEditorIncomingState: EmbeddableEditorState | undefined,
- dashboardFeatureFlag: DashboardFeatureFlagConfig
+ dashboardFeatureFlag: DashboardFeatureFlagConfig,
+ datasourceMap: Record,
+ visualizationMap: Record,
+ initialContext?: VisualizeFieldContext
) {
const { attributeService, chrome, notifications, data } = lensServices;
- const { persistedDoc } = lensStore.getState().app;
+ const { persistedDoc } = lensStore.getState().lens;
if (
!initialInput ||
(attributeService.inputIsRefType(initialInput) &&
initialInput.savedObjectId === persistedDoc?.savedObjectId)
) {
- return;
+ return initializeDatasources(
+ datasourceMap,
+ lensStore.getState().lens.datasourceStates,
+ undefined,
+ initialContext,
+ {
+ isFullEditor: true,
+ }
+ )
+ .then((result) => {
+ const datasourceStates = Object.entries(result).reduce(
+ (state, [datasourceId, datasourceState]) => ({
+ ...state,
+ [datasourceId]: {
+ ...datasourceState,
+ isLoading: false,
+ },
+ }),
+ {}
+ );
+ lensStore.dispatch(
+ setState({
+ datasourceStates,
+ isLoading: false,
+ })
+ );
+ if (initialContext) {
+ const selectedSuggestion = getVisualizeFieldSuggestions({
+ datasourceMap,
+ datasourceStates,
+ visualizationMap,
+ activeVisualizationId: Object.keys(visualizationMap)[0] || null,
+ visualizationState: null,
+ visualizeTriggerFieldContext: initialContext,
+ });
+ if (selectedSuggestion) {
+ switchToSuggestion(lensStore.dispatch, selectedSuggestion, 'SWITCH_VISUALIZATION');
+ }
+ }
+ const activeDatasourceId = getInitialDatasourceId(datasourceMap);
+ const visualization = lensStore.getState().lens.visualization;
+ const activeVisualization =
+ visualization.activeId && visualizationMap[visualization.activeId];
+
+ if (visualization.state === null && activeVisualization) {
+ const newLayerId = generateId();
+
+ const initialVisualizationState = activeVisualization.initialize(() => newLayerId);
+ lensStore.dispatch(
+ updateLayer({
+ datasourceId: activeDatasourceId!,
+ layerId: newLayerId,
+ updater: datasourceMap[activeDatasourceId!].insertLayer,
+ })
+ );
+ lensStore.dispatch(
+ updateVisualizationState({
+ visualizationId: activeVisualization.id,
+ updater: initialVisualizationState,
+ })
+ );
+ }
+ })
+ .catch((e: { message: string }) => {
+ notifications.toasts.addDanger({
+ title: e.message,
+ });
+ redirectCallback();
+ });
}
- lensStore.dispatch(setState({ isAppLoading: true }));
- getLastKnownDoc({
+ getPersistedDoc({
initialInput,
attributeService,
data,
chrome,
notifications,
- }).then(
- (newState) => {
- if (newState) {
- const { doc, indexPatterns } = newState;
- const currentSessionId = data.search.session.getSessionId();
+ })
+ .then(
+ (doc) => {
+ if (doc) {
+ const currentSessionId = data.search.session.getSessionId();
+ const docDatasourceStates = Object.entries(doc.state.datasourceStates).reduce(
+ (stateMap, [datasourceId, datasourceState]) => ({
+ ...stateMap,
+ [datasourceId]: {
+ isLoading: true,
+ state: datasourceState,
+ },
+ }),
+ {}
+ );
+
+ initializeDatasources(
+ datasourceMap,
+ docDatasourceStates,
+ doc.references,
+ initialContext,
+ {
+ isFullEditor: true,
+ }
+ )
+ .then((result) => {
+ const activeDatasourceId = getInitialDatasourceId(datasourceMap, doc);
+
+ lensStore.dispatch(
+ setState({
+ query: doc.state.query,
+ searchSessionId:
+ dashboardFeatureFlag.allowByValueEmbeddables &&
+ Boolean(embeddableEditorIncomingState?.originatingApp) &&
+ !(initialInput as LensByReferenceInput)?.savedObjectId &&
+ currentSessionId
+ ? currentSessionId
+ : data.search.session.start(),
+ ...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
+ activeDatasourceId,
+ visualization: {
+ activeId: doc.visualizationType,
+ state: doc.state.visualization,
+ },
+ datasourceStates: Object.entries(result).reduce(
+ (state, [datasourceId, datasourceState]) => ({
+ ...state,
+ [datasourceId]: {
+ ...datasourceState,
+ isLoading: false,
+ },
+ }),
+ {}
+ ),
+ isLoading: false,
+ })
+ );
+ })
+ .catch((e: { message: string }) =>
+ notifications.toasts.addDanger({
+ title: e.message,
+ })
+ );
+ } else {
+ redirectCallback();
+ }
+ },
+ () => {
lensStore.dispatch(
setState({
- query: doc.state.query,
- isAppLoading: false,
- indexPatternsForTopNav: indexPatterns,
- lastKnownDoc: doc,
- searchSessionId:
- dashboardFeatureFlag.allowByValueEmbeddables &&
- Boolean(embeddableEditorIncomingState?.originatingApp) &&
- !(initialInput as LensByReferenceInput)?.savedObjectId &&
- currentSessionId
- ? currentSessionId
- : data.search.session.start(),
- ...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
+ isLoading: false,
})
);
- } else {
redirectCallback();
}
- },
- () => {
- lensStore.dispatch(
- setState({
- isAppLoading: false,
- })
- );
-
- redirectCallback();
- }
- );
+ )
+ .catch((e: { message: string }) =>
+ notifications.toasts.addDanger({
+ title: e.message,
+ })
+ );
}
diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal.tsx
index cb4c5325aefbb..124702e0dd90e 100644
--- a/x-pack/plugins/lens/public/app_plugin/save_modal.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/save_modal.tsx
@@ -7,8 +7,6 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
-
-import { Document } from '../persistence';
import type { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public';
import {
@@ -23,7 +21,6 @@ import {
export type SaveProps = OriginSaveProps | DashboardSaveProps;
export interface Props {
- isVisible: boolean;
savingToLibraryPermitted?: boolean;
originatingApp?: string;
@@ -32,7 +29,9 @@ export interface Props {
savedObjectsTagging?: SavedObjectTaggingPluginStart;
tagsIds: string[];
- lastKnownDoc?: Document;
+ title?: string;
+ savedObjectId?: string;
+ description?: string;
getAppNameFromId: () => string | undefined;
returnToOriginSwitchLabel?: string;
@@ -42,16 +41,14 @@ export interface Props {
}
export const SaveModal = (props: Props) => {
- if (!props.isVisible || !props.lastKnownDoc) {
- return null;
- }
-
const {
originatingApp,
savingToLibraryPermitted,
savedObjectsTagging,
tagsIds,
- lastKnownDoc,
+ savedObjectId,
+ title,
+ description,
allowByValueEmbeddables,
returnToOriginSwitchLabel,
getAppNameFromId,
@@ -70,9 +67,9 @@ export const SaveModal = (props: Props) => {
onSave={(saveProps) => onSave(saveProps, { saveToLibrary: true })}
getAppNameFromId={getAppNameFromId}
documentInfo={{
- id: lastKnownDoc.savedObjectId,
- title: lastKnownDoc.title || '',
- description: lastKnownDoc.description || '',
+ id: savedObjectId,
+ title: title || '',
+ description: description || '',
}}
returnToOriginSwitchLabel={returnToOriginSwitchLabel}
objectType={i18n.translate('xpack.lens.app.saveModalType', {
@@ -95,9 +92,9 @@ export const SaveModal = (props: Props) => {
onClose={onClose}
documentInfo={{
// if the user cannot save to the library - treat this as a new document.
- id: savingToLibraryPermitted ? lastKnownDoc.savedObjectId : undefined,
- title: lastKnownDoc.title || '',
- description: lastKnownDoc.description || '',
+ id: savingToLibraryPermitted ? savedObjectId : undefined,
+ title: title || '',
+ description: description || '',
}}
objectType={i18n.translate('xpack.lens.app.saveModalType', {
defaultMessage: 'Lens visualization',
diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx
index facf85d45bcbb..2912daccf8899 100644
--- a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx
@@ -8,21 +8,16 @@
import React, { useEffect, useState } from 'react';
import { ChromeStart, NotificationsStart } from 'kibana/public';
import { i18n } from '@kbn/i18n';
-import { partition, uniq } from 'lodash';
import { METRIC_TYPE } from '@kbn/analytics';
+import { partition } from 'lodash';
import { SaveModal } from './save_modal';
import { LensAppProps, LensAppServices } from './types';
import type { SaveProps } from './app';
import { Document, injectFilterReferences } from '../persistence';
import { LensByReferenceInput, LensEmbeddableInput } from '../embeddable';
import { LensAttributeService } from '../lens_attribute_service';
-import {
- DataPublicPluginStart,
- esFilters,
- IndexPattern,
-} from '../../../../../src/plugins/data/public';
+import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public';
import { APP_ID, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../common';
-import { getAllIndexPatterns } from '../utils';
import { trackUiEvent } from '../lens_ui_telemetry';
import { checkForDuplicateTitle } from '../../../../../src/plugins/saved_objects/public';
import { LensAppState } from '../state_management';
@@ -31,7 +26,6 @@ type ExtraProps = Pick &
Partial>;
export type SaveModalContainerProps = {
- isVisible: boolean;
originatingApp?: string;
persistedDoc?: Document;
lastKnownDoc?: Document;
@@ -49,7 +43,6 @@ export function SaveModalContainer({
onClose,
onSave,
runSave,
- isVisible,
persistedDoc,
originatingApp,
initialInput,
@@ -61,6 +54,14 @@ export function SaveModalContainer({
lensServices,
}: SaveModalContainerProps) {
const [lastKnownDoc, setLastKnownDoc] = useState(initLastKnowDoc);
+ let title = '';
+ let description;
+ let savedObjectId;
+ if (lastKnownDoc) {
+ title = lastKnownDoc.title;
+ description = lastKnownDoc.description;
+ savedObjectId = lastKnownDoc.savedObjectId;
+ }
const {
attributeService,
@@ -77,22 +78,26 @@ export function SaveModalContainer({
}, [initLastKnowDoc]);
useEffect(() => {
- async function loadLastKnownDoc() {
- if (initialInput && isVisible) {
- getLastKnownDoc({
+ let isMounted = true;
+ async function loadPersistedDoc() {
+ if (initialInput) {
+ getPersistedDoc({
data,
initialInput,
chrome,
notifications,
attributeService,
- }).then((result) => {
- if (result) setLastKnownDoc(result.doc);
+ }).then((doc) => {
+ if (doc && isMounted) setLastKnownDoc(doc);
});
}
}
- loadLastKnownDoc();
- }, [chrome, data, initialInput, notifications, attributeService, isVisible]);
+ loadPersistedDoc();
+ return () => {
+ isMounted = false;
+ };
+ }, [chrome, data, initialInput, notifications, attributeService]);
const tagsIds =
persistedDoc && savedObjectsTagging
@@ -131,7 +136,6 @@ export function SaveModalContainer({
return (
);
@@ -330,7 +336,10 @@ export const runSaveLensVisualization = async (
...newInput,
};
- return { persistedDoc: newDoc, lastKnownDoc: newDoc, isLinkedToOriginatingApp: false };
+ return {
+ persistedDoc: newDoc,
+ isLinkedToOriginatingApp: false,
+ };
} catch (e) {
// eslint-disable-next-line no-console
console.dir(e);
@@ -356,7 +365,7 @@ export function getLastKnownDocWithoutPinnedFilters(doc?: Document) {
: doc;
}
-export const getLastKnownDoc = async ({
+export const getPersistedDoc = async ({
initialInput,
attributeService,
data,
@@ -368,7 +377,7 @@ export const getLastKnownDoc = async ({
data: DataPublicPluginStart;
notifications: NotificationsStart;
chrome: ChromeStart;
-}): Promise<{ doc: Document; indexPatterns: IndexPattern[] } | undefined> => {
+}): Promise => {
let doc: Document;
try {
@@ -387,19 +396,12 @@ export const getLastKnownDoc = async ({
initialInput.savedObjectId
);
}
- const indexPatternIds = uniq(
- doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
- );
- const { indexPatterns } = await getAllIndexPatterns(indexPatternIds, data.indexPatterns);
// Don't overwrite any pinned filters
data.query.filterManager.setAppFilters(
injectFilterReferences(doc.state.filters, doc.references)
);
- return {
- doc,
- indexPatterns,
- };
+ return doc;
} catch (e) {
notifications.toasts.addDanger(
i18n.translate('xpack.lens.app.docLoadingError', {
diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts
index b4e7f18ccfeb8..7f1c21fa5a9bd 100644
--- a/x-pack/plugins/lens/public/app_plugin/types.ts
+++ b/x-pack/plugins/lens/public/app_plugin/types.ts
@@ -34,7 +34,7 @@ import {
EmbeddableEditorState,
EmbeddableStateTransfer,
} from '../../../../../src/plugins/embeddable/public';
-import { EditorFrameInstance } from '../types';
+import { Datasource, EditorFrameInstance, Visualization } from '../types';
import { PresentationUtilPluginStart } from '../../../../../src/plugins/presentation_util/public';
export interface RedirectToOriginProps {
input?: LensEmbeddableInput;
@@ -54,7 +54,8 @@ export interface LensAppProps {
// State passed in by the container which is used to determine the id of the Originating App.
incomingState?: EmbeddableEditorState;
- initialContext?: VisualizeFieldContext;
+ datasourceMap: Record;
+ visualizationMap: Record;
}
export type RunSave = (
@@ -81,6 +82,8 @@ export interface LensTopNavMenuProps {
indicateNoData: boolean;
setIsSaveModalVisible: React.Dispatch>;
runSave: RunSave;
+ datasourceMap: Record;
+ title?: string;
}
export interface HistoryLocationState {
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
index 3479a9e964d53..d755c5c297d04 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { EuiButtonGroup, EuiComboBox, EuiFieldText } from '@elastic/eui';
import { FramePublicAPI, Operation, VisualizationDimensionEditorProps } from '../../types';
import { DatatableVisualizationState } from '../visualization';
-import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks';
+import { createMockDatasource, createMockFramePublicAPI } from '../../mocks';
import { mountWithIntl } from '@kbn/test/jest';
import { TableDimensionEditor } from './dimension_editor';
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
index ea8237defc291..552f0f94a67de 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
@@ -7,7 +7,7 @@
import { Ast } from '@kbn/interpreter/common';
import { buildExpression } from '../../../../../src/plugins/expressions/public';
-import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
+import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
import { DatatableVisualizationState, getDatatableVisualization } from './visualization';
import {
Operation,
@@ -21,8 +21,6 @@ import { chartPluginMock } from 'src/plugins/charts/public/mocks';
function mockFrame(): FramePublicAPI {
return {
...createMockFramePublicAPI(),
- addNewLayer: () => 'aaa',
- removeLayers: () => {},
datasourceLayers: {},
query: { query: '', language: 'lucene' },
dateRange: {
@@ -40,7 +38,7 @@ const datatableVisualization = getDatatableVisualization({
describe('Datatable Visualization', () => {
describe('#initialize', () => {
it('should initialize from the empty state', () => {
- expect(datatableVisualization.initialize(mockFrame(), undefined)).toEqual({
+ expect(datatableVisualization.initialize(() => 'aaa', undefined)).toEqual({
layerId: 'aaa',
columns: [],
});
@@ -51,7 +49,7 @@ describe('Datatable Visualization', () => {
layerId: 'foo',
columns: [{ columnId: 'saved' }],
};
- expect(datatableVisualization.initialize(mockFrame(), expectedState)).toEqual(expectedState);
+ expect(datatableVisualization.initialize(() => 'foo', expectedState)).toEqual(expectedState);
});
});
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
index e48cb1b28c084..e7ab4aab88f2e 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
@@ -101,11 +101,11 @@ export const getDatatableVisualization = ({
switchVisualizationType: (_, state) => state,
- initialize(frame, state) {
+ initialize(addNewLayer, state) {
return (
state || {
columns: [],
- layerId: frame.addNewLayer(),
+ layerId: addNewLayer(),
}
);
},
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
index 1ec48f516bd32..25d99ed9bfd41 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
@@ -12,13 +12,13 @@ import {
createMockFramePublicAPI,
createMockDatasource,
DatasourceMock,
-} from '../../mocks';
+} from '../../../mocks';
import { Visualization } from '../../../types';
-import { mountWithIntl } from '@kbn/test/jest';
import { LayerPanels } from './config_panel';
import { LayerPanel } from './layer_panel';
import { coreMock } from 'src/core/public/mocks';
import { generateId } from '../../../id_generator';
+import { mountWithProvider } from '../../../mocks';
jest.mock('../../../id_generator');
@@ -54,17 +54,17 @@ describe('ConfigPanel', () => {
vis1: mockVisualization,
vis2: mockVisualization2,
},
- activeDatasourceId: 'ds1',
+ activeDatasourceId: 'mockindexpattern',
datasourceMap: {
- ds1: mockDatasource,
+ mockindexpattern: mockDatasource,
},
activeVisualization: ({
...mockVisualization,
getLayerIds: () => Object.keys(frame.datasourceLayers),
- appendLayer: true,
+ appendLayer: jest.fn(),
} as unknown) as Visualization,
datasourceStates: {
- ds1: {
+ mockindexpattern: {
isLoading: false,
state: 'state',
},
@@ -110,113 +110,184 @@ describe('ConfigPanel', () => {
};
mockVisualization.getLayerIds.mockReturnValue(Object.keys(frame.datasourceLayers));
- mockDatasource = createMockDatasource('ds1');
+ mockDatasource = createMockDatasource('mockindexpattern');
});
// in what case is this test needed?
- it('should fail to render layerPanels if the public API is out of date', () => {
+ it('should fail to render layerPanels if the public API is out of date', async () => {
const props = getDefaultProps();
props.framePublicAPI.datasourceLayers = {};
- const component = mountWithIntl();
- expect(component.find(LayerPanel).exists()).toBe(false);
+ const { instance } = await mountWithProvider();
+ expect(instance.find(LayerPanel).exists()).toBe(false);
});
it('allow datasources and visualizations to use setters', async () => {
const props = getDefaultProps();
- const component = mountWithIntl();
- const { updateDatasource, updateAll } = component.find(LayerPanel).props();
+ const { instance, lensStore } = await mountWithProvider(, {
+ preloadedState: {
+ datasourceStates: {
+ mockindexpattern: {
+ isLoading: false,
+ state: 'state',
+ },
+ },
+ },
+ });
+ const { updateDatasource, updateAll } = instance.find(LayerPanel).props();
const updater = () => 'updated';
- updateDatasource('ds1', updater);
- // wait for one tick so async updater has a chance to trigger
+ updateDatasource('mockindexpattern', updater);
await new Promise((r) => setTimeout(r, 0));
- expect(props.dispatch).toHaveBeenCalledTimes(1);
- expect(props.dispatch.mock.calls[0][0].updater(props.datasourceStates.ds1.state)).toEqual(
- 'updated'
- );
+ expect(lensStore.dispatch).toHaveBeenCalledTimes(1);
+ expect(
+ (lensStore.dispatch as jest.Mock).mock.calls[0][0].payload.updater(
+ props.datasourceStates.mockindexpattern.state
+ )
+ ).toEqual('updated');
- updateAll('ds1', updater, props.visualizationState);
+ updateAll('mockindexpattern', updater, props.visualizationState);
// wait for one tick so async updater has a chance to trigger
await new Promise((r) => setTimeout(r, 0));
- expect(props.dispatch).toHaveBeenCalledTimes(2);
- expect(props.dispatch.mock.calls[0][0].updater(props.datasourceStates.ds1.state)).toEqual(
- 'updated'
- );
+ expect(lensStore.dispatch).toHaveBeenCalledTimes(2);
+ expect(
+ (lensStore.dispatch as jest.Mock).mock.calls[0][0].payload.updater(
+ props.datasourceStates.mockindexpattern.state
+ )
+ ).toEqual('updated');
});
describe('focus behavior when adding or removing layers', () => {
- it('should focus the only layer when resetting the layer', () => {
- const component = mountWithIntl(, {
- attachTo: container,
- });
- const firstLayerFocusable = component
+ it('should focus the only layer when resetting the layer', async () => {
+ const { instance } = await mountWithProvider(
+ ,
+ {
+ preloadedState: {
+ datasourceStates: {
+ mockindexpattern: {
+ isLoading: false,
+ state: 'state',
+ },
+ },
+ },
+ },
+ {
+ attachTo: container,
+ }
+ );
+ const firstLayerFocusable = instance
.find(LayerPanel)
.first()
.find('section')
.first()
.instance();
act(() => {
- component.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click');
+ instance.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click');
});
const focusedEl = document.activeElement;
expect(focusedEl).toEqual(firstLayerFocusable);
});
- it('should focus the second layer when removing the first layer', () => {
+ it('should focus the second layer when removing the first layer', async () => {
const defaultProps = getDefaultProps();
// overwriting datasourceLayers to test two layers
frame.datasourceLayers = {
first: mockDatasource.publicAPIMock,
second: mockDatasource.publicAPIMock,
};
- const component = mountWithIntl(, { attachTo: container });
- const secondLayerFocusable = component
+ const { instance } = await mountWithProvider(
+ ,
+ {
+ preloadedState: {
+ datasourceStates: {
+ mockindexpattern: {
+ isLoading: false,
+ state: 'state',
+ },
+ },
+ },
+ },
+ {
+ attachTo: container,
+ }
+ );
+
+ const secondLayerFocusable = instance
.find(LayerPanel)
.at(1)
.find('section')
.first()
.instance();
act(() => {
- component.find('[data-test-subj="lnsLayerRemove"]').at(0).simulate('click');
+ instance.find('[data-test-subj="lnsLayerRemove"]').at(0).simulate('click');
});
const focusedEl = document.activeElement;
expect(focusedEl).toEqual(secondLayerFocusable);
});
- it('should focus the first layer when removing the second layer', () => {
+ it('should focus the first layer when removing the second layer', async () => {
const defaultProps = getDefaultProps();
// overwriting datasourceLayers to test two layers
frame.datasourceLayers = {
first: mockDatasource.publicAPIMock,
second: mockDatasource.publicAPIMock,
};
- const component = mountWithIntl(, { attachTo: container });
- const firstLayerFocusable = component
+ const { instance } = await mountWithProvider(
+ ,
+ {
+ preloadedState: {
+ datasourceStates: {
+ mockindexpattern: {
+ isLoading: false,
+ state: 'state',
+ },
+ },
+ },
+ },
+ {
+ attachTo: container,
+ }
+ );
+ const firstLayerFocusable = instance
.find(LayerPanel)
.first()
.find('section')
.first()
.instance();
act(() => {
- component.find('[data-test-subj="lnsLayerRemove"]').at(2).simulate('click');
+ instance.find('[data-test-subj="lnsLayerRemove"]').at(2).simulate('click');
});
const focusedEl = document.activeElement;
expect(focusedEl).toEqual(firstLayerFocusable);
});
- it('should focus the added layer', () => {
+ it('should focus the added layer', async () => {
(generateId as jest.Mock).mockReturnValue(`second`);
- const dispatch = jest.fn((x) => {
- if (x.subType === 'ADD_LAYER') {
- frame.datasourceLayers.second = mockDatasource.publicAPIMock;
- }
- });
- const component = mountWithIntl(, {
- attachTo: container,
- });
+ const { instance } = await mountWithProvider(
+ ,
+
+ {
+ preloadedState: {
+ datasourceStates: {
+ mockindexpattern: {
+ isLoading: false,
+ state: 'state',
+ },
+ },
+ activeDatasourceId: 'mockindexpattern',
+ },
+ dispatch: jest.fn((x) => {
+ if (x.payload.subType === 'ADD_LAYER') {
+ frame.datasourceLayers.second = mockDatasource.publicAPIMock;
+ }
+ }),
+ },
+ {
+ attachTo: container,
+ }
+ );
act(() => {
- component.find('[data-test-subj="lnsLayerAddButton"]').first().simulate('click');
+ instance.find('[data-test-subj="lnsLayerAddButton"]').first().simulate('click');
});
const focusedEl = document.activeElement;
expect(focusedEl?.children[0].getAttribute('data-test-subj')).toEqual('lns-layerPanel-1');
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
index 81c044af532fb..c7147e75af59a 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
@@ -10,13 +10,21 @@ import './config_panel.scss';
import React, { useMemo, memo } from 'react';
import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { mapValues } from 'lodash';
import { Visualization } from '../../../types';
import { LayerPanel } from './layer_panel';
import { trackUiEvent } from '../../../lens_ui_telemetry';
import { generateId } from '../../../id_generator';
-import { removeLayer, appendLayer } from './layer_actions';
+import { appendLayer } from './layer_actions';
import { ConfigPanelWrapperProps } from './types';
import { useFocusUpdate } from './use_focus_update';
+import {
+ useLensDispatch,
+ updateState,
+ updateDatasourceState,
+ updateVisualizationState,
+ setToggleFullscreen,
+} from '../../../state_management';
export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
const activeVisualization = props.visualizationMap[props.activeVisualizationId || ''];
@@ -33,13 +41,8 @@ export function LayerPanels(
activeVisualization: Visualization;
}
) {
- const {
- activeVisualization,
- visualizationState,
- dispatch,
- activeDatasourceId,
- datasourceMap,
- } = props;
+ const { activeVisualization, visualizationState, activeDatasourceId, datasourceMap } = props;
+ const dispatchLens = useLensDispatch();
const layerIds = activeVisualization.getLayerIds(visualizationState);
const {
@@ -50,26 +53,28 @@ export function LayerPanels(
const setVisualizationState = useMemo(
() => (newState: unknown) => {
- dispatch({
- type: 'UPDATE_VISUALIZATION_STATE',
- visualizationId: activeVisualization.id,
- updater: newState,
- clearStagedPreview: false,
- });
+ dispatchLens(
+ updateVisualizationState({
+ visualizationId: activeVisualization.id,
+ updater: newState,
+ clearStagedPreview: false,
+ })
+ );
},
- [dispatch, activeVisualization]
+ [activeVisualization, dispatchLens]
);
const updateDatasource = useMemo(
() => (datasourceId: string, newState: unknown) => {
- dispatch({
- type: 'UPDATE_DATASOURCE_STATE',
- updater: (prevState: unknown) =>
- typeof newState === 'function' ? newState(prevState) : newState,
- datasourceId,
- clearStagedPreview: false,
- });
+ dispatchLens(
+ updateDatasourceState({
+ updater: (prevState: unknown) =>
+ typeof newState === 'function' ? newState(prevState) : newState,
+ datasourceId,
+ clearStagedPreview: false,
+ })
+ );
},
- [dispatch]
+ [dispatchLens]
);
const updateDatasourceAsync = useMemo(
() => (datasourceId: string, newState: unknown) => {
@@ -86,42 +91,42 @@ export function LayerPanels(
// React will synchronously update if this is triggered from a third party component,
// which we don't want. The timeout lets user interaction have priority, then React updates.
setTimeout(() => {
- dispatch({
- type: 'UPDATE_STATE',
- subType: 'UPDATE_ALL_STATES',
- updater: (prevState) => {
- const updatedDatasourceState =
- typeof newDatasourceState === 'function'
- ? newDatasourceState(prevState.datasourceStates[datasourceId].state)
- : newDatasourceState;
- return {
- ...prevState,
- datasourceStates: {
- ...prevState.datasourceStates,
- [datasourceId]: {
- state: updatedDatasourceState,
- isLoading: false,
+ dispatchLens(
+ updateState({
+ subType: 'UPDATE_ALL_STATES',
+ updater: (prevState) => {
+ const updatedDatasourceState =
+ typeof newDatasourceState === 'function'
+ ? newDatasourceState(prevState.datasourceStates[datasourceId].state)
+ : newDatasourceState;
+ return {
+ ...prevState,
+ datasourceStates: {
+ ...prevState.datasourceStates,
+ [datasourceId]: {
+ state: updatedDatasourceState,
+ isLoading: false,
+ },
+ },
+ visualization: {
+ ...prevState.visualization,
+ state: newVisualizationState,
},
- },
- visualization: {
- ...prevState.visualization,
- state: newVisualizationState,
- },
- stagedPreview: undefined,
- };
- },
- });
+ stagedPreview: undefined,
+ };
+ },
+ })
+ );
}, 0);
},
- [dispatch]
+ [dispatchLens]
);
+
const toggleFullscreen = useMemo(
() => () => {
- dispatch({
- type: 'TOGGLE_FULLSCREEN',
- });
+ dispatchLens(setToggleFullscreen());
},
- [dispatch]
+ [dispatchLens]
);
const datasourcePublicAPIs = props.framePublicAPI.datasourceLayers;
@@ -144,18 +149,41 @@ export function LayerPanels(
updateAll={updateAll}
isOnlyLayer={layerIds.length === 1}
onRemoveLayer={() => {
- dispatch({
- type: 'UPDATE_STATE',
- subType: 'REMOVE_OR_CLEAR_LAYER',
- updater: (state) =>
- removeLayer({
- activeVisualization,
- layerId,
- trackUiEvent,
- datasourceMap,
- state,
- }),
- });
+ dispatchLens(
+ updateState({
+ subType: 'REMOVE_OR_CLEAR_LAYER',
+ updater: (state) => {
+ const isOnlyLayer = activeVisualization
+ .getLayerIds(state.visualization.state)
+ .every((id) => id === layerId);
+
+ return {
+ ...state,
+ datasourceStates: mapValues(
+ state.datasourceStates,
+ (datasourceState, datasourceId) => {
+ const datasource = datasourceMap[datasourceId!];
+ return {
+ ...datasourceState,
+ state: isOnlyLayer
+ ? datasource.clearLayer(datasourceState.state, layerId)
+ : datasource.removeLayer(datasourceState.state, layerId),
+ };
+ }
+ ),
+ visualization: {
+ ...state.visualization,
+ state:
+ isOnlyLayer || !activeVisualization.removeLayer
+ ? activeVisualization.clearLayer(state.visualization.state, layerId)
+ : activeVisualization.removeLayer(state.visualization.state, layerId),
+ },
+ stagedPreview: undefined,
+ };
+ },
+ })
+ );
+
removeLayerRef(layerId);
}}
toggleFullscreen={toggleFullscreen}
@@ -187,18 +215,19 @@ export function LayerPanels(
color="text"
onClick={() => {
const id = generateId();
- dispatch({
- type: 'UPDATE_STATE',
- subType: 'ADD_LAYER',
- updater: (state) =>
- appendLayer({
- activeVisualization,
- generateId: () => id,
- trackUiEvent,
- activeDatasource: datasourceMap[activeDatasourceId],
- state,
- }),
- });
+ dispatchLens(
+ updateState({
+ subType: 'ADD_LAYER',
+ updater: (state) =>
+ appendLayer({
+ activeVisualization,
+ generateId: () => id,
+ trackUiEvent,
+ activeDatasource: datasourceMap[activeDatasourceId],
+ state,
+ }),
+ })
+ );
setNextFocusedLayerId(id);
}}
iconType="plusInCircleFilled"
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts
index d28d3acbf3bae..ad15be170e631 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { initialState } from '../../../state_management/lens_slice';
import { removeLayer, appendLayer } from './layer_actions';
function createTestArgs(initialLayerIds: string[]) {
@@ -42,6 +43,7 @@ function createTestArgs(initialLayerIds: string[]) {
return {
state: {
+ ...initialState,
activeDatasourceId: 'ds1',
datasourceStates,
title: 'foo',
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts
index 7d8a373192ee5..328a868cfb893 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts
@@ -6,12 +6,13 @@
*/
import { mapValues } from 'lodash';
-import { EditorFrameState } from '../state_management';
+import { LensAppState } from '../../../state_management';
+
import { Datasource, Visualization } from '../../../types';
interface RemoveLayerOptions {
trackUiEvent: (name: string) => void;
- state: EditorFrameState;
+ state: LensAppState;
layerId: string;
activeVisualization: Pick;
datasourceMap: Record>;
@@ -19,13 +20,13 @@ interface RemoveLayerOptions {
interface AppendLayerOptions {
trackUiEvent: (name: string) => void;
- state: EditorFrameState;
+ state: LensAppState;
generateId: () => string;
activeDatasource: Pick;
activeVisualization: Pick;
}
-export function removeLayer(opts: RemoveLayerOptions): EditorFrameState {
+export function removeLayer(opts: RemoveLayerOptions): LensAppState {
const { state, trackUiEvent: trackUiEvent, activeVisualization, layerId, datasourceMap } = opts;
const isOnlyLayer = activeVisualization
.getLayerIds(state.visualization.state)
@@ -61,7 +62,7 @@ export function appendLayer({
state,
generateId,
activeDatasource,
-}: AppendLayerOptions): EditorFrameState {
+}: AppendLayerOptions): LensAppState {
trackUiEvent('layer_added');
if (!activeVisualization.appendLayer) {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
index dd1241af14f5a..3bb5fca2141a0 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
@@ -19,7 +19,7 @@ import {
createMockFramePublicAPI,
createMockDatasource,
DatasourceMock,
-} from '../../mocks';
+} from '../../../mocks';
jest.mock('../../../id_generator');
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts
index 1af8c16fa1395..683b96c6b8773 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { Action } from '../state_management';
import {
Visualization,
FramePublicAPI,
@@ -18,7 +17,6 @@ export interface ConfigPanelWrapperProps {
visualizationState: unknown;
visualizationMap: Record;
activeVisualizationId: string | null;
- dispatch: (action: Action) => void;
framePublicAPI: FramePublicAPI;
datasourceMap: Record;
datasourceStates: Record<
@@ -37,7 +35,6 @@ export interface LayerPanelProps {
visualizationState: unknown;
datasourceMap: Record;
activeVisualization: Visualization;
- dispatch: (action: Action) => void;
framePublicAPI: FramePublicAPI;
datasourceStates: Record<
string,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx
index 9bf03025e400f..c50d3f41479f1 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx
@@ -7,54 +7,94 @@
import './data_panel_wrapper.scss';
-import React, { useMemo, memo, useContext, useState } from 'react';
+import React, { useMemo, memo, useContext, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui';
+import { createSelector } from '@reduxjs/toolkit';
import { NativeRenderer } from '../../native_renderer';
-import { Action } from './state_management';
import { DragContext, DragDropIdentifier } from '../../drag_drop';
-import { StateSetter, FramePublicAPI, DatasourceDataPanelProps, Datasource } from '../../types';
-import { Query, Filter } from '../../../../../../src/plugins/data/public';
+import { StateSetter, DatasourceDataPanelProps, Datasource } from '../../types';
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
+import {
+ switchDatasource,
+ useLensDispatch,
+ updateDatasourceState,
+ LensState,
+ useLensSelector,
+ setState,
+} from '../../state_management';
+import { initializeDatasources } from './state_helpers';
interface DataPanelWrapperProps {
datasourceState: unknown;
datasourceMap: Record;
activeDatasource: string | null;
datasourceIsLoading: boolean;
- dispatch: (action: Action) => void;
showNoDataPopover: () => void;
core: DatasourceDataPanelProps['core'];
- query: Query;
- dateRange: FramePublicAPI['dateRange'];
- filters: Filter[];
dropOntoWorkspace: (field: DragDropIdentifier) => void;
hasSuggestionForField: (field: DragDropIdentifier) => boolean;
plugins: { uiActions: UiActionsStart };
}
+const getExternals = createSelector(
+ (state: LensState) => state.lens,
+ ({ resolvedDateRange, query, filters, datasourceStates, activeDatasourceId }) => ({
+ dateRange: resolvedDateRange,
+ query,
+ filters,
+ datasourceStates,
+ activeDatasourceId,
+ })
+);
+
export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
- const { dispatch, activeDatasource } = props;
- const setDatasourceState: StateSetter = useMemo(
- () => (updater) => {
- dispatch({
- type: 'UPDATE_DATASOURCE_STATE',
- updater,
- datasourceId: activeDatasource!,
- clearStagedPreview: true,
- });
- },
- [dispatch, activeDatasource]
+ const { activeDatasource } = props;
+
+ const { filters, query, dateRange, datasourceStates, activeDatasourceId } = useLensSelector(
+ getExternals
);
+ const dispatchLens = useLensDispatch();
+ const setDatasourceState: StateSetter = useMemo(() => {
+ return (updater) => {
+ dispatchLens(
+ updateDatasourceState({
+ updater,
+ datasourceId: activeDatasource!,
+ clearStagedPreview: true,
+ })
+ );
+ };
+ }, [activeDatasource, dispatchLens]);
+
+ useEffect(() => {
+ if (activeDatasourceId && datasourceStates[activeDatasourceId].state === null) {
+ initializeDatasources(props.datasourceMap, datasourceStates, undefined, undefined, {
+ isFullEditor: true,
+ }).then((result) => {
+ const newDatasourceStates = Object.entries(result).reduce(
+ (state, [datasourceId, datasourceState]) => ({
+ ...state,
+ [datasourceId]: {
+ ...datasourceState,
+ isLoading: false,
+ },
+ }),
+ {}
+ );
+ dispatchLens(setState({ datasourceStates: newDatasourceStates }));
+ });
+ }
+ }, [datasourceStates, activeDatasourceId, props.datasourceMap, dispatchLens]);
const datasourceProps: DatasourceDataPanelProps = {
dragDropContext: useContext(DragContext),
state: props.datasourceState,
setState: setDatasourceState,
core: props.core,
- query: props.query,
- dateRange: props.dateRange,
- filters: props.filters,
+ filters,
+ query,
+ dateRange,
showNoDataPopover: props.showNoDataPopover,
dropOntoWorkspace: props.dropOntoWorkspace,
hasSuggestionForField: props.hasSuggestionForField,
@@ -98,10 +138,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
icon={props.activeDatasource === datasourceId ? 'check' : 'empty'}
onClick={() => {
setDatasourceSwitcher(false);
- props.dispatch({
- type: 'SWITCH_DATASOURCE',
- newDatasourceId: datasourceId,
- });
+ dispatchLens(switchDatasource({ newDatasourceId: datasourceId }));
}}
>
{datasourceId}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
index 0e2ba5ce8ad59..4ce68dc3bc70a 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
@@ -7,7 +7,6 @@
import React, { ReactElement } from 'react';
import { ReactWrapper } from 'enzyme';
-import { setState, LensRootStore } from '../../state_management/index';
// Tests are executed in a jsdom environment who does not have sizing methods,
// thus the AutoSizer will always compute a 0x0 size space
@@ -37,16 +36,17 @@ import { fromExpression } from '@kbn/interpreter/common';
import {
createMockVisualization,
createMockDatasource,
- createExpressionRendererMock,
DatasourceMock,
-} from '../mocks';
+ createExpressionRendererMock,
+} from '../../mocks';
import { ReactExpressionRendererType } from 'src/plugins/expressions/public';
import { DragDrop } from '../../drag_drop';
-import { FrameLayout } from './frame_layout';
import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks';
import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks';
import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks';
import { mockDataPlugin, mountWithProvider } from '../../mocks';
+import { setState, setToggleFullscreen } from '../../state_management';
+import { FrameLayout } from './frame_layout';
function generateSuggestion(state = {}): DatasourceSuggestion {
return {
@@ -130,68 +130,6 @@ describe('editor_frame', () => {
});
describe('initialization', () => {
- it('should initialize initial datasource', async () => {
- mockVisualization.getLayerIds.mockReturnValue([]);
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- },
-
- ExpressionRenderer: expressionRendererMock,
- };
-
- await mountWithProvider(, props.plugins.data);
- expect(mockDatasource.initialize).toHaveBeenCalled();
- });
-
- it('should initialize all datasources with state from doc', async () => {
- const mockDatasource3 = createMockDatasource('testDatasource3');
- const datasource1State = { datasource1: '' };
- const datasource2State = { datasource2: '' };
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- testDatasource2: mockDatasource2,
- testDatasource3: mockDatasource3,
- },
-
- ExpressionRenderer: expressionRendererMock,
- };
-
- await mountWithProvider(, props.plugins.data, {
- persistedDoc: {
- visualizationType: 'testVis',
- title: '',
- state: {
- datasourceStates: {
- testDatasource: datasource1State,
- testDatasource2: datasource2State,
- },
- visualization: {},
- query: { query: '', language: 'lucene' },
- filters: [],
- },
- references: [],
- },
- });
-
- expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State, [], undefined, {
- isFullEditor: true,
- });
- expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State, [], undefined, {
- isFullEditor: true,
- });
- expect(mockDatasource3.initialize).not.toHaveBeenCalled();
- });
-
it('should not render something before all datasources are initialized', async () => {
const props = {
...getDefaultProps(),
@@ -204,177 +142,36 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
-
- await act(async () => {
- mountWithProvider(, props.plugins.data);
- expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled();
- });
- expect(mockDatasource.renderDataPanel).toHaveBeenCalled();
- });
-
- it('should not initialize visualization before datasource is initialized', async () => {
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- },
-
- ExpressionRenderer: expressionRendererMock,
- };
-
- await act(async () => {
- mountWithProvider(, props.plugins.data);
- expect(mockVisualization.initialize).not.toHaveBeenCalled();
- });
-
- expect(mockVisualization.initialize).toHaveBeenCalled();
- });
-
- it('should pass the public frame api into visualization initialize', async () => {
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- },
-
- ExpressionRenderer: expressionRendererMock,
- };
- await act(async () => {
- mountWithProvider(, props.plugins.data);
- expect(mockVisualization.initialize).not.toHaveBeenCalled();
- });
-
- expect(mockVisualization.initialize).toHaveBeenCalledWith({
- datasourceLayers: {},
- addNewLayer: expect.any(Function),
- removeLayers: expect.any(Function),
- query: { query: '', language: 'lucene' },
- filters: [],
- dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' },
- availablePalettes: props.palettes,
- searchSessionId: 'sessionId-1',
- });
- });
-
- it('should add new layer on active datasource on frame api call', async () => {
- const initialState = { datasource2: '' };
- mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState));
-
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- testDatasource2: mockDatasource2,
- },
-
- ExpressionRenderer: expressionRendererMock,
- };
- await mountWithProvider(, props.plugins.data, {
- persistedDoc: {
- visualizationType: 'testVis',
- title: '',
- state: {
+ const lensStore = (
+ await mountWithProvider(, {
+ data: props.plugins.data,
+ preloadedState: {
+ activeDatasourceId: 'testDatasource',
datasourceStates: {
- testDatasource2: mockDatasource2,
+ testDatasource: {
+ isLoading: true,
+ state: {
+ internalState1: '',
+ },
+ },
},
- visualization: {},
- query: { query: '', language: 'lucene' },
- filters: [],
},
- references: [],
- },
- });
- act(() => {
- mockVisualization.initialize.mock.calls[0][0].addNewLayer();
- });
-
- expect(mockDatasource2.insertLayer).toHaveBeenCalledWith(initialState, expect.anything());
- });
-
- it('should remove layer on active datasource on frame api call', async () => {
- const initialState = { datasource2: '' };
- mockDatasource.getLayers.mockReturnValue(['first']);
- mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState));
- mockDatasource2.getLayers.mockReturnValue(['abc', 'def']);
- mockDatasource2.removeLayer.mockReturnValue({ removed: true });
- mockVisualization.getLayerIds.mockReturnValue(['first', 'abc', 'def']);
-
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- testDatasource2: mockDatasource2,
- },
- ExpressionRenderer: expressionRendererMock,
- };
-
- await mountWithProvider(, props.plugins.data, {
- persistedDoc: {
- visualizationType: 'testVis',
- title: '',
- state: {
- datasourceStates: {
- testDatasource2: mockDatasource2,
+ })
+ ).lensStore;
+ expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled();
+ lensStore.dispatch(
+ setState({
+ datasourceStates: {
+ testDatasource: {
+ isLoading: false,
+ state: {
+ internalState1: '',
+ },
},
- visualization: {},
- query: { query: '', language: 'lucene' },
- filters: [],
},
- references: [],
- },
- });
-
- act(() => {
- mockVisualization.initialize.mock.calls[0][0].removeLayers(['abc', 'def']);
- });
-
- expect(mockDatasource2.removeLayer).toHaveBeenCalledWith(initialState, 'abc');
- expect(mockDatasource2.removeLayer).toHaveBeenCalledWith({ removed: true }, 'def');
- });
-
- it('should render data panel after initialization is complete', async () => {
- const initialState = {};
- let databaseInitialized: ({}) => void;
-
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: {
- ...mockDatasource,
- initialize: () =>
- new Promise((resolve) => {
- databaseInitialized = resolve;
- }),
- },
- },
-
- ExpressionRenderer: expressionRendererMock,
- };
-
- await mountWithProvider(, props.plugins.data);
-
- await act(async () => {
- databaseInitialized!(initialState);
- });
- expect(mockDatasource.renderDataPanel).toHaveBeenCalledWith(
- expect.any(Element),
- expect.objectContaining({ state: initialState })
+ })
);
+ expect(mockDatasource.renderDataPanel).toHaveBeenCalled();
});
it('should initialize visualization state and render config panel', async () => {
@@ -396,7 +193,12 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- await mountWithProvider(, props.plugins.data);
+ await mountWithProvider(, {
+ data: props.plugins.data,
+ preloadedState: {
+ visualization: { activeId: 'testVis', state: initialState },
+ },
+ });
expect(mockVisualization.getConfiguration).toHaveBeenCalledWith(
expect.objectContaining({ state: initialState })
@@ -422,7 +224,22 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- instance = (await mountWithProvider(, props.plugins.data)).instance;
+ instance = (
+ await mountWithProvider(, {
+ data: props.plugins.data,
+ preloadedState: {
+ visualization: { activeId: 'testVis', state: null },
+ datasourceStates: {
+ testDatasource: {
+ isLoading: false,
+ state: {
+ internalState1: '',
+ },
+ },
+ },
+ },
+ })
+ ).instance;
instance.update();
@@ -437,37 +254,50 @@ describe('editor_frame', () => {
mockDatasource.toExpression.mockReturnValue('datasource');
mockDatasource2.toExpression.mockImplementation((_state, layerId) => `datasource_${layerId}`);
mockDatasource.initialize.mockImplementation((initialState) => Promise.resolve(initialState));
- mockDatasource.getLayers.mockReturnValue(['first']);
+ mockDatasource.getLayers.mockReturnValue(['first', 'second']);
mockDatasource2.initialize.mockImplementation((initialState) =>
Promise.resolve(initialState)
);
- mockDatasource2.getLayers.mockReturnValue(['second', 'third']);
+ mockDatasource2.getLayers.mockReturnValue(['third']);
const props = {
...getDefaultProps(),
visualizationMap: {
testVis: { ...mockVisualization, toExpression: () => 'vis' },
},
- datasourceMap: { testDatasource: mockDatasource, testDatasource2: mockDatasource2 },
+ datasourceMap: {
+ testDatasource: {
+ ...mockDatasource,
+ toExpression: () => 'datasource',
+ },
+ testDatasource2: {
+ ...mockDatasource2,
+ toExpression: () => 'datasource_second',
+ },
+ },
ExpressionRenderer: expressionRendererMock,
};
instance = (
- await mountWithProvider(, props.plugins.data, {
- persistedDoc: {
- visualizationType: 'testVis',
- title: '',
- state: {
- datasourceStates: {
- testDatasource: {},
- testDatasource2: {},
+ await mountWithProvider(, {
+ data: props.plugins.data,
+ preloadedState: {
+ visualization: { activeId: 'testVis', state: null },
+ datasourceStates: {
+ testDatasource: {
+ isLoading: false,
+ state: {
+ internalState1: '',
+ },
+ },
+ testDatasource2: {
+ isLoading: false,
+ state: {
+ internalState1: '',
+ },
},
- visualization: {},
- query: { query: '', language: 'lucene' },
- filters: [],
},
- references: [],
},
})
).instance;
@@ -515,7 +345,7 @@ describe('editor_frame', () => {
"chain": Array [
Object {
"arguments": Object {},
- "function": "datasource_second",
+ "function": "datasource",
"type": "function",
},
],
@@ -525,7 +355,7 @@ describe('editor_frame', () => {
"chain": Array [
Object {
"arguments": Object {},
- "function": "datasource_third",
+ "function": "datasource_second",
"type": "function",
},
],
@@ -562,7 +392,19 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- await mountWithProvider(, props.plugins.data);
+ await mountWithProvider(, {
+ data: props.plugins.data,
+ preloadedState: {
+ activeDatasourceId: 'testDatasource',
+ visualization: { activeId: mockVisualization.id, state: {} },
+ datasourceStates: {
+ testDatasource: {
+ isLoading: false,
+ state: '',
+ },
+ },
+ },
+ });
const updatedState = {};
const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1]
.setState;
@@ -593,7 +435,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- await mountWithProvider(, props.plugins.data);
+ await mountWithProvider(, { data: props.plugins.data });
const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1]
.setState;
@@ -629,7 +471,10 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- await mountWithProvider(, props.plugins.data);
+ await mountWithProvider(, {
+ data: props.plugins.data,
+ preloadedState: { visualization: { activeId: mockVisualization.id, state: {} } },
+ });
const updatedPublicAPI: DatasourcePublicAPI = {
datasourceId: 'testDatasource',
@@ -659,58 +504,10 @@ describe('editor_frame', () => {
});
describe('datasource public api communication', () => {
- it('should pass the datasource api for each layer to the visualization', async () => {
- mockDatasource.getLayers.mockReturnValue(['first']);
- mockDatasource2.getLayers.mockReturnValue(['second', 'third']);
- mockVisualization.getLayerIds.mockReturnValue(['first', 'second', 'third']);
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- testDatasource2: mockDatasource2,
- },
-
- ExpressionRenderer: expressionRendererMock,
- };
- await mountWithProvider(, props.plugins.data, {
- persistedDoc: {
- visualizationType: 'testVis',
- title: '',
- state: {
- datasourceStates: {
- testDatasource: {},
- testDatasource2: {},
- },
- visualization: {},
- query: { query: '', language: 'lucene' },
- filters: [],
- },
- references: [],
- },
- });
-
- expect(mockVisualization.getConfiguration).toHaveBeenCalled();
-
- const datasourceLayers =
- mockVisualization.getConfiguration.mock.calls[0][0].frame.datasourceLayers;
- expect(datasourceLayers.first).toBe(mockDatasource.publicAPIMock);
- expect(datasourceLayers.second).toBe(mockDatasource2.publicAPIMock);
- expect(datasourceLayers.third).toBe(mockDatasource2.publicAPIMock);
- });
-
- it('should create a separate datasource public api for each layer', async () => {
- mockDatasource.initialize.mockImplementation((initialState) => Promise.resolve(initialState));
+ it('should give access to the datasource state in the datasource factory function', async () => {
+ const datasourceState = {};
+ mockDatasource.initialize.mockResolvedValue(datasourceState);
mockDatasource.getLayers.mockReturnValue(['first']);
- mockDatasource2.initialize.mockImplementation((initialState) =>
- Promise.resolve(initialState)
- );
- mockDatasource2.getLayers.mockReturnValue(['second', 'third']);
-
- const datasource1State = { datasource1: '' };
- const datasource2State = { datasource2: '' };
const props = {
...getDefaultProps(),
@@ -719,66 +516,22 @@ describe('editor_frame', () => {
},
datasourceMap: {
testDatasource: mockDatasource,
- testDatasource2: mockDatasource2,
},
ExpressionRenderer: expressionRendererMock,
};
- await mountWithProvider(, props.plugins.data, {
- persistedDoc: {
- visualizationType: 'testVis',
- title: '',
- state: {
- datasourceStates: {
- testDatasource: datasource1State,
- testDatasource2: datasource2State,
+ await mountWithProvider(, {
+ data: props.plugins.data,
+ preloadedState: {
+ datasourceStates: {
+ testDatasource: {
+ isLoading: false,
+ state: {},
},
- visualization: {},
- query: { query: '', language: 'lucene' },
- filters: [],
},
- references: [],
},
});
- expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith(
- expect.objectContaining({
- state: datasource1State,
- layerId: 'first',
- })
- );
- expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith(
- expect.objectContaining({
- state: datasource2State,
- layerId: 'second',
- })
- );
- expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith(
- expect.objectContaining({
- state: datasource2State,
- layerId: 'third',
- })
- );
- });
-
- it('should give access to the datasource state in the datasource factory function', async () => {
- const datasourceState = {};
- mockDatasource.initialize.mockResolvedValue(datasourceState);
- mockDatasource.getLayers.mockReturnValue(['first']);
-
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- },
-
- ExpressionRenderer: expressionRendererMock,
- };
- await mountWithProvider(, props.plugins.data);
-
expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({
state: datasourceState,
layerId: 'first',
@@ -832,7 +585,8 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- instance = (await mountWithProvider(, props.plugins.data)).instance;
+ instance = (await mountWithProvider(, { data: props.plugins.data }))
+ .instance;
// necessary to flush elements to dom synchronously
instance.update();
@@ -842,14 +596,6 @@ describe('editor_frame', () => {
instance.unmount();
});
- it('should have initialized only the initial datasource and visualization', () => {
- expect(mockDatasource.initialize).toHaveBeenCalled();
- expect(mockDatasource2.initialize).not.toHaveBeenCalled();
-
- expect(mockVisualization.initialize).toHaveBeenCalled();
- expect(mockVisualization2.initialize).not.toHaveBeenCalled();
- });
-
it('should initialize other datasource on switch', async () => {
await act(async () => {
instance.find('button[data-test-subj="datasource-switch"]').simulate('click');
@@ -859,6 +605,7 @@ describe('editor_frame', () => {
'[data-test-subj="datasource-switch-testDatasource2"]'
) as HTMLButtonElement).click();
});
+ instance.update();
expect(mockDatasource2.initialize).toHaveBeenCalled();
});
@@ -915,9 +662,7 @@ describe('editor_frame', () => {
expect(mockDatasource.publicAPIMock.getTableSpec).toHaveBeenCalled();
expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
expect(mockVisualization2.initialize).toHaveBeenCalledWith(
- expect.objectContaining({
- datasourceLayers: expect.objectContaining({ first: mockDatasource.publicAPIMock }),
- }),
+ expect.any(Function), // generated layerId
undefined,
undefined
);
@@ -928,28 +673,6 @@ describe('editor_frame', () => {
});
describe('suggestions', () => {
- it('should fetch suggestions of currently active datasource when initializes from visualization trigger', async () => {
- const props = {
- ...getDefaultProps(),
- initialContext: {
- indexPatternId: '1',
- fieldName: 'test',
- },
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- testDatasource2: mockDatasource2,
- },
-
- ExpressionRenderer: expressionRendererMock,
- };
- await mountWithProvider(, props.plugins.data);
-
- expect(mockDatasource.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalled();
- });
-
it('should fetch suggestions of currently active datasource', async () => {
const props = {
...getDefaultProps(),
@@ -963,7 +686,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- await mountWithProvider(, props.plugins.data);
+ await mountWithProvider(, { data: props.plugins.data });
expect(mockDatasource.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
expect(mockDatasource2.getDatasourceSuggestionsFromCurrentState).not.toHaveBeenCalled();
@@ -996,7 +719,7 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- await mountWithProvider(, props.plugins.data);
+ await mountWithProvider(, { data: props.plugins.data });
expect(mockVisualization.getSuggestions).toHaveBeenCalled();
expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
@@ -1064,10 +787,9 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- instance = (await mountWithProvider(, props.plugins.data)).instance;
+ instance = (await mountWithProvider(, { data: props.plugins.data }))
+ .instance;
- // TODO why is this necessary?
- instance.update();
expect(
instance
.find('[data-test-subj="lnsSuggestion"]')
@@ -1112,18 +834,16 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- instance = (await mountWithProvider(, props.plugins.data)).instance;
-
- // TODO why is this necessary?
- instance.update();
+ instance = (await mountWithProvider(, { data: props.plugins.data }))
+ .instance;
act(() => {
instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
});
// validation requires to calls this getConfiguration API
- expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(7);
- expect(mockVisualization.getConfiguration).toHaveBeenCalledWith(
+ expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(6);
+ expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith(
expect.objectContaining({
state: suggestionVisState,
})
@@ -1172,10 +892,8 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- instance = (await mountWithProvider(, props.plugins.data)).instance;
-
- // TODO why is this necessary?
- instance.update();
+ instance = (await mountWithProvider(, { data: props.plugins.data }))
+ .instance;
act(() => {
instance.find('[data-test-subj="lnsWorkspace"]').last().simulate('drop');
@@ -1191,7 +909,6 @@ describe('editor_frame', () => {
it('should use the currently selected visualization if possible on field drop', async () => {
mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']);
const suggestionVisState = {};
-
const props = {
...getDefaultProps(),
visualizationMap: {
@@ -1243,9 +960,21 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
} as EditorFrameProps;
- instance = (await mountWithProvider(, props.plugins.data)).instance;
- // TODO why is this necessary?
- instance.update();
+ instance = (
+ await mountWithProvider(, {
+ data: props.plugins.data,
+ preloadedState: {
+ datasourceStates: {
+ testDatasource: {
+ isLoading: false,
+ state: {
+ internalState1: '',
+ },
+ },
+ },
+ },
+ })
+ ).instance;
act(() => {
instance.find('[data-test-subj="mockVisA"]').find(DragDrop).prop('onDrop')!(
@@ -1345,10 +1074,11 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
} as EditorFrameProps;
- instance = (await mountWithProvider(, props.plugins.data)).instance;
-
- // TODO why is this necessary?
- instance.update();
+ instance = (
+ await mountWithProvider(, {
+ data: props.plugins.data,
+ })
+ ).instance;
act(() => {
instance.find(DragDrop).filter('[dataTestSubj="lnsWorkspace"]').prop('onDrop')!(
@@ -1389,32 +1119,21 @@ describe('editor_frame', () => {
ExpressionRenderer: expressionRendererMock,
};
- const { instance: el } = await mountWithProvider(
- ,
- props.plugins.data
- );
+ const { instance: el, lensStore } = await mountWithProvider(, {
+ data: props.plugins.data,
+ });
instance = el;
expect(
instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement
).not.toBeUndefined();
- await act(async () => {
- (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
- type: 'TOGGLE_FULLSCREEN',
- });
- });
-
+ lensStore.dispatch(setToggleFullscreen());
instance.update();
expect(instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement).toBe(false);
- await act(async () => {
- (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
- type: 'TOGGLE_FULLSCREEN',
- });
- });
-
+ lensStore.dispatch(setToggleFullscreen());
instance.update();
expect(
@@ -1422,211 +1141,4 @@ describe('editor_frame', () => {
).not.toBeUndefined();
});
});
-
- describe('passing state back to the caller', () => {
- let resolver: (value: unknown) => void;
- let instance: ReactWrapper;
-
- it('should call onChange only when the active datasource is finished loading', async () => {
- const onChange = jest.fn();
-
- mockDatasource.initialize.mockReturnValue(
- new Promise((resolve) => {
- resolver = resolve;
- })
- );
- mockDatasource.getLayers.mockReturnValue(['first']);
- mockDatasource.getPersistableState = jest.fn((x) => ({
- state: x,
- savedObjectReferences: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
- }));
- mockVisualization.initialize.mockReturnValue({ initialState: true });
-
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- },
-
- ExpressionRenderer: expressionRendererMock,
- onChange,
- };
-
- let lensStore: LensRootStore = {} as LensRootStore;
- await act(async () => {
- const mounted = await mountWithProvider(, props.plugins.data);
- lensStore = mounted.lensStore;
- expect(lensStore.dispatch).toHaveBeenCalledTimes(0);
- resolver({});
- });
-
- expect(lensStore.dispatch).toHaveBeenCalledTimes(2);
- expect(lensStore.dispatch).toHaveBeenNthCalledWith(1, {
- payload: {
- indexPatternsForTopNav: [{ id: '1' }],
- lastKnownDoc: {
- savedObjectId: undefined,
- description: undefined,
- references: [
- {
- id: '1',
- name: 'index-pattern-0',
- type: 'index-pattern',
- },
- ],
- state: {
- visualization: null, // Not yet loaded
- datasourceStates: { testDatasource: {} },
- query: { query: '', language: 'lucene' },
- filters: [],
- },
- title: '',
- type: 'lens',
- visualizationType: 'testVis',
- },
- },
- type: 'app/onChangeFromEditorFrame',
- });
- expect(lensStore.dispatch).toHaveBeenLastCalledWith({
- payload: {
- indexPatternsForTopNav: [{ id: '1' }],
- lastKnownDoc: {
- references: [
- {
- id: '1',
- name: 'index-pattern-0',
- type: 'index-pattern',
- },
- ],
- description: undefined,
- savedObjectId: undefined,
- state: {
- visualization: { initialState: true }, // Now loaded
- datasourceStates: { testDatasource: {} },
- query: { query: '', language: 'lucene' },
- filters: [],
- },
- title: '',
- type: 'lens',
- visualizationType: 'testVis',
- },
- },
- type: 'app/onChangeFromEditorFrame',
- });
- });
-
- it('should send back a persistable document when the state changes', async () => {
- const onChange = jest.fn();
-
- const initialState = { datasource: '' };
-
- mockDatasource.initialize.mockResolvedValue(initialState);
- mockDatasource.getLayers.mockReturnValue(['first']);
- mockVisualization.initialize.mockReturnValue({ initialState: true });
-
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- },
-
- ExpressionRenderer: expressionRendererMock,
- onChange,
- };
-
- const { instance: el, lensStore } = await mountWithProvider(
- ,
- props.plugins.data
- );
- instance = el;
-
- expect(lensStore.dispatch).toHaveBeenCalledTimes(2);
-
- mockDatasource.toExpression.mockReturnValue('data expression');
- mockVisualization.toExpression.mockReturnValue('vis expression');
- await act(async () => {
- lensStore.dispatch(setState({ query: { query: 'new query', language: 'lucene' } }));
- });
-
- instance.update();
-
- expect(lensStore.dispatch).toHaveBeenCalledTimes(4);
- expect(lensStore.dispatch).toHaveBeenNthCalledWith(3, {
- payload: {
- query: {
- language: 'lucene',
- query: 'new query',
- },
- },
- type: 'app/setState',
- });
- expect(lensStore.dispatch).toHaveBeenNthCalledWith(4, {
- payload: {
- lastKnownDoc: {
- savedObjectId: undefined,
- references: [],
- state: {
- datasourceStates: { testDatasource: { datasource: '' } },
- visualization: { initialState: true },
- query: { query: 'new query', language: 'lucene' },
- filters: [],
- },
- title: '',
- type: 'lens',
- visualizationType: 'testVis',
- },
- isSaveable: true,
- },
- type: 'app/onChangeFromEditorFrame',
- });
- });
-
- it('should call onChange when the datasource makes an internal state change', async () => {
- const onChange = jest.fn();
-
- mockDatasource.initialize.mockResolvedValue({});
- mockDatasource.getLayers.mockReturnValue(['first']);
- mockDatasource.getPersistableState = jest.fn((x) => ({
- state: x,
- savedObjectReferences: [{ type: 'index-pattern', id: '1', name: '' }],
- }));
- mockVisualization.initialize.mockReturnValue({ initialState: true });
-
- const props = {
- ...getDefaultProps(),
- visualizationMap: {
- testVis: mockVisualization,
- },
- datasourceMap: {
- testDatasource: mockDatasource,
- },
-
- ExpressionRenderer: expressionRendererMock,
- onChange,
- };
- const mounted = await mountWithProvider(, props.plugins.data);
- instance = mounted.instance;
- const { lensStore } = mounted;
-
- expect(lensStore.dispatch).toHaveBeenCalledTimes(2);
-
- await act(async () => {
- (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
- type: 'UPDATE_DATASOURCE_STATE',
- updater: () => ({
- newState: true,
- }),
- datasourceId: 'testDatasource',
- });
- });
-
- expect(lensStore.dispatch).toHaveBeenCalledTimes(3);
- });
- });
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
index bd96682f427fa..4b725c4cd1850 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
@@ -5,118 +5,53 @@
* 2.0.
*/
-import React, { useEffect, useReducer, useState, useCallback, useRef, useMemo } from 'react';
+import React, { useCallback, useRef, useMemo } from 'react';
import { CoreStart } from 'kibana/public';
-import { isEqual } from 'lodash';
-import { PaletteRegistry } from 'src/plugins/charts/public';
-import { IndexPattern } from '../../../../../../src/plugins/data/public';
-import { getAllIndexPatterns } from '../../utils';
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
import { Datasource, FramePublicAPI, Visualization } from '../../types';
-import { reducer, getInitialState } from './state_management';
import { DataPanelWrapper } from './data_panel_wrapper';
import { ConfigPanelWrapper } from './config_panel';
import { FrameLayout } from './frame_layout';
import { SuggestionPanel } from './suggestion_panel';
import { WorkspacePanel } from './workspace_panel';
-import { Document } from '../../persistence/saved_object_store';
import { DragDropIdentifier, RootDragDropProvider } from '../../drag_drop';
-import { getSavedObjectFormat } from './save';
-import { generateId } from '../../id_generator';
-import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
import { EditorFrameStartPlugins } from '../service';
-import { initializeDatasources, createDatasourceLayers } from './state_helpers';
-import {
- applyVisualizeFieldSuggestions,
- getTopSuggestionForField,
- switchToSuggestion,
- Suggestion,
-} from './suggestion_helpers';
+import { createDatasourceLayers } from './state_helpers';
+import { getTopSuggestionForField, switchToSuggestion, Suggestion } from './suggestion_helpers';
import { trackUiEvent } from '../../lens_ui_telemetry';
-import {
- useLensSelector,
- useLensDispatch,
- LensAppState,
- DispatchSetState,
- onChangeFromEditorFrame,
-} from '../../state_management';
+import { useLensSelector, useLensDispatch } from '../../state_management';
export interface EditorFrameProps {
datasourceMap: Record;
visualizationMap: Record;
ExpressionRenderer: ReactExpressionRendererType;
- palettes: PaletteRegistry;
- onError: (e: { message: string }) => void;
core: CoreStart;
plugins: EditorFrameStartPlugins;
showNoDataPopover: () => void;
- initialContext?: VisualizeFieldContext;
}
export function EditorFrame(props: EditorFrameProps) {
const {
- filters,
- searchSessionId,
- savedQuery,
- query,
- persistedDoc,
- indexPatternsForTopNav,
- lastKnownDoc,
activeData,
- isSaveable,
resolvedDateRange: dateRange,
- } = useLensSelector((state) => state.app);
- const [state, dispatch] = useReducer(reducer, { ...props, doc: persistedDoc }, getInitialState);
+ query,
+ filters,
+ searchSessionId,
+ activeDatasourceId,
+ visualization,
+ datasourceStates,
+ stagedPreview,
+ isFullscreenDatasource,
+ } = useLensSelector((state) => state.lens);
+
const dispatchLens = useLensDispatch();
- const dispatchChange: DispatchSetState = useCallback(
- (s: Partial) => dispatchLens(onChangeFromEditorFrame(s)),
- [dispatchLens]
- );
- const [visualizeTriggerFieldContext, setVisualizeTriggerFieldContext] = useState(
- props.initialContext
- );
- const { onError } = props;
- const activeVisualization =
- state.visualization.activeId && props.visualizationMap[state.visualization.activeId];
- const allLoaded = Object.values(state.datasourceStates).every(
- ({ isLoading }) => typeof isLoading === 'boolean' && !isLoading
- );
+ const allLoaded = Object.values(datasourceStates).every(({ isLoading }) => isLoading === false);
- // Initialize current datasource and all active datasources
- useEffect(
- () => {
- // prevents executing dispatch on unmounted component
- let isUnmounted = false;
- if (!allLoaded) {
- initializeDatasources(
- props.datasourceMap,
- state.datasourceStates,
- persistedDoc?.references,
- visualizeTriggerFieldContext,
- { isFullEditor: true }
- )
- .then((result) => {
- if (!isUnmounted) {
- Object.entries(result).forEach(([datasourceId, { state: datasourceState }]) => {
- dispatch({
- type: 'UPDATE_DATASOURCE_STATE',
- updater: datasourceState,
- datasourceId,
- });
- });
- }
- })
- .catch(onError);
- }
- return () => {
- isUnmounted = true;
- };
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [allLoaded, onError]
+ const datasourceLayers = React.useMemo(
+ () => createDatasourceLayers(props.datasourceMap, datasourceStates),
+ [props.datasourceMap, datasourceStates]
);
- const datasourceLayers = createDatasourceLayers(props.datasourceMap, state.datasourceStates);
const framePublicAPI: FramePublicAPI = useMemo(
() => ({
@@ -126,232 +61,15 @@ export function EditorFrame(props: EditorFrameProps) {
query,
filters,
searchSessionId,
- availablePalettes: props.palettes,
-
- addNewLayer() {
- const newLayerId = generateId();
-
- dispatch({
- type: 'UPDATE_LAYER',
- datasourceId: state.activeDatasourceId!,
- layerId: newLayerId,
- updater: props.datasourceMap[state.activeDatasourceId!].insertLayer,
- });
-
- return newLayerId;
- },
-
- removeLayers(layerIds: string[]) {
- if (activeVisualization && activeVisualization.removeLayer && state.visualization.state) {
- dispatch({
- type: 'UPDATE_VISUALIZATION_STATE',
- visualizationId: activeVisualization.id,
- updater: layerIds.reduce(
- (acc, layerId) =>
- activeVisualization.removeLayer
- ? activeVisualization.removeLayer(acc, layerId)
- : acc,
- state.visualization.state
- ),
- });
- }
-
- layerIds.forEach((layerId) => {
- const layerDatasourceId = Object.entries(props.datasourceMap).find(
- ([datasourceId, datasource]) =>
- state.datasourceStates[datasourceId] &&
- datasource.getLayers(state.datasourceStates[datasourceId].state).includes(layerId)
- )![0];
- dispatch({
- type: 'UPDATE_LAYER',
- layerId,
- datasourceId: layerDatasourceId,
- updater: props.datasourceMap[layerDatasourceId].removeLayer,
- });
- });
- },
}),
- [
- activeData,
- activeVisualization,
- datasourceLayers,
- dateRange,
- query,
- filters,
- searchSessionId,
- props.palettes,
- props.datasourceMap,
- state.activeDatasourceId,
- state.datasourceStates,
- state.visualization.state,
- ]
- );
-
- useEffect(
- () => {
- if (persistedDoc) {
- dispatch({
- type: 'VISUALIZATION_LOADED',
- doc: {
- ...persistedDoc,
- state: {
- ...persistedDoc.state,
- visualization: persistedDoc.visualizationType
- ? props.visualizationMap[persistedDoc.visualizationType].initialize(
- framePublicAPI,
- persistedDoc.state.visualization
- )
- : persistedDoc.state.visualization,
- },
- },
- });
- } else {
- dispatch({
- type: 'RESET',
- state: getInitialState(props),
- });
- }
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [persistedDoc]
- );
-
- // Initialize visualization as soon as all datasources are ready
- useEffect(
- () => {
- if (allLoaded && state.visualization.state === null && activeVisualization) {
- const initialVisualizationState = activeVisualization.initialize(framePublicAPI);
- dispatch({
- type: 'UPDATE_VISUALIZATION_STATE',
- visualizationId: activeVisualization.id,
- updater: initialVisualizationState,
- });
- }
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [allLoaded, activeVisualization, state.visualization.state]
- );
-
- // Get suggestions for visualize field when all datasources are ready
- useEffect(() => {
- if (allLoaded && visualizeTriggerFieldContext && !persistedDoc) {
- applyVisualizeFieldSuggestions({
- datasourceMap: props.datasourceMap,
- datasourceStates: state.datasourceStates,
- visualizationMap: props.visualizationMap,
- activeVisualizationId: state.visualization.activeId,
- visualizationState: state.visualization.state,
- visualizeTriggerFieldContext,
- dispatch,
- });
- setVisualizeTriggerFieldContext(undefined);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [allLoaded]);
-
- const getStateToUpdate: (
- arg: {
- filterableIndexPatterns: string[];
- doc: Document;
- isSaveable: boolean;
- },
- oldState: {
- isSaveable: boolean;
- indexPatternsForTopNav: IndexPattern[];
- persistedDoc?: Document;
- lastKnownDoc?: Document;
- }
- ) => Promise | undefined> = async (
- { filterableIndexPatterns, doc, isSaveable: incomingIsSaveable },
- prevState
- ) => {
- const batchedStateToUpdate: Partial = {};
-
- if (incomingIsSaveable !== prevState.isSaveable) {
- batchedStateToUpdate.isSaveable = incomingIsSaveable;
- }
-
- if (!isEqual(prevState.persistedDoc, doc) && !isEqual(prevState.lastKnownDoc, doc)) {
- batchedStateToUpdate.lastKnownDoc = doc;
- }
- const hasIndexPatternsChanged =
- prevState.indexPatternsForTopNav.length !== filterableIndexPatterns.length ||
- filterableIndexPatterns.some(
- (id) => !prevState.indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id)
- );
- // Update the cached index patterns if the user made a change to any of them
- if (hasIndexPatternsChanged) {
- const { indexPatterns } = await getAllIndexPatterns(
- filterableIndexPatterns,
- props.plugins.data.indexPatterns
- );
- if (indexPatterns) {
- batchedStateToUpdate.indexPatternsForTopNav = indexPatterns;
- }
- }
- if (Object.keys(batchedStateToUpdate).length) {
- return batchedStateToUpdate;
- }
- };
-
- // The frame needs to call onChange every time its internal state changes
- useEffect(
- () => {
- const activeDatasource =
- state.activeDatasourceId && !state.datasourceStates[state.activeDatasourceId].isLoading
- ? props.datasourceMap[state.activeDatasourceId]
- : undefined;
-
- if (!activeDatasource || !activeVisualization) {
- return;
- }
-
- const savedObjectFormat = getSavedObjectFormat({
- activeDatasources: Object.keys(state.datasourceStates).reduce(
- (datasourceMap, datasourceId) => ({
- ...datasourceMap,
- [datasourceId]: props.datasourceMap[datasourceId],
- }),
- {}
- ),
- visualization: activeVisualization,
- state,
- framePublicAPI,
- });
-
- // Frame loader (app or embeddable) is expected to call this when it loads and updates
- // This should be replaced with a top-down state
- getStateToUpdate(savedObjectFormat, {
- isSaveable,
- persistedDoc,
- indexPatternsForTopNav,
- lastKnownDoc,
- }).then((batchedStateToUpdate) => {
- if (batchedStateToUpdate) {
- dispatchChange(batchedStateToUpdate);
- }
- });
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [
- activeVisualization,
- state.datasourceStates,
- state.visualization,
- activeData,
- query,
- filters,
- savedQuery,
- state.title,
- dispatchChange,
- ]
+ [activeData, datasourceLayers, dateRange, query, filters, searchSessionId]
);
// Using a ref to prevent rerenders in the child components while keeping the latest state
const getSuggestionForField = useRef<(field: DragDropIdentifier) => Suggestion | undefined>();
getSuggestionForField.current = (field: DragDropIdentifier) => {
- const { activeDatasourceId, datasourceStates } = state;
- const activeVisualizationId = state.visualization.activeId;
- const visualizationState = state.visualization.state;
+ const activeVisualizationId = visualization.activeId;
+ const visualizationState = visualization.state;
const { visualizationMap, datasourceMap } = props;
if (!field || !activeDatasourceId) {
@@ -379,93 +97,77 @@ export function EditorFrame(props: EditorFrameProps) {
const suggestion = getSuggestionForField.current!(field);
if (suggestion) {
trackUiEvent('drop_onto_workspace');
- switchToSuggestion(dispatch, suggestion, 'SWITCH_VISUALIZATION');
+ switchToSuggestion(dispatchLens, suggestion, 'SWITCH_VISUALIZATION');
}
},
- [getSuggestionForField]
+ [getSuggestionForField, dispatchLens]
);
return (
}
configPanel={
allLoaded && (
)
}
workspacePanel={
allLoaded && (
)
}
suggestionsPanel={
allLoaded &&
- !state.isFullscreenDatasource && (
+ !isFullscreenDatasource && (
)
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.ts
index 66d83b1cd697f..8d4fb0683cb0c 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.ts
@@ -7,4 +7,3 @@
export * from './editor_frame';
export * from './state_helpers';
-export * from './state_management';
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts
deleted file mode 100644
index b0bff1800d32f..0000000000000
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { getSavedObjectFormat, Props } from './save';
-import { createMockDatasource, createMockFramePublicAPI, createMockVisualization } from '../mocks';
-import { esFilters, IIndexPattern, IFieldType } from '../../../../../../src/plugins/data/public';
-
-jest.mock('./expression_helpers');
-
-describe('save editor frame state', () => {
- const mockVisualization = createMockVisualization();
- const mockDatasource = createMockDatasource('a');
- const mockIndexPattern = ({ id: 'indexpattern' } as unknown) as IIndexPattern;
- const mockField = ({ name: '@timestamp' } as unknown) as IFieldType;
-
- mockDatasource.getPersistableState.mockImplementation((x) => ({
- state: x,
- savedObjectReferences: [],
- }));
- const saveArgs: Props = {
- activeDatasources: {
- indexpattern: mockDatasource,
- },
- visualization: mockVisualization,
- state: {
- title: 'aaa',
- datasourceStates: {
- indexpattern: {
- state: 'hello',
- isLoading: false,
- },
- },
- activeDatasourceId: 'indexpattern',
- visualization: { activeId: '2', state: {} },
- },
- framePublicAPI: {
- ...createMockFramePublicAPI(),
- addNewLayer: jest.fn(),
- removeLayers: jest.fn(),
- datasourceLayers: {
- first: mockDatasource.publicAPIMock,
- },
- query: { query: '', language: 'lucene' },
- dateRange: { fromDate: 'now-7d', toDate: 'now' },
- filters: [esFilters.buildExistsFilter(mockField, mockIndexPattern)],
- },
- };
-
- it('transforms from internal state to persisted doc format', async () => {
- const datasource = createMockDatasource('a');
- datasource.getPersistableState.mockImplementation((state) => ({
- state: {
- stuff: `${state}_datasource_persisted`,
- },
- savedObjectReferences: [],
- }));
- datasource.toExpression.mockReturnValue('my | expr');
-
- const visualization = createMockVisualization();
- visualization.toExpression.mockReturnValue('vis | expr');
-
- const { doc, filterableIndexPatterns, isSaveable } = await getSavedObjectFormat({
- ...saveArgs,
- activeDatasources: {
- indexpattern: datasource,
- },
- state: {
- title: 'bbb',
- datasourceStates: {
- indexpattern: {
- state: '2',
- isLoading: false,
- },
- },
- activeDatasourceId: 'indexpattern',
- visualization: { activeId: '3', state: '4' },
- },
- visualization,
- });
-
- expect(filterableIndexPatterns).toEqual([]);
- expect(isSaveable).toEqual(true);
- expect(doc).toEqual({
- id: undefined,
- state: {
- datasourceStates: {
- indexpattern: {
- stuff: '2_datasource_persisted',
- },
- },
- visualization: '4',
- query: { query: '', language: 'lucene' },
- filters: [
- {
- meta: { indexRefName: 'filter-index-pattern-0' },
- exists: { field: '@timestamp' },
- },
- ],
- },
- references: [
- {
- id: 'indexpattern',
- name: 'filter-index-pattern-0',
- type: 'index-pattern',
- },
- ],
- title: 'bbb',
- type: 'lens',
- visualizationType: '3',
- });
- });
-});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts
deleted file mode 100644
index 86a28be65d2b9..0000000000000
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { uniq } from 'lodash';
-import { SavedObjectReference } from 'kibana/public';
-import { EditorFrameState } from './state_management';
-import { Document } from '../../persistence/saved_object_store';
-import { Datasource, Visualization, FramePublicAPI } from '../../types';
-import { extractFilterReferences } from '../../persistence';
-import { buildExpression } from './expression_helpers';
-
-export interface Props {
- activeDatasources: Record;
- state: EditorFrameState;
- visualization: Visualization;
- framePublicAPI: FramePublicAPI;
-}
-
-export function getSavedObjectFormat({
- activeDatasources,
- state,
- visualization,
- framePublicAPI,
-}: Props): {
- doc: Document;
- filterableIndexPatterns: string[];
- isSaveable: boolean;
-} {
- const datasourceStates: Record = {};
- const references: SavedObjectReference[] = [];
- Object.entries(activeDatasources).forEach(([id, datasource]) => {
- const { state: persistableState, savedObjectReferences } = datasource.getPersistableState(
- state.datasourceStates[id].state
- );
- datasourceStates[id] = persistableState;
- references.push(...savedObjectReferences);
- });
-
- const uniqueFilterableIndexPatternIds = uniq(
- references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
- );
-
- const { persistableFilters, references: filterReferences } = extractFilterReferences(
- framePublicAPI.filters
- );
-
- references.push(...filterReferences);
-
- const expression = buildExpression({
- visualization,
- visualizationState: state.visualization.state,
- datasourceMap: activeDatasources,
- datasourceStates: state.datasourceStates,
- datasourceLayers: framePublicAPI.datasourceLayers,
- });
-
- return {
- doc: {
- savedObjectId: state.persistedId,
- title: state.title,
- description: state.description,
- type: 'lens',
- visualizationType: state.visualization.activeId,
- state: {
- datasourceStates,
- visualization: state.visualization.state,
- query: framePublicAPI.query,
- filters: persistableFilters,
- },
- references,
- },
- filterableIndexPatterns: uniqueFilterableIndexPatternIds,
- isSaveable: expression !== null,
- };
-}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
index dffb0e75f2109..e861112f3f7b4 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
@@ -19,7 +19,7 @@ import {
import { buildExpression } from './expression_helpers';
import { Document } from '../../persistence/saved_object_store';
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
-import { getActiveDatasourceIdFromDoc } from './state_management';
+import { getActiveDatasourceIdFromDoc } from '../../utils';
import { ErrorMessage } from '../types';
import {
getMissingCurrentDatasource,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts
deleted file mode 100644
index af8a9c0a85558..0000000000000
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts
+++ /dev/null
@@ -1,415 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { getInitialState, reducer } from './state_management';
-import { EditorFrameProps } from './index';
-import { Datasource, Visualization } from '../../types';
-import { createExpressionRendererMock } from '../mocks';
-import { coreMock } from 'src/core/public/mocks';
-import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks';
-import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
-import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks';
-import { chartPluginMock } from 'src/plugins/charts/public/mocks';
-
-describe('editor_frame state management', () => {
- describe('initialization', () => {
- let props: EditorFrameProps;
-
- beforeEach(() => {
- props = {
- onError: jest.fn(),
- datasourceMap: { testDatasource: ({} as unknown) as Datasource },
- visualizationMap: { testVis: ({ initialize: jest.fn() } as unknown) as Visualization },
- ExpressionRenderer: createExpressionRendererMock(),
- core: coreMock.createStart(),
- plugins: {
- uiActions: uiActionsPluginMock.createStartContract(),
- data: dataPluginMock.createStartContract(),
- expressions: expressionsPluginMock.createStartContract(),
- charts: chartPluginMock.createStartContract(),
- },
- palettes: chartPluginMock.createPaletteRegistry(),
- showNoDataPopover: jest.fn(),
- };
- });
-
- it('should store initial datasource and visualization', () => {
- const initialState = getInitialState(props);
- expect(initialState.activeDatasourceId).toEqual('testDatasource');
- expect(initialState.visualization.activeId).toEqual('testVis');
- });
-
- it('should not initialize visualization but set active id', () => {
- const initialState = getInitialState(props);
-
- expect(initialState.visualization.state).toBe(null);
- expect(initialState.visualization.activeId).toBe('testVis');
- expect(props.visualizationMap.testVis.initialize).not.toHaveBeenCalled();
- });
-
- it('should prefill state if doc is passed in', () => {
- const initialState = getInitialState({
- ...props,
- doc: {
- state: {
- datasourceStates: {
- testDatasource: { internalState1: '' },
- testDatasource2: { internalState2: '' },
- },
- visualization: {},
- query: { query: '', language: 'lucene' },
- filters: [],
- },
- references: [],
- title: '',
- visualizationType: 'testVis',
- },
- });
-
- expect(initialState.datasourceStates).toMatchInlineSnapshot(`
- Object {
- "testDatasource": Object {
- "isLoading": true,
- "state": Object {
- "internalState1": "",
- },
- },
- "testDatasource2": Object {
- "isLoading": true,
- "state": Object {
- "internalState2": "",
- },
- },
- }
- `);
- expect(initialState.visualization).toMatchInlineSnapshot(`
- Object {
- "activeId": "testVis",
- "state": null,
- }
- `);
- });
-
- it('should not set active id if initiated with empty document and visualizationMap is empty', () => {
- const initialState = getInitialState({ ...props, visualizationMap: {} });
-
- expect(initialState.visualization.state).toEqual(null);
- expect(initialState.visualization.activeId).toEqual(null);
- expect(props.visualizationMap.testVis.initialize).not.toHaveBeenCalled();
- });
- });
-
- describe('state update', () => {
- it('should update the corresponding visualization state on update', () => {
- const newVisState = {};
- const newState = reducer(
- {
- datasourceStates: {
- testDatasource: {
- state: {},
- isLoading: false,
- },
- },
- activeDatasourceId: 'testDatasource',
- title: 'aaa',
- visualization: {
- activeId: 'testVis',
- state: {},
- },
- },
- {
- type: 'UPDATE_VISUALIZATION_STATE',
- visualizationId: 'testVis',
- updater: newVisState,
- }
- );
-
- expect(newState.visualization.state).toBe(newVisState);
- });
-
- it('should update the datasource state with passed in reducer', () => {
- const datasourceReducer = jest.fn(() => ({ changed: true }));
- const newState = reducer(
- {
- datasourceStates: {
- testDatasource: {
- state: {},
- isLoading: false,
- },
- },
- activeDatasourceId: 'testDatasource',
- title: 'bbb',
- visualization: {
- activeId: 'testVis',
- state: {},
- },
- },
- {
- type: 'UPDATE_DATASOURCE_STATE',
- updater: datasourceReducer,
- datasourceId: 'testDatasource',
- }
- );
-
- expect(newState.datasourceStates.testDatasource.state).toEqual({ changed: true });
- expect(datasourceReducer).toHaveBeenCalledTimes(1);
- });
-
- it('should update the layer state with passed in reducer', () => {
- const newDatasourceState = {};
- const newState = reducer(
- {
- datasourceStates: {
- testDatasource: {
- state: {},
- isLoading: false,
- },
- },
- activeDatasourceId: 'testDatasource',
- title: 'bbb',
- visualization: {
- activeId: 'testVis',
- state: {},
- },
- },
- {
- type: 'UPDATE_DATASOURCE_STATE',
- updater: newDatasourceState,
- datasourceId: 'testDatasource',
- }
- );
-
- expect(newState.datasourceStates.testDatasource.state).toBe(newDatasourceState);
- });
-
- it('should should switch active visualization', () => {
- const testVisState = {};
- const newVisState = {};
- const newState = reducer(
- {
- datasourceStates: {
- testDatasource: {
- state: {},
- isLoading: false,
- },
- },
- activeDatasourceId: 'testDatasource',
- title: 'ccc',
- visualization: {
- activeId: 'testVis',
- state: testVisState,
- },
- },
- {
- type: 'SWITCH_VISUALIZATION',
- newVisualizationId: 'testVis2',
- initialState: newVisState,
- }
- );
-
- expect(newState.visualization.state).toBe(newVisState);
- });
-
- it('should should switch active visualization and update datasource state', () => {
- const testVisState = {};
- const newVisState = {};
- const newDatasourceState = {};
- const newState = reducer(
- {
- datasourceStates: {
- testDatasource: {
- state: {},
- isLoading: false,
- },
- },
- activeDatasourceId: 'testDatasource',
- title: 'ddd',
- visualization: {
- activeId: 'testVis',
- state: testVisState,
- },
- },
- {
- type: 'SWITCH_VISUALIZATION',
- newVisualizationId: 'testVis2',
- initialState: newVisState,
- datasourceState: newDatasourceState,
- datasourceId: 'testDatasource',
- }
- );
-
- expect(newState.visualization.state).toBe(newVisState);
- expect(newState.datasourceStates.testDatasource.state).toBe(newDatasourceState);
- });
-
- it('should should switch active datasource and initialize new state', () => {
- const newState = reducer(
- {
- datasourceStates: {
- testDatasource: {
- state: {},
- isLoading: false,
- },
- },
- activeDatasourceId: 'testDatasource',
- title: 'eee',
- visualization: {
- activeId: 'testVis',
- state: {},
- },
- },
- {
- type: 'SWITCH_DATASOURCE',
- newDatasourceId: 'testDatasource2',
- }
- );
-
- expect(newState.activeDatasourceId).toEqual('testDatasource2');
- expect(newState.datasourceStates.testDatasource2.isLoading).toEqual(true);
- });
-
- it('not initialize already initialized datasource on switch', () => {
- const datasource2State = {};
- const newState = reducer(
- {
- datasourceStates: {
- testDatasource: {
- state: {},
- isLoading: false,
- },
- testDatasource2: {
- state: datasource2State,
- isLoading: false,
- },
- },
- activeDatasourceId: 'testDatasource',
- title: 'eee',
- visualization: {
- activeId: 'testVis',
- state: {},
- },
- },
- {
- type: 'SWITCH_DATASOURCE',
- newDatasourceId: 'testDatasource2',
- }
- );
-
- expect(newState.activeDatasourceId).toEqual('testDatasource2');
- expect(newState.datasourceStates.testDatasource2.state).toBe(datasource2State);
- });
-
- it('should reset the state', () => {
- const newState = reducer(
- {
- datasourceStates: {
- a: {
- state: {},
- isLoading: false,
- },
- },
- activeDatasourceId: 'a',
- title: 'jjj',
- visualization: {
- activeId: 'b',
- state: {},
- },
- },
- {
- type: 'RESET',
- state: {
- datasourceStates: {
- z: {
- isLoading: false,
- state: { hola: 'muchacho' },
- },
- },
- activeDatasourceId: 'z',
- persistedId: 'bar',
- title: 'lll',
- visualization: {
- activeId: 'q',
- state: { my: 'viz' },
- },
- },
- }
- );
-
- expect(newState).toMatchObject({
- datasourceStates: {
- z: {
- isLoading: false,
- state: { hola: 'muchacho' },
- },
- },
- activeDatasourceId: 'z',
- persistedId: 'bar',
- visualization: {
- activeId: 'q',
- state: { my: 'viz' },
- },
- });
- });
-
- it('should load the state from the doc', () => {
- const newState = reducer(
- {
- datasourceStates: {
- a: {
- state: {},
- isLoading: false,
- },
- },
- activeDatasourceId: 'a',
- title: 'mmm',
- visualization: {
- activeId: 'b',
- state: {},
- },
- },
- {
- type: 'VISUALIZATION_LOADED',
- doc: {
- savedObjectId: 'b',
- state: {
- datasourceStates: { a: { foo: 'c' } },
- visualization: { bar: 'd' },
- query: { query: '', language: 'lucene' },
- filters: [],
- },
- title: 'heyo!',
- description: 'My lens',
- type: 'lens',
- visualizationType: 'line',
- references: [],
- },
- }
- );
-
- expect(newState).toEqual({
- activeDatasourceId: 'a',
- datasourceStates: {
- a: {
- isLoading: true,
- state: {
- foo: 'c',
- },
- },
- },
- persistedId: 'b',
- title: 'heyo!',
- description: 'My lens',
- visualization: {
- activeId: 'line',
- state: {
- bar: 'd',
- },
- },
- });
- });
- });
-});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts
deleted file mode 100644
index a87aa7a2cb428..0000000000000
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { EditorFrameProps } from './index';
-import { Document } from '../../persistence/saved_object_store';
-
-export interface PreviewState {
- visualization: {
- activeId: string | null;
- state: unknown;
- };
- datasourceStates: Record;
-}
-
-export interface EditorFrameState extends PreviewState {
- persistedId?: string;
- title: string;
- description?: string;
- stagedPreview?: PreviewState;
- activeDatasourceId: string | null;
- isFullscreenDatasource?: boolean;
-}
-
-export type Action =
- | {
- type: 'RESET';
- state: EditorFrameState;
- }
- | {
- type: 'UPDATE_TITLE';
- title: string;
- }
- | {
- type: 'UPDATE_STATE';
- // Just for diagnostics, so we can determine what action
- // caused this update.
- subType: string;
- updater: (prevState: EditorFrameState) => EditorFrameState;
- }
- | {
- type: 'UPDATE_DATASOURCE_STATE';
- updater: unknown | ((prevState: unknown) => unknown);
- datasourceId: string;
- clearStagedPreview?: boolean;
- }
- | {
- type: 'UPDATE_VISUALIZATION_STATE';
- visualizationId: string;
- updater: unknown | ((state: unknown) => unknown);
- clearStagedPreview?: boolean;
- }
- | {
- type: 'UPDATE_LAYER';
- layerId: string;
- datasourceId: string;
- updater: (state: unknown, layerId: string) => unknown;
- }
- | {
- type: 'VISUALIZATION_LOADED';
- doc: Document;
- }
- | {
- type: 'SWITCH_VISUALIZATION';
- newVisualizationId: string;
- initialState: unknown;
- }
- | {
- type: 'SWITCH_VISUALIZATION';
- newVisualizationId: string;
- initialState: unknown;
- datasourceState: unknown;
- datasourceId: string;
- }
- | {
- type: 'SELECT_SUGGESTION';
- newVisualizationId: string;
- initialState: unknown;
- datasourceState: unknown;
- datasourceId: string;
- }
- | {
- type: 'ROLLBACK_SUGGESTION';
- }
- | {
- type: 'SUBMIT_SUGGESTION';
- }
- | {
- type: 'SWITCH_DATASOURCE';
- newDatasourceId: string;
- }
- | {
- type: 'TOGGLE_FULLSCREEN';
- };
-
-export function getActiveDatasourceIdFromDoc(doc?: Document) {
- if (!doc) {
- return null;
- }
-
- const [firstDatasourceFromDoc] = Object.keys(doc.state.datasourceStates);
- return firstDatasourceFromDoc || null;
-}
-
-export const getInitialState = (
- params: EditorFrameProps & { doc?: Document }
-): EditorFrameState => {
- const datasourceStates: EditorFrameState['datasourceStates'] = {};
-
- const initialDatasourceId =
- getActiveDatasourceIdFromDoc(params.doc) || Object.keys(params.datasourceMap)[0] || null;
-
- const initialVisualizationId =
- (params.doc && params.doc.visualizationType) || Object.keys(params.visualizationMap)[0] || null;
-
- if (params.doc) {
- Object.entries(params.doc.state.datasourceStates).forEach(([datasourceId, state]) => {
- datasourceStates[datasourceId] = { isLoading: true, state };
- });
- } else if (initialDatasourceId) {
- datasourceStates[initialDatasourceId] = {
- state: null,
- isLoading: true,
- };
- }
-
- return {
- title: '',
- datasourceStates,
- activeDatasourceId: initialDatasourceId,
- visualization: {
- state: null,
- activeId: initialVisualizationId,
- },
- };
-};
-
-export const reducer = (state: EditorFrameState, action: Action): EditorFrameState => {
- switch (action.type) {
- case 'RESET':
- return action.state;
- case 'UPDATE_TITLE':
- return { ...state, title: action.title };
- case 'UPDATE_STATE':
- return action.updater(state);
- case 'UPDATE_LAYER':
- return {
- ...state,
- datasourceStates: {
- ...state.datasourceStates,
- [action.datasourceId]: {
- ...state.datasourceStates[action.datasourceId],
- state: action.updater(
- state.datasourceStates[action.datasourceId].state,
- action.layerId
- ),
- },
- },
- };
- case 'VISUALIZATION_LOADED':
- return {
- ...state,
- persistedId: action.doc.savedObjectId,
- title: action.doc.title,
- description: action.doc.description,
- datasourceStates: Object.entries(action.doc.state.datasourceStates).reduce(
- (stateMap, [datasourceId, datasourceState]) => ({
- ...stateMap,
- [datasourceId]: {
- isLoading: true,
- state: datasourceState,
- },
- }),
- {}
- ),
- activeDatasourceId: getActiveDatasourceIdFromDoc(action.doc),
- visualization: {
- ...state.visualization,
- activeId: action.doc.visualizationType,
- state: action.doc.state.visualization,
- },
- };
- case 'SWITCH_DATASOURCE':
- return {
- ...state,
- datasourceStates: {
- ...state.datasourceStates,
- [action.newDatasourceId]: state.datasourceStates[action.newDatasourceId] || {
- state: null,
- isLoading: true,
- },
- },
- activeDatasourceId: action.newDatasourceId,
- };
- case 'SWITCH_VISUALIZATION':
- return {
- ...state,
- datasourceStates:
- 'datasourceId' in action && action.datasourceId
- ? {
- ...state.datasourceStates,
- [action.datasourceId]: {
- ...state.datasourceStates[action.datasourceId],
- state: action.datasourceState,
- },
- }
- : state.datasourceStates,
- visualization: {
- ...state.visualization,
- activeId: action.newVisualizationId,
- state: action.initialState,
- },
- stagedPreview: undefined,
- };
- case 'SELECT_SUGGESTION':
- return {
- ...state,
- datasourceStates:
- 'datasourceId' in action && action.datasourceId
- ? {
- ...state.datasourceStates,
- [action.datasourceId]: {
- ...state.datasourceStates[action.datasourceId],
- state: action.datasourceState,
- },
- }
- : state.datasourceStates,
- visualization: {
- ...state.visualization,
- activeId: action.newVisualizationId,
- state: action.initialState,
- },
- stagedPreview: state.stagedPreview || {
- datasourceStates: state.datasourceStates,
- visualization: state.visualization,
- },
- };
- case 'ROLLBACK_SUGGESTION':
- return {
- ...state,
- ...(state.stagedPreview || {}),
- stagedPreview: undefined,
- };
- case 'SUBMIT_SUGGESTION':
- return {
- ...state,
- stagedPreview: undefined,
- };
- case 'UPDATE_DATASOURCE_STATE':
- return {
- ...state,
- datasourceStates: {
- ...state.datasourceStates,
- [action.datasourceId]: {
- state:
- typeof action.updater === 'function'
- ? action.updater(state.datasourceStates[action.datasourceId].state)
- : action.updater,
- isLoading: false,
- },
- },
- stagedPreview: action.clearStagedPreview ? undefined : state.stagedPreview,
- };
- case 'UPDATE_VISUALIZATION_STATE':
- if (!state.visualization.activeId) {
- throw new Error('Invariant: visualization state got updated without active visualization');
- }
- // This is a safeguard that prevents us from accidentally updating the
- // wrong visualization. This occurs in some cases due to the uncoordinated
- // way we manage state across plugins.
- if (state.visualization.activeId !== action.visualizationId) {
- return state;
- }
- return {
- ...state,
- visualization: {
- ...state.visualization,
- state:
- typeof action.updater === 'function'
- ? action.updater(state.visualization.state)
- : action.updater,
- },
- stagedPreview: action.clearStagedPreview ? undefined : state.stagedPreview,
- };
- case 'TOGGLE_FULLSCREEN':
- return { ...state, isFullscreenDatasource: !state.isFullscreenDatasource };
- default:
- return state;
- }
-};
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts
index 0e8c9b962b995..6f33cc4b8aab8 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts
@@ -6,7 +6,7 @@
*/
import { getSuggestions, getTopSuggestionForField } from './suggestion_helpers';
-import { createMockVisualization, createMockDatasource, DatasourceMock } from '../mocks';
+import { createMockVisualization, createMockDatasource, DatasourceMock } from '../../mocks';
import { TableSuggestion, DatasourceSuggestion, Visualization } from '../../types';
import { PaletteOutput } from 'src/plugins/charts/public';
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
index bd8f134f59fbb..9fdc283c3cc29 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
@@ -19,8 +19,8 @@ import {
DatasourceSuggestion,
DatasourcePublicAPI,
} from '../../types';
-import { Action } from './state_management';
import { DragDropIdentifier } from '../../drag_drop';
+import { LensDispatch, selectSuggestion, switchVisualization } from '../../state_management';
export interface Suggestion {
visualizationId: string;
@@ -132,14 +132,13 @@ export function getSuggestions({
).sort((a, b) => b.score - a.score);
}
-export function applyVisualizeFieldSuggestions({
+export function getVisualizeFieldSuggestions({
datasourceMap,
datasourceStates,
visualizationMap,
activeVisualizationId,
visualizationState,
visualizeTriggerFieldContext,
- dispatch,
}: {
datasourceMap: Record;
datasourceStates: Record<
@@ -154,8 +153,7 @@ export function applyVisualizeFieldSuggestions({
subVisualizationId?: string;
visualizationState: unknown;
visualizeTriggerFieldContext?: VisualizeFieldContext;
- dispatch: (action: Action) => void;
-}): void {
+}): Suggestion | undefined {
const suggestions = getSuggestions({
datasourceMap,
datasourceStates,
@@ -165,9 +163,7 @@ export function applyVisualizeFieldSuggestions({
visualizeTriggerFieldContext,
});
if (suggestions.length) {
- const selectedSuggestion =
- suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0];
- switchToSuggestion(dispatch, selectedSuggestion, 'SWITCH_VISUALIZATION');
+ return suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0];
}
}
@@ -207,22 +203,25 @@ function getVisualizationSuggestions(
}
export function switchToSuggestion(
- dispatch: (action: Action) => void,
+ dispatchLens: LensDispatch,
suggestion: Pick<
Suggestion,
'visualizationId' | 'visualizationState' | 'datasourceState' | 'datasourceId'
>,
type: 'SWITCH_VISUALIZATION' | 'SELECT_SUGGESTION' = 'SELECT_SUGGESTION'
) {
- const action: Action = {
- type,
+ const pickedSuggestion = {
newVisualizationId: suggestion.visualizationId,
initialState: suggestion.visualizationState,
datasourceState: suggestion.datasourceState,
datasourceId: suggestion.datasourceId!,
};
- dispatch(action);
+ dispatchLens(
+ type === 'SELECT_SUGGESTION'
+ ? selectSuggestion(pickedSuggestion)
+ : switchVisualization(pickedSuggestion)
+ );
}
export function getTopSuggestionForField(
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
index 2b755a2e8bf08..6445038e40d7c 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
@@ -6,7 +6,6 @@
*/
import React from 'react';
-import { mountWithIntl as mount } from '@kbn/test/jest';
import { Visualization } from '../../types';
import {
createMockVisualization,
@@ -14,15 +13,15 @@ import {
createExpressionRendererMock,
DatasourceMock,
createMockFramePublicAPI,
-} from '../mocks';
+} from '../../mocks';
import { act } from 'react-dom/test-utils';
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
import { esFilters, IFieldType, IIndexPattern } from '../../../../../../src/plugins/data/public';
import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel';
import { getSuggestions, Suggestion } from './suggestion_helpers';
import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui';
-import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
import { LensIconChartDatatable } from '../../assets/chart_datatable';
+import { mountWithProvider } from '../../mocks';
jest.mock('./suggestion_helpers');
@@ -33,7 +32,6 @@ describe('suggestion_panel', () => {
let mockDatasource: DatasourceMock;
let expressionRendererMock: ReactExpressionRendererType;
- let dispatchMock: jest.Mock;
const suggestion1State = { suggestion1: true };
const suggestion2State = { suggestion2: true };
@@ -44,7 +42,6 @@ describe('suggestion_panel', () => {
mockVisualization = createMockVisualization();
mockDatasource = createMockDatasource('a');
expressionRendererMock = createExpressionRendererMock();
- dispatchMock = jest.fn();
getSuggestionsMock.mockReturnValue([
{
@@ -84,18 +81,16 @@ describe('suggestion_panel', () => {
vis2: createMockVisualization(),
},
visualizationState: {},
- dispatch: dispatchMock,
ExpressionRenderer: expressionRendererMock,
frame: createMockFramePublicAPI(),
- plugins: { data: dataPluginMock.createStartContract() },
};
});
- it('should list passed in suggestions', () => {
- const wrapper = mount();
+ it('should list passed in suggestions', async () => {
+ const { instance } = await mountWithProvider();
expect(
- wrapper
+ instance
.find('[data-test-subj="lnsSuggestion"]')
.find(EuiPanel)
.map((el) => el.parents(EuiToolTip).prop('content'))
@@ -129,90 +124,97 @@ describe('suggestion_panel', () => {
};
});
- it('should not update suggestions if current state is moved to staged preview', () => {
- const wrapper = mount();
+ it('should not update suggestions if current state is moved to staged preview', async () => {
+ const { instance } = await mountWithProvider();
getSuggestionsMock.mockClear();
- wrapper.setProps({
+ instance.setProps({
stagedPreview,
...suggestionState,
});
- wrapper.update();
+ instance.update();
expect(getSuggestionsMock).not.toHaveBeenCalled();
});
- it('should update suggestions if staged preview is removed', () => {
- const wrapper = mount();
+ it('should update suggestions if staged preview is removed', async () => {
+ const { instance } = await mountWithProvider();
getSuggestionsMock.mockClear();
- wrapper.setProps({
+ instance.setProps({
stagedPreview,
...suggestionState,
});
- wrapper.update();
- wrapper.setProps({
+ instance.update();
+ instance.setProps({
stagedPreview: undefined,
...suggestionState,
});
- wrapper.update();
+ instance.update();
expect(getSuggestionsMock).toHaveBeenCalledTimes(1);
});
- it('should highlight currently active suggestion', () => {
- const wrapper = mount();
+ it('should highlight currently active suggestion', async () => {
+ const { instance } = await mountWithProvider();
act(() => {
- wrapper.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
+ instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
});
- wrapper.update();
+ instance.update();
- expect(wrapper.find('[data-test-subj="lnsSuggestion"]').at(2).prop('className')).toContain(
+ expect(instance.find('[data-test-subj="lnsSuggestion"]').at(2).prop('className')).toContain(
'lnsSuggestionPanel__button-isSelected'
);
});
- it('should rollback suggestion if current panel is clicked', () => {
- const wrapper = mount();
+ it('should rollback suggestion if current panel is clicked', async () => {
+ const { instance, lensStore } = await mountWithProvider(
+
+ );
act(() => {
- wrapper.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
+ instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
});
- wrapper.update();
+ instance.update();
act(() => {
- wrapper.find('[data-test-subj="lnsSuggestion"]').at(0).simulate('click');
+ instance.find('[data-test-subj="lnsSuggestion"]').at(0).simulate('click');
});
- wrapper.update();
+ instance.update();
- expect(dispatchMock).toHaveBeenCalledWith({
- type: 'ROLLBACK_SUGGESTION',
+ expect(lensStore.dispatch).toHaveBeenCalledWith({
+ type: 'lens/rollbackSuggestion',
});
});
});
- it('should dispatch visualization switch action if suggestion is clicked', () => {
- const wrapper = mount();
+ it('should dispatch visualization switch action if suggestion is clicked', async () => {
+ const { instance, lensStore } = await mountWithProvider();
act(() => {
- wrapper.find('button[data-test-subj="lnsSuggestion"]').at(1).simulate('click');
+ instance.find('button[data-test-subj="lnsSuggestion"]').at(1).simulate('click');
});
- wrapper.update();
+ instance.update();
- expect(dispatchMock).toHaveBeenCalledWith(
+ expect(lensStore.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
- type: 'SELECT_SUGGESTION',
- initialState: suggestion1State,
+ type: 'lens/selectSuggestion',
+ payload: {
+ datasourceId: undefined,
+ datasourceState: {},
+ initialState: { suggestion1: true },
+ newVisualizationId: 'vis',
+ },
})
);
});
- it('should render preview expression if there is one', () => {
+ it('should render render icon if there is no preview expression', async () => {
mockDatasource.getLayers.mockReturnValue(['first']);
- (getSuggestions as jest.Mock).mockReturnValue([
+ getSuggestionsMock.mockReturnValue([
{
datasourceState: {},
- previewIcon: 'empty',
+ previewIcon: LensIconChartDatatable,
score: 0.5,
visualizationState: suggestion1State,
visualizationId: 'vis',
@@ -225,43 +227,51 @@ describe('suggestion_panel', () => {
visualizationState: suggestion2State,
visualizationId: 'vis',
title: 'Suggestion2',
+ previewExpression: 'test | expression',
},
] as Suggestion[]);
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce(undefined);
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression');
+
+ // this call will go to the currently active visualization
+ (mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('current | preview');
+
mockDatasource.toExpression.mockReturnValue('datasource_expression');
- const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
- const field = ({ name: 'myfield' } as unknown) as IFieldType;
+ const { instance } = await mountWithProvider();
- mount(
-
- );
+ expect(instance.find(EuiIcon)).toHaveLength(1);
+ expect(instance.find(EuiIcon).prop('type')).toEqual(LensIconChartDatatable);
+ });
- expect(expressionRendererMock).toHaveBeenCalledTimes(1);
- const passedExpression = (expressionRendererMock as jest.Mock).mock.calls[0][0].expression;
+ it('should return no suggestion if visualization has missing index-patterns', async () => {
+ // create a layer that is referencing an indexPatterns not retrieved by the datasource
+ const missingIndexPatternsState = {
+ layers: { indexPatternId: 'a' },
+ indexPatterns: {},
+ };
+ mockDatasource.checkIntegrity.mockReturnValue(['a']);
+ const newProps = {
+ ...defaultProps,
+ datasourceStates: {
+ mock: {
+ ...defaultProps.datasourceStates.mock,
+ state: missingIndexPatternsState,
+ },
+ },
+ };
- expect(passedExpression).toMatchInlineSnapshot(`
- "kibana
- | lens_merge_tables layerIds=\\"first\\" tables={datasource_expression}
- | test
- | expression"
- `);
+ const { instance } = await mountWithProvider();
+ expect(instance.html()).toEqual(null);
});
- it('should render render icon if there is no preview expression', () => {
+ it('should render preview expression if there is one', () => {
mockDatasource.getLayers.mockReturnValue(['first']);
- getSuggestionsMock.mockReturnValue([
+ (getSuggestions as jest.Mock).mockReturnValue([
{
datasourceState: {},
- previewIcon: LensIconChartDatatable,
+ previewIcon: 'empty',
score: 0.5,
visualizationState: suggestion1State,
visualizationId: 'vis',
@@ -274,41 +284,34 @@ describe('suggestion_panel', () => {
visualizationState: suggestion2State,
visualizationId: 'vis',
title: 'Suggestion2',
- previewExpression: 'test | expression',
},
] as Suggestion[]);
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce(undefined);
(mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression');
-
- // this call will go to the currently active visualization
- (mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('current | preview');
-
mockDatasource.toExpression.mockReturnValue('datasource_expression');
- const wrapper = mount();
+ const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
+ const field = ({ name: 'myfield' } as unknown) as IFieldType;
- expect(wrapper.find(EuiIcon)).toHaveLength(1);
- expect(wrapper.find(EuiIcon).prop('type')).toEqual(LensIconChartDatatable);
- });
+ mountWithProvider(
+
+ );
- it('should return no suggestion if visualization has missing index-patterns', () => {
- // create a layer that is referencing an indexPatterns not retrieved by the datasource
- const missingIndexPatternsState = {
- layers: { indexPatternId: 'a' },
- indexPatterns: {},
- };
- mockDatasource.checkIntegrity.mockReturnValue(['a']);
- const newProps = {
- ...defaultProps,
- datasourceStates: {
- mock: {
- ...defaultProps.datasourceStates.mock,
- state: missingIndexPatternsState,
- },
- },
- };
- const wrapper = mount();
- expect(wrapper.html()).toEqual(null);
+ expect(expressionRendererMock).toHaveBeenCalledTimes(1);
+ const passedExpression = (expressionRendererMock as jest.Mock).mock.calls[0][0].expression;
+
+ expect(passedExpression).toMatchInlineSnapshot(`
+ "kibana
+ | lens_merge_tables layerIds=\\"first\\" tables={datasource_expression}
+ | test
+ | expression"
+ `);
});
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
index 8107b6646500d..6d360a09a5b49 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
@@ -24,8 +24,7 @@ import { IconType } from '@elastic/eui/src/components/icon/icon';
import { Ast, toExpression } from '@kbn/interpreter/common';
import { i18n } from '@kbn/i18n';
import classNames from 'classnames';
-import { DataPublicPluginStart, ExecutionContextSearch } from 'src/plugins/data/public';
-import { Action, PreviewState } from './state_management';
+import { ExecutionContextSearch } from 'src/plugins/data/public';
import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types';
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
import {
@@ -35,6 +34,12 @@ import {
import { prependDatasourceExpression } from './expression_helpers';
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';
import { getMissingIndexPattern, validateDatasourceAndVisualization } from './state_helpers';
+import {
+ PreviewState,
+ rollbackSuggestion,
+ submitSuggestion,
+ useLensDispatch,
+} from '../../state_management';
const MAX_SUGGESTIONS_DISPLAYED = 5;
@@ -51,11 +56,9 @@ export interface SuggestionPanelProps {
activeVisualizationId: string | null;
visualizationMap: Record;
visualizationState: unknown;
- dispatch: (action: Action) => void;
ExpressionRenderer: ReactExpressionRendererType;
frame: FramePublicAPI;
stagedPreview?: PreviewState;
- plugins: { data: DataPublicPluginStart };
}
const PreviewRenderer = ({
@@ -170,12 +173,12 @@ export function SuggestionPanel({
activeVisualizationId,
visualizationMap,
visualizationState,
- dispatch,
frame,
ExpressionRenderer: ExpressionRendererComponent,
stagedPreview,
- plugins,
}: SuggestionPanelProps) {
+ const dispatchLens = useLensDispatch();
+
const currentDatasourceStates = stagedPreview ? stagedPreview.datasourceStates : datasourceStates;
const currentVisualizationState = stagedPreview
? stagedPreview.visualization.state
@@ -320,9 +323,7 @@ export function SuggestionPanel({
if (lastSelectedSuggestion !== -1) {
trackSuggestionEvent('back_to_current');
setLastSelectedSuggestion(-1);
- dispatch({
- type: 'ROLLBACK_SUGGESTION',
- });
+ dispatchLens(rollbackSuggestion());
}
}
@@ -352,9 +353,7 @@ export function SuggestionPanel({
iconType="refresh"
onClick={() => {
trackUiEvent('suggestion_confirmed');
- dispatch({
- type: 'SUBMIT_SUGGESTION',
- });
+ dispatchLens(submitSuggestion());
}}
>
{i18n.translate('xpack.lens.sugegstion.refreshSuggestionLabel', {
@@ -401,7 +400,7 @@ export function SuggestionPanel({
rollbackToCurrentVisualization();
} else {
setLastSelectedSuggestion(index);
- switchToSuggestion(dispatch, suggestion);
+ switchToSuggestion(dispatchLens, suggestion);
}
}}
selected={index === lastSelectedSuggestion}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx
index 46e287297828d..9b5766c3e3bfa 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx
@@ -11,7 +11,8 @@ import {
createMockVisualization,
createMockFramePublicAPI,
createMockDatasource,
-} from '../../mocks';
+} from '../../../mocks';
+import { mountWithProvider } from '../../../mocks';
// Tests are executed in a jsdom environment who does not have sizing methods,
// thus the AutoSizer will always compute a 0x0 size space
@@ -25,9 +26,7 @@ jest.mock('react-virtualized-auto-sizer', () => {
};
});
-import { mountWithIntl as mount } from '@kbn/test/jest';
import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types';
-import { Action } from '../state_management';
import { ChartSwitch } from './chart_switch';
import { PaletteOutput } from 'src/plugins/charts/public';
@@ -157,6 +156,8 @@ describe('chart_switch', () => {
keptLayerIds: ['a'],
},
]);
+
+ datasource.getLayers.mockReturnValue(['a']);
return {
testDatasource: datasource,
};
@@ -171,78 +172,94 @@ describe('chart_switch', () => {
};
}
- function showFlyout(component: ReactWrapper) {
- component.find('[data-test-subj="lnsChartSwitchPopover"]').first().simulate('click');
+ function showFlyout(instance: ReactWrapper) {
+ instance.find('[data-test-subj="lnsChartSwitchPopover"]').first().simulate('click');
}
- function switchTo(subType: string, component: ReactWrapper) {
- showFlyout(component);
- component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).first().simulate('click');
+ function switchTo(subType: string, instance: ReactWrapper) {
+ showFlyout(instance);
+ instance.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).first().simulate('click');
}
- function getMenuItem(subType: string, component: ReactWrapper) {
- showFlyout(component);
- return component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).first();
+ function getMenuItem(subType: string, instance: ReactWrapper) {
+ showFlyout(instance);
+ return instance.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).first();
}
-
- it('should use suggested state if there is a suggestion from the target visualization', () => {
- const dispatch = jest.fn();
+ it('should use suggested state if there is a suggestion from the target visualization', async () => {
const visualizations = mockVisualizations();
- const component = mount(
+ const { instance, lensStore } = await mountWithProvider(
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visA',
+ state: 'state from a',
+ },
+ },
+ }
);
- switchTo('visB', component);
+ switchTo('visB', instance);
- expect(dispatch).toHaveBeenCalledWith({
- initialState: 'suggestion visB',
- newVisualizationId: 'visB',
- type: 'SWITCH_VISUALIZATION',
- datasourceId: 'testDatasource',
- datasourceState: {},
+ expect(lensStore.dispatch).toHaveBeenCalledWith({
+ type: 'lens/switchVisualization',
+ payload: {
+ initialState: 'suggestion visB',
+ newVisualizationId: 'visB',
+ datasourceId: 'testDatasource',
+ datasourceState: {},
+ },
});
});
- it('should use initial state if there is no suggestion from the target visualization', () => {
- const dispatch = jest.fn();
+ it('should use initial state if there is no suggestion from the target visualization', async () => {
const visualizations = mockVisualizations();
visualizations.visB.getSuggestions.mockReturnValueOnce([]);
const frame = mockFrame(['a']);
(frame.datasourceLayers.a.getTableSpec as jest.Mock).mockReturnValue([]);
-
- const component = mount(
+ const datasourceMap = mockDatasourceMap();
+ const datasourceStates = mockDatasourceStates();
+ const { instance, lensStore } = await mountWithProvider(
+ datasourceMap={datasourceMap}
+ />,
+ {
+ preloadedState: {
+ datasourceStates,
+ activeDatasourceId: 'testDatasource',
+ visualization: {
+ activeId: 'visA',
+ state: {},
+ },
+ },
+ }
);
- switchTo('visB', component);
-
- expect(frame.removeLayers).toHaveBeenCalledWith(['a']);
-
- expect(dispatch).toHaveBeenCalledWith({
- initialState: 'visB initial state',
- newVisualizationId: 'visB',
- type: 'SWITCH_VISUALIZATION',
+ switchTo('visB', instance);
+ expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith({}, 'a'); // from preloaded state
+ expect(lensStore.dispatch).toHaveBeenCalledWith({
+ type: 'lens/switchVisualization',
+ payload: {
+ initialState: 'visB initial state',
+ newVisualizationId: 'visB',
+ },
+ });
+ expect(lensStore.dispatch).toHaveBeenCalledWith({
+ type: 'lens/updateLayer',
+ payload: expect.objectContaining({
+ datasourceId: 'testDatasource',
+ layerId: 'a',
+ }),
});
});
- it('should indicate data loss if not all columns will be used', () => {
- const dispatch = jest.fn();
+ it('should indicate data loss if not all columns will be used', async () => {
const visualizations = mockVisualizations();
const frame = mockFrame(['a']);
@@ -282,53 +299,59 @@ describe('chart_switch', () => {
{ columnId: 'col3' },
]);
- const component = mount(
+ const { instance } = await mountWithProvider(
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visA',
+ state: {},
+ },
+ },
+ }
);
expect(
- getMenuItem('visB', component)
+ getMenuItem('visB', instance)
.find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
.first()
.props().type
).toEqual('alert');
});
- it('should indicate data loss if not all layers will be used', () => {
- const dispatch = jest.fn();
+ it('should indicate data loss if not all layers will be used', async () => {
const visualizations = mockVisualizations();
const frame = mockFrame(['a', 'b']);
- const component = mount(
+ const { instance } = await mountWithProvider(
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visA',
+ state: {},
+ },
+ },
+ }
);
expect(
- getMenuItem('visB', component)
+ getMenuItem('visB', instance)
.find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
.first()
.props().type
).toEqual('alert');
});
- it('should support multi-layer suggestions without data loss', () => {
- const dispatch = jest.fn();
+ it('should support multi-layer suggestions without data loss', async () => {
const visualizations = mockVisualizations();
const frame = mockFrame(['a', 'b']);
@@ -355,75 +378,85 @@ describe('chart_switch', () => {
},
]);
- const component = mount(
+ const { instance } = await mountWithProvider(
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visA',
+ state: {},
+ },
+ },
+ }
);
expect(
- getMenuItem('visB', component).find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
+ getMenuItem('visB', instance).find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
).toHaveLength(0);
});
- it('should indicate data loss if no data will be used', () => {
- const dispatch = jest.fn();
+ it('should indicate data loss if no data will be used', async () => {
const visualizations = mockVisualizations();
visualizations.visB.getSuggestions.mockReturnValueOnce([]);
const frame = mockFrame(['a']);
- const component = mount(
+ const { instance } = await mountWithProvider(
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visA',
+ state: {},
+ },
+ },
+ }
);
expect(
- getMenuItem('visB', component)
+ getMenuItem('visB', instance)
.find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
.first()
.props().type
).toEqual('alert');
});
- it('should not indicate data loss if there is no data', () => {
- const dispatch = jest.fn();
+ it('should not indicate data loss if there is no data', async () => {
const visualizations = mockVisualizations();
visualizations.visB.getSuggestions.mockReturnValueOnce([]);
const frame = mockFrame(['a']);
(frame.datasourceLayers.a.getTableSpec as jest.Mock).mockReturnValue([]);
- const component = mount(
+ const { instance } = await mountWithProvider(
+ />,
+
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visA',
+ state: {},
+ },
+ },
+ }
);
expect(
- getMenuItem('visB', component).find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
+ getMenuItem('visB', instance).find('[data-test-subj="lnsChartSwitchPopoverAlert_visB"]')
).toHaveLength(0);
});
- it('should not show a warning when the subvisualization is the same', () => {
- const dispatch = jest.fn();
+ it('should not show a warning when the subvisualization is the same', async () => {
const frame = mockFrame(['a', 'b', 'c']);
const visualizations = mockVisualizations();
visualizations.visC.getVisualizationTypeId.mockReturnValue('subvisC2');
@@ -431,64 +464,81 @@ describe('chart_switch', () => {
visualizations.visC.switchVisualizationType = switchVisualizationType;
- const component = mount(
+ const datasourceMap = mockDatasourceMap();
+ const datasourceStates = mockDatasourceStates();
+
+ const { instance } = await mountWithProvider(
+ datasourceMap={datasourceMap}
+ />,
+ {
+ preloadedState: {
+ datasourceStates,
+ activeDatasourceId: 'testDatasource',
+ visualization: {
+ activeId: 'visC',
+ state: { type: 'subvisC2' },
+ },
+ },
+ }
);
expect(
- getMenuItem('subvisC2', component).find(
+ getMenuItem('subvisC2', instance).find(
'[data-test-subj="lnsChartSwitchPopoverAlert_subvisC2"]'
)
).toHaveLength(0);
});
- it('should get suggestions when switching subvisualization', () => {
- const dispatch = jest.fn();
+ it('should get suggestions when switching subvisualization', async () => {
const visualizations = mockVisualizations();
visualizations.visB.getSuggestions.mockReturnValueOnce([]);
const frame = mockFrame(['a', 'b', 'c']);
+ const datasourceMap = mockDatasourceMap();
+ datasourceMap.testDatasource.getLayers.mockReturnValue(['a', 'b', 'c']);
+ const datasourceStates = mockDatasourceStates();
- const component = mount(
+ const { instance, lensStore } = await mountWithProvider(
+ datasourceMap={datasourceMap}
+ />,
+ {
+ preloadedState: {
+ datasourceStates,
+ visualization: {
+ activeId: 'visA',
+ state: {},
+ },
+ },
+ }
);
- switchTo('visB', component);
-
- expect(frame.removeLayers).toHaveBeenCalledTimes(1);
- expect(frame.removeLayers).toHaveBeenCalledWith(['a', 'b', 'c']);
-
+ switchTo('visB', instance);
+ expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith({}, 'a');
+ expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith(undefined, 'b');
+ expect(datasourceMap.testDatasource.removeLayer).toHaveBeenCalledWith(undefined, 'c');
expect(visualizations.visB.getSuggestions).toHaveBeenCalledWith(
expect.objectContaining({
keptLayerIds: ['a'],
})
);
- expect(dispatch).toHaveBeenCalledWith(
- expect.objectContaining({
- type: 'SWITCH_VISUALIZATION',
+ expect(lensStore.dispatch).toHaveBeenCalledWith({
+ type: 'lens/switchVisualization',
+ payload: {
+ datasourceId: undefined,
+ datasourceState: undefined,
initialState: 'visB initial state',
- })
- );
+ newVisualizationId: 'visB',
+ },
+ });
});
- it('should query main palette from active chart and pass into suggestions', () => {
- const dispatch = jest.fn();
+ it('should query main palette from active chart and pass into suggestions', async () => {
const visualizations = mockVisualizations();
const mockPalette: PaletteOutput = { type: 'palette', name: 'mock' };
visualizations.visA.getMainPalette = jest.fn(() => mockPalette);
@@ -496,19 +546,26 @@ describe('chart_switch', () => {
const frame = mockFrame(['a', 'b', 'c']);
const currentVisState = {};
- const component = mount(
+ const datasourceMap = mockDatasourceMap();
+ datasourceMap.testDatasource.getLayers.mockReturnValue(['a', 'b', 'c']);
+
+ const { instance } = await mountWithProvider(
+ datasourceMap={datasourceMap}
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visA',
+ state: currentVisState,
+ },
+ },
+ }
);
- switchTo('visB', component);
+ switchTo('visB', instance);
expect(visualizations.visA.getMainPalette).toHaveBeenCalledWith(currentVisState);
@@ -520,67 +577,76 @@ describe('chart_switch', () => {
);
});
- it('should not remove layers when switching between subtypes', () => {
- const dispatch = jest.fn();
+ it('should not remove layers when switching between subtypes', async () => {
const frame = mockFrame(['a', 'b', 'c']);
const visualizations = mockVisualizations();
const switchVisualizationType = jest.fn(() => 'switched');
visualizations.visC.switchVisualizationType = switchVisualizationType;
-
- const component = mount(
+ const datasourceMap = mockDatasourceMap();
+ const { instance, lensStore } = await mountWithProvider(
+ datasourceMap={datasourceMap}
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visC',
+ state: { type: 'subvisC1' },
+ },
+ },
+ }
);
- switchTo('subvisC3', component);
+ switchTo('subvisC3', instance);
expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', { type: 'subvisC3' });
- expect(dispatch).toHaveBeenCalledWith(
- expect.objectContaining({
- type: 'SWITCH_VISUALIZATION',
+
+ expect(lensStore.dispatch).toHaveBeenCalledWith({
+ type: 'lens/switchVisualization',
+ payload: {
+ datasourceId: 'testDatasource',
+ datasourceState: {},
initialState: 'switched',
- })
- );
- expect(frame.removeLayers).not.toHaveBeenCalled();
+ newVisualizationId: 'visC',
+ },
+ });
+ expect(datasourceMap.testDatasource.removeLayer).not.toHaveBeenCalled();
});
- it('should not remove layers and initialize with existing state when switching between subtypes without data', () => {
- const dispatch = jest.fn();
+ it('should not remove layers and initialize with existing state when switching between subtypes without data', async () => {
const frame = mockFrame(['a']);
frame.datasourceLayers.a.getTableSpec = jest.fn().mockReturnValue([]);
const visualizations = mockVisualizations();
visualizations.visC.getSuggestions = jest.fn().mockReturnValue([]);
visualizations.visC.switchVisualizationType = jest.fn(() => 'switched');
-
- const component = mount(
+ const datasourceMap = mockDatasourceMap();
+ const { instance } = await mountWithProvider(
+ datasourceMap={datasourceMap}
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visC',
+ state: { type: 'subvisC1' },
+ },
+ },
+ }
);
- switchTo('subvisC3', component);
+ switchTo('subvisC3', instance);
expect(visualizations.visC.switchVisualizationType).toHaveBeenCalledWith('subvisC3', {
type: 'subvisC1',
});
- expect(frame.removeLayers).not.toHaveBeenCalled();
+ expect(datasourceMap.testDatasource.removeLayer).not.toHaveBeenCalled();
});
- it('should switch to the updated datasource state', () => {
- const dispatch = jest.fn();
+ it('should switch to the updated datasource state', async () => {
const visualizations = mockVisualizations();
const frame = mockFrame(['a', 'b']);
@@ -615,31 +681,36 @@ describe('chart_switch', () => {
},
]);
- const component = mount(
+ const { instance, lensStore } = await mountWithProvider(
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visA',
+ state: {},
+ },
+ },
+ }
);
- switchTo('visB', component);
+ switchTo('visB', instance);
- expect(dispatch).toHaveBeenCalledWith({
- type: 'SWITCH_VISUALIZATION',
- newVisualizationId: 'visB',
- datasourceId: 'testDatasource',
- datasourceState: 'testDatasource suggestion',
- initialState: 'suggestion visB',
- } as Action);
+ expect(lensStore.dispatch).toHaveBeenCalledWith({
+ type: 'lens/switchVisualization',
+ payload: {
+ newVisualizationId: 'visB',
+ datasourceId: 'testDatasource',
+ datasourceState: 'testDatasource suggestion',
+ initialState: 'suggestion visB',
+ },
+ });
});
- it('should ensure the new visualization has the proper subtype', () => {
- const dispatch = jest.fn();
+ it('should ensure the new visualization has the proper subtype', async () => {
const visualizations = mockVisualizations();
const switchVisualizationType = jest.fn(
(visualizationType, state) => `${state} ${visualizationType}`
@@ -647,72 +718,85 @@ describe('chart_switch', () => {
visualizations.visB.switchVisualizationType = switchVisualizationType;
- const component = mount(
+ const { instance, lensStore } = await mountWithProvider(
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visA',
+ state: {},
+ },
+ },
+ }
);
- switchTo('visB', component);
+ switchTo('visB', instance);
- expect(dispatch).toHaveBeenCalledWith({
- initialState: 'suggestion visB visB',
- newVisualizationId: 'visB',
- type: 'SWITCH_VISUALIZATION',
- datasourceId: 'testDatasource',
- datasourceState: {},
+ expect(lensStore.dispatch).toHaveBeenCalledWith({
+ type: 'lens/switchVisualization',
+ payload: {
+ initialState: 'suggestion visB visB',
+ newVisualizationId: 'visB',
+ datasourceId: 'testDatasource',
+ datasourceState: {},
+ },
});
});
- it('should use the suggestion that matches the subtype', () => {
- const dispatch = jest.fn();
+ it('should use the suggestion that matches the subtype', async () => {
const visualizations = mockVisualizations();
const switchVisualizationType = jest.fn();
visualizations.visC.switchVisualizationType = switchVisualizationType;
- const component = mount(
+ const { instance } = await mountWithProvider(
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visC',
+ state: { type: 'subvisC3' },
+ },
+ },
+ }
);
- switchTo('subvisC1', component);
+ switchTo('subvisC1', instance);
expect(switchVisualizationType).toHaveBeenCalledWith('subvisC1', {
type: 'subvisC1',
notPrimary: true,
});
});
- it('should show all visualization types', () => {
- const component = mount(
+ it('should show all visualization types', async () => {
+ const { instance } = await mountWithProvider(
+ />,
+ {
+ preloadedState: {
+ visualization: {
+ activeId: 'visA',
+ state: {},
+ },
+ },
+ }
);
- showFlyout(component);
+ showFlyout(instance);
const allDisplayed = ['visA', 'visB', 'subvisC1', 'subvisC2', 'subvisC3'].every(
- (subType) => component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).length > 0
+ (subType) => instance.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).length > 0
);
expect(allDisplayed).toBeTruthy();
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
index 0c3a992e3dd7a..f948ec6a59687 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
@@ -21,10 +21,16 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { Visualization, FramePublicAPI, Datasource, VisualizationType } from '../../../types';
-import { Action } from '../state_management';
import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers';
import { trackUiEvent } from '../../../lens_ui_telemetry';
import { ToolbarButton } from '../../../../../../../src/plugins/kibana_react/public';
+import {
+ updateLayer,
+ updateVisualizationState,
+ useLensDispatch,
+ useLensSelector,
+} from '../../../state_management';
+import { generateId } from '../../../id_generator/id_generator';
interface VisualizationSelection {
visualizationId: string;
@@ -38,27 +44,26 @@ interface VisualizationSelection {
}
interface Props {
- dispatch: (action: Action) => void;
visualizationMap: Record;
- visualizationId: string | null;
- visualizationState: unknown;
framePublicAPI: FramePublicAPI;
datasourceMap: Record;
- datasourceStates: Record<
- string,
- {
- isLoading: boolean;
- state: unknown;
- }
- >;
}
type SelectableEntry = EuiSelectableOption<{ value: string }>;
-function VisualizationSummary(props: Props) {
- const visualization = props.visualizationMap[props.visualizationId || ''];
+function VisualizationSummary({
+ visualizationMap,
+ visualization,
+}: {
+ visualizationMap: Record;
+ visualization: {
+ activeId: string | null;
+ state: unknown;
+ };
+}) {
+ const activeVisualization = visualizationMap[visualization.activeId || ''];
- if (!visualization) {
+ if (!activeVisualization) {
return (
<>
{i18n.translate('xpack.lens.configPanel.selectVisualization', {
@@ -68,7 +73,7 @@ function VisualizationSummary(props: Props) {
);
}
- const description = visualization.getDescription(props.visualizationState);
+ const description = activeVisualization.getDescription(visualization.state);
return (
<>
@@ -99,6 +104,44 @@ function getCurrentVisualizationId(
export const ChartSwitch = memo(function ChartSwitch(props: Props) {
const [flyoutOpen, setFlyoutOpen] = useState(false);
+ const dispatchLens = useLensDispatch();
+ const activeDatasourceId = useLensSelector((state) => state.lens.activeDatasourceId);
+ const visualization = useLensSelector((state) => state.lens.visualization);
+ const datasourceStates = useLensSelector((state) => state.lens.datasourceStates);
+
+ function removeLayers(layerIds: string[]) {
+ const activeVisualization =
+ visualization.activeId && props.visualizationMap[visualization.activeId];
+ if (activeVisualization && activeVisualization.removeLayer && visualization.state) {
+ dispatchLens(
+ updateVisualizationState({
+ visualizationId: activeVisualization.id,
+ updater: layerIds.reduce(
+ (acc, layerId) =>
+ activeVisualization.removeLayer ? activeVisualization.removeLayer(acc, layerId) : acc,
+ visualization.state
+ ),
+ })
+ );
+ }
+ layerIds.forEach((layerId) => {
+ const layerDatasourceId = Object.entries(props.datasourceMap).find(
+ ([datasourceId, datasource]) => {
+ return (
+ datasourceStates[datasourceId] &&
+ datasource.getLayers(datasourceStates[datasourceId].state).includes(layerId)
+ );
+ }
+ )![0];
+ dispatchLens(
+ updateLayer({
+ layerId,
+ datasourceId: layerDatasourceId,
+ updater: props.datasourceMap[layerDatasourceId].removeLayer,
+ })
+ );
+ });
+ }
const commitSelection = (selection: VisualizationSelection) => {
setFlyoutOpen(false);
@@ -106,7 +149,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
trackUiEvent(`chart_switch`);
switchToSuggestion(
- props.dispatch,
+ dispatchLens,
{
...selection,
visualizationState: selection.getVisualizationState(),
@@ -118,7 +161,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
(!selection.datasourceId && !selection.sameDatasources) ||
selection.dataLoss === 'everything'
) {
- props.framePublicAPI.removeLayers(Object.keys(props.framePublicAPI.datasourceLayers));
+ removeLayers(Object.keys(props.framePublicAPI.datasourceLayers));
}
};
@@ -136,16 +179,16 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
);
// Always show the active visualization as a valid selection
if (
- props.visualizationId === visualizationId &&
- props.visualizationState &&
- newVisualization.getVisualizationTypeId(props.visualizationState) === subVisualizationId
+ visualization.activeId === visualizationId &&
+ visualization.state &&
+ newVisualization.getVisualizationTypeId(visualization.state) === subVisualizationId
) {
return {
visualizationId,
subVisualizationId,
dataLoss: 'nothing',
keptLayerIds: Object.keys(props.framePublicAPI.datasourceLayers),
- getVisualizationState: () => switchVisType(subVisualizationId, props.visualizationState),
+ getVisualizationState: () => switchVisType(subVisualizationId, visualization.state),
sameDatasources: true,
};
}
@@ -153,6 +196,8 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
const topSuggestion = getTopSuggestion(
props,
visualizationId,
+ datasourceStates,
+ visualization,
newVisualization,
subVisualizationId
);
@@ -171,6 +216,19 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
dataLoss = 'nothing';
}
+ function addNewLayer() {
+ const newLayerId = generateId();
+ dispatchLens(
+ updateLayer({
+ datasourceId: activeDatasourceId!,
+ layerId: newLayerId,
+ updater: props.datasourceMap[activeDatasourceId!].insertLayer,
+ })
+ );
+
+ return newLayerId;
+ }
+
return {
visualizationId,
subVisualizationId,
@@ -179,29 +237,26 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
? () =>
switchVisType(
subVisualizationId,
- newVisualization.initialize(props.framePublicAPI, topSuggestion.visualizationState)
+ newVisualization.initialize(addNewLayer, topSuggestion.visualizationState)
)
- : () => {
- return switchVisType(
+ : () =>
+ switchVisType(
subVisualizationId,
newVisualization.initialize(
- props.framePublicAPI,
- props.visualizationId === newVisualization.id
- ? props.visualizationState
- : undefined,
- props.visualizationId &&
- props.visualizationMap[props.visualizationId].getMainPalette
- ? props.visualizationMap[props.visualizationId].getMainPalette!(
- props.visualizationState
+ addNewLayer,
+ visualization.activeId === newVisualization.id ? visualization.state : undefined,
+ visualization.activeId &&
+ props.visualizationMap[visualization.activeId].getMainPalette
+ ? props.visualizationMap[visualization.activeId].getMainPalette!(
+ visualization.state
)
: undefined
)
- );
- },
+ ),
keptLayerIds: topSuggestion ? topSuggestion.keptLayerIds : [],
datasourceState: topSuggestion ? topSuggestion.datasourceState : undefined,
datasourceId: topSuggestion ? topSuggestion.datasourceId : undefined,
- sameDatasources: dataLoss === 'nothing' && props.visualizationId === newVisualization.id,
+ sameDatasources: dataLoss === 'nothing' && visualization.activeId === newVisualization.id,
};
}
@@ -213,8 +268,8 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
return { visualizationTypes: [], visualizationsLookup: {} };
}
const subVisualizationId = getCurrentVisualizationId(
- props.visualizationMap[props.visualizationId || ''],
- props.visualizationState
+ props.visualizationMap[visualization.activeId || ''],
+ visualization.state
);
const lowercasedSearchTerm = searchTerm.toLowerCase();
// reorganize visualizations in groups
@@ -351,8 +406,8 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
flyoutOpen,
props.visualizationMap,
props.framePublicAPI,
- props.visualizationId,
- props.visualizationState,
+ visualization.activeId,
+ visualization.state,
searchTerm,
]
);
@@ -371,7 +426,10 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
data-test-subj="lnsChartSwitchPopover"
fontWeight="bold"
>
-
+
}
isOpen={flyoutOpen}
@@ -402,7 +460,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
}}
options={visualizationTypes}
onChange={(newOptions) => {
- const chosenType = newOptions.find(({ checked }) => checked === 'on')!;
+ const chosenType = newOptions.find(({ checked }) => checked === 'on');
if (!chosenType) {
return;
}
@@ -434,21 +492,26 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
function getTopSuggestion(
props: Props,
visualizationId: string,
+ datasourceStates: Record,
+ visualization: {
+ activeId: string | null;
+ state: unknown;
+ },
newVisualization: Visualization,
subVisualizationId?: string
): Suggestion | undefined {
const mainPalette =
- props.visualizationId &&
- props.visualizationMap[props.visualizationId] &&
- props.visualizationMap[props.visualizationId].getMainPalette
- ? props.visualizationMap[props.visualizationId].getMainPalette!(props.visualizationState)
+ visualization.activeId &&
+ props.visualizationMap[visualization.activeId] &&
+ props.visualizationMap[visualization.activeId].getMainPalette
+ ? props.visualizationMap[visualization.activeId].getMainPalette!(visualization.state)
: undefined;
const unfilteredSuggestions = getSuggestions({
datasourceMap: props.datasourceMap,
- datasourceStates: props.datasourceStates,
+ datasourceStates,
visualizationMap: { [visualizationId]: newVisualization },
- activeVisualizationId: props.visualizationId,
- visualizationState: props.visualizationState,
+ activeVisualizationId: visualization.activeId,
+ visualizationState: visualization.state,
subVisualizationId,
activeData: props.framePublicAPI.activeData,
mainPalette,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/title.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/title.tsx
new file mode 100644
index 0000000000000..b7d3d211eb777
--- /dev/null
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/title.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import './workspace_panel_wrapper.scss';
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiScreenReaderOnly } from '@elastic/eui';
+import { LensState, useLensSelector } from '../../../state_management';
+
+export function WorkspaceTitle() {
+ const title = useLensSelector((state: LensState) => state.lens.persistedDoc?.title);
+ return (
+
+
+ {title ||
+ i18n.translate('xpack.lens.chartTitle.unsaved', {
+ defaultMessage: 'Unsaved visualization',
+ })}
+
+
+ );
+}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
index 38e9bb868b26a..4feb13fcfffd9 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
@@ -15,7 +15,7 @@ import {
createExpressionRendererMock,
DatasourceMock,
createMockFramePublicAPI,
-} from '../../mocks';
+} from '../../../mocks';
import { mockDataPlugin, mountWithProvider } from '../../../mocks';
jest.mock('../../../debounced_component', () => {
return {
@@ -24,7 +24,6 @@ jest.mock('../../../debounced_component', () => {
});
import { WorkspacePanel } from './workspace_panel';
-import { mountWithIntl as mount } from '@kbn/test/jest';
import { ReactWrapper } from 'enzyme';
import { DragDrop, ChildDragDropProvider } from '../../../drag_drop';
import { fromExpression } from '@kbn/interpreter/common';
@@ -56,7 +55,6 @@ const defaultProps = {
framePublicAPI: createMockFramePublicAPI(),
activeVisualizationId: 'vis',
visualizationState: {},
- dispatch: () => {},
ExpressionRenderer: createExpressionRendererMock(),
core: createCoreStartWithPermissions(),
plugins: {
@@ -104,7 +102,8 @@ describe('workspace_panel', () => {
}}
ExpressionRenderer={expressionRendererMock}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2);
@@ -119,7 +118,8 @@ describe('workspace_panel', () => {
vis: { ...mockVisualization, toExpression: () => null },
}}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -135,7 +135,8 @@ describe('workspace_panel', () => {
vis: { ...mockVisualization, toExpression: () => 'vis' },
}}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -169,7 +170,7 @@ describe('workspace_panel', () => {
}}
ExpressionRenderer={expressionRendererMock}
/>,
- defaultProps.plugins.data
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -209,7 +210,8 @@ describe('workspace_panel', () => {
ExpressionRenderer={expressionRendererMock}
plugins={{ ...props.plugins, uiActions: uiActionsMock }}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -229,7 +231,6 @@ describe('workspace_panel', () => {
};
mockDatasource.toExpression.mockReturnValue('datasource');
mockDatasource.getLayers.mockReturnValue(['first']);
- const dispatch = jest.fn();
const mounted = await mountWithProvider(
{
visualizationMap={{
vis: { ...mockVisualization, toExpression: () => 'vis' },
}}
- dispatch={dispatch}
ExpressionRenderer={expressionRendererMock}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -261,8 +262,8 @@ describe('workspace_panel', () => {
onData(undefined, { tables: { tables: tableData } });
expect(mounted.lensStore.dispatch).toHaveBeenCalledWith({
- type: 'app/onActiveDataChange',
- payload: { activeData: tableData },
+ type: 'lens/onActiveDataChange',
+ payload: tableData,
});
});
@@ -302,7 +303,8 @@ describe('workspace_panel', () => {
}}
ExpressionRenderer={expressionRendererMock}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -377,7 +379,8 @@ describe('workspace_panel', () => {
}}
ExpressionRenderer={expressionRendererMock}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
});
@@ -430,7 +433,8 @@ describe('workspace_panel', () => {
}}
ExpressionRenderer={expressionRendererMock}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
});
@@ -481,7 +485,8 @@ describe('workspace_panel', () => {
vis: { ...mockVisualization, toExpression: () => 'vis' },
}}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -520,7 +525,8 @@ describe('workspace_panel', () => {
management: { kibana: { indexPatterns: true } },
})}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -559,7 +565,8 @@ describe('workspace_panel', () => {
management: { kibana: { indexPatterns: false } },
})}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -595,7 +602,8 @@ describe('workspace_panel', () => {
vis: { ...mockVisualization, toExpression: () => 'vis' },
}}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -632,7 +640,8 @@ describe('workspace_panel', () => {
vis: mockVisualization,
}}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -671,7 +680,8 @@ describe('workspace_panel', () => {
vis: mockVisualization,
}}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -707,7 +717,8 @@ describe('workspace_panel', () => {
vis: { ...mockVisualization, toExpression: () => 'vis' },
}}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
@@ -742,7 +753,8 @@ describe('workspace_panel', () => {
}}
ExpressionRenderer={expressionRendererMock}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
});
@@ -783,7 +795,8 @@ describe('workspace_panel', () => {
}}
ExpressionRenderer={expressionRendererMock}
/>,
- defaultProps.plugins.data
+
+ { data: defaultProps.plugins.data }
);
instance = mounted.instance;
});
@@ -805,7 +818,6 @@ describe('workspace_panel', () => {
});
describe('suggestions from dropping in workspace panel', () => {
- let mockDispatch: jest.Mock;
let mockGetSuggestionForField: jest.Mock;
let frame: jest.Mocked;
@@ -813,12 +825,11 @@ describe('workspace_panel', () => {
beforeEach(() => {
frame = createMockFramePublicAPI();
- mockDispatch = jest.fn();
mockGetSuggestionForField = jest.fn();
});
- function initComponent(draggingContext = draggedField) {
- instance = mount(
+ async function initComponent(draggingContext = draggedField) {
+ const mounted = await mountWithProvider(
{}}
@@ -846,11 +857,12 @@ describe('workspace_panel', () => {
vis: mockVisualization,
vis2: mockVisualization2,
}}
- dispatch={mockDispatch}
getSuggestionForField={mockGetSuggestionForField}
/>
);
+ instance = mounted.instance;
+ return mounted;
}
it('should immediately transition if exactly one suggestion is returned', async () => {
@@ -860,32 +872,34 @@ describe('workspace_panel', () => {
datasourceId: 'mock',
visualizationState: {},
});
- initComponent();
+ const { lensStore } = await initComponent();
instance.find(DragDrop).prop('onDrop')!(draggedField, 'field_replace');
- expect(mockDispatch).toHaveBeenCalledWith({
- type: 'SWITCH_VISUALIZATION',
- newVisualizationId: 'vis',
- initialState: {},
- datasourceState: {},
- datasourceId: 'mock',
+ expect(lensStore.dispatch).toHaveBeenCalledWith({
+ type: 'lens/switchVisualization',
+ payload: {
+ newVisualizationId: 'vis',
+ initialState: {},
+ datasourceState: {},
+ datasourceId: 'mock',
+ },
});
});
- it('should allow to drop if there are suggestions', () => {
+ it('should allow to drop if there are suggestions', async () => {
mockGetSuggestionForField.mockReturnValue({
visualizationId: 'vis',
datasourceState: {},
datasourceId: 'mock',
visualizationState: {},
});
- initComponent();
+ await initComponent();
expect(instance.find(DragDrop).prop('dropTypes')).toBeTruthy();
});
- it('should refuse to drop if there are no suggestions', () => {
- initComponent();
+ it('should refuse to drop if there are no suggestions', async () => {
+ await initComponent();
expect(instance.find(DragDrop).prop('dropType')).toBeFalsy();
});
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
index 01d4e84ec4374..943dec8f0ed20 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
@@ -33,7 +33,6 @@ import {
ExpressionRenderError,
ReactExpressionRendererType,
} from '../../../../../../../src/plugins/expressions/public';
-import { Action } from '../state_management';
import {
Datasource,
Visualization,
@@ -46,17 +45,20 @@ import { DragDrop, DragContext, DragDropIdentifier } from '../../../drag_drop';
import { Suggestion, switchToSuggestion } from '../suggestion_helpers';
import { buildExpression } from '../expression_helpers';
import { trackUiEvent } from '../../../lens_ui_telemetry';
-import {
- UiActionsStart,
- VisualizeFieldContext,
-} from '../../../../../../../src/plugins/ui_actions/public';
+import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public';
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
import { DropIllustration } from '../../../assets/drop_illustration';
import { getOriginalRequestErrorMessages } from '../../error_helper';
import { getMissingIndexPattern, validateDatasourceAndVisualization } from '../state_helpers';
import { DefaultInspectorAdapters } from '../../../../../../../src/plugins/expressions/common';
-import { onActiveDataChange, useLensDispatch } from '../../../state_management';
+import {
+ onActiveDataChange,
+ useLensDispatch,
+ updateVisualizationState,
+ updateDatasourceState,
+ setSaveable,
+} from '../../../state_management';
export interface WorkspacePanelProps {
activeVisualizationId: string | null;
@@ -72,12 +74,9 @@ export interface WorkspacePanelProps {
}
>;
framePublicAPI: FramePublicAPI;
- dispatch: (action: Action) => void;
ExpressionRenderer: ReactExpressionRendererType;
core: CoreStart;
plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart };
- title?: string;
- visualizeTriggerFieldContext?: VisualizeFieldContext;
getSuggestionForField: (field: DragDropIdentifier) => Suggestion | undefined;
isFullscreen: boolean;
}
@@ -128,17 +127,15 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
datasourceMap,
datasourceStates,
framePublicAPI,
- dispatch,
core,
plugins,
ExpressionRenderer: ExpressionRendererComponent,
- title,
- visualizeTriggerFieldContext,
suggestionForDraggedField,
isFullscreen,
}: Omit & {
suggestionForDraggedField: Suggestion | undefined;
}) {
+ const dispatchLens = useLensDispatch();
const [localState, setLocalState] = useState({
expressionBuildError: undefined,
expandError: false,
@@ -196,6 +193,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
datasourceStates,
datasourceLayers: framePublicAPI.datasourceLayers,
});
+
if (ast) {
// expression has to be turned into a string for dirty checking - if the ast is rebuilt,
// turning it into a string will make sure the expression renderer only re-renders if the
@@ -233,6 +231,14 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
);
const expressionExists = Boolean(expression);
+ const hasLoaded = Boolean(
+ activeVisualization && visualizationState && datasourceMap && datasourceStates
+ );
+ useEffect(() => {
+ if (hasLoaded) {
+ dispatchLens(setSaveable(expressionExists));
+ }
+ }, [hasLoaded, expressionExists, dispatchLens]);
const onEvent = useCallback(
(event: ExpressionRendererEvent) => {
@@ -251,14 +257,15 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
});
}
if (isLensEditEvent(event) && activeVisualization?.onEditAction) {
- dispatch({
- type: 'UPDATE_VISUALIZATION_STATE',
- visualizationId: activeVisualization.id,
- updater: (oldState: unknown) => activeVisualization.onEditAction!(oldState, event),
- });
+ dispatchLens(
+ updateVisualizationState({
+ visualizationId: activeVisualization.id,
+ updater: (oldState: unknown) => activeVisualization.onEditAction!(oldState, event),
+ })
+ );
}
},
- [plugins.uiActions, dispatch, activeVisualization]
+ [plugins.uiActions, activeVisualization, dispatchLens]
);
useEffect(() => {
@@ -275,9 +282,9 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
if (suggestionForDraggedField) {
trackUiEvent('drop_onto_workspace');
trackUiEvent(expressionExists ? 'drop_non_empty' : 'drop_empty');
- switchToSuggestion(dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION');
+ switchToSuggestion(dispatchLens, suggestionForDraggedField, 'SWITCH_VISUALIZATION');
}
- }, [suggestionForDraggedField, expressionExists, dispatch]);
+ }, [suggestionForDraggedField, expressionExists, dispatchLens]);
const renderEmptyWorkspace = () => {
return (
@@ -327,9 +334,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
};
const renderVisualization = () => {
- // we don't want to render the emptyWorkspace on visualizing field from Discover
- // as it is specific for the drag and drop functionality and can confuse the users
- if (expression === null && !visualizeTriggerFieldContext) {
+ if (expression === null) {
return renderEmptyWorkspace();
}
return (
@@ -337,7 +342,6 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
expression={expression}
framePublicAPI={framePublicAPI}
timefilter={plugins.data.query.timefilter.timefilter}
- dispatch={dispatch}
onEvent={onEvent}
setLocalState={setLocalState}
localState={{ ...localState, configurationValidationError, missingRefsErrors }}
@@ -387,9 +391,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
return (
void;
- dispatch: (action: Action) => void;
setLocalState: (dispatch: (prevState: WorkspaceState) => WorkspaceState) => void;
localState: WorkspaceState & {
configurationValidationError?: Array<{
@@ -454,7 +454,7 @@ export const VisualizationWrapper = ({
const onData$ = useCallback(
(data: unknown, inspectorAdapters?: Partial) => {
if (inspectorAdapters && inspectorAdapters.tables) {
- dispatchLens(onActiveDataChange({ activeData: { ...inspectorAdapters.tables.tables } }));
+ dispatchLens(onActiveDataChange({ ...inspectorAdapters.tables.tables }));
}
},
[dispatchLens]
@@ -480,11 +480,12 @@ export const VisualizationWrapper = ({
data-test-subj="errorFixAction"
onClick={async () => {
const newState = await validationError.fixAction?.newState(framePublicAPI);
- dispatch({
- type: 'UPDATE_DATASOURCE_STATE',
- datasourceId: activeDatasourceId,
- updater: newState,
- });
+ dispatchLens(
+ updateDatasourceState({
+ updater: newState,
+ datasourceId: activeDatasourceId,
+ })
+ );
}}
>
{validationError.fixAction.label}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx
index c18b362e2faa4..fb77ff75324f0 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx
@@ -7,30 +7,23 @@
import React from 'react';
import { Visualization } from '../../../types';
-import { createMockVisualization, createMockFramePublicAPI, FrameMock } from '../../mocks';
-import { mountWithIntl as mount } from '@kbn/test/jest';
-import { ReactWrapper } from 'enzyme';
-import { WorkspacePanelWrapper, WorkspacePanelWrapperProps } from './workspace_panel_wrapper';
+import { createMockVisualization, createMockFramePublicAPI, FrameMock } from '../../../mocks';
+import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
+import { mountWithProvider } from '../../../mocks';
describe('workspace_panel_wrapper', () => {
let mockVisualization: jest.Mocked;
let mockFrameAPI: FrameMock;
- let instance: ReactWrapper;
beforeEach(() => {
mockVisualization = createMockVisualization();
mockFrameAPI = createMockFramePublicAPI();
});
- afterEach(() => {
- instance.unmount();
- });
-
- it('should render its children', () => {
+ it('should render its children', async () => {
const MyChild = () => The child elements;
- instance = mount(
+ const { instance } = await mountWithProvider(
{
expect(instance.find(MyChild)).toHaveLength(1);
});
- it('should call the toolbar renderer if provided', () => {
+ it('should call the toolbar renderer if provided', async () => {
const renderToolbarMock = jest.fn();
const visState = { internalState: 123 };
- instance = mount(
+ await mountWithProvider(
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
index 6724002d23e0b..d0e8e0d5a1bab 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
@@ -8,21 +8,19 @@
import './workspace_panel_wrapper.scss';
import React, { useCallback } from 'react';
-import { i18n } from '@kbn/i18n';
-import { EuiPageContent, EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly } from '@elastic/eui';
+import { EuiPageContent, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import classNames from 'classnames';
import { Datasource, FramePublicAPI, Visualization } from '../../../types';
import { NativeRenderer } from '../../../native_renderer';
-import { Action } from '../state_management';
import { ChartSwitch } from './chart_switch';
import { WarningsPopover } from './warnings_popover';
+import { useLensDispatch, updateVisualizationState } from '../../../state_management';
+import { WorkspaceTitle } from './title';
export interface WorkspacePanelWrapperProps {
children: React.ReactNode | React.ReactNode[];
framePublicAPI: FramePublicAPI;
visualizationState: unknown;
- dispatch: (action: Action) => void;
- title?: string;
visualizationMap: Record;
visualizationId: string | null;
datasourceMap: Record;
@@ -40,28 +38,29 @@ export function WorkspacePanelWrapper({
children,
framePublicAPI,
visualizationState,
- dispatch,
- title,
visualizationId,
visualizationMap,
datasourceMap,
datasourceStates,
isFullscreen,
}: WorkspacePanelWrapperProps) {
+ const dispatchLens = useLensDispatch();
+
const activeVisualization = visualizationId ? visualizationMap[visualizationId] : null;
const setVisualizationState = useCallback(
(newState: unknown) => {
if (!activeVisualization) {
return;
}
- dispatch({
- type: 'UPDATE_VISUALIZATION_STATE',
- visualizationId: activeVisualization.id,
- updater: newState,
- clearStagedPreview: false,
- });
+ dispatchLens(
+ updateVisualizationState({
+ visualizationId: activeVisualization.id,
+ updater: newState,
+ clearStagedPreview: false,
+ })
+ );
},
- [dispatch, activeVisualization]
+ [dispatchLens, activeVisualization]
);
const warningMessages: React.ReactNode[] = [];
if (activeVisualization?.getWarningMessages) {
@@ -101,11 +100,7 @@ export function WorkspacePanelWrapper({
@@ -136,14 +131,7 @@ export function WorkspacePanelWrapper({
'lnsWorkspacePanelWrapper--fullscreen': isFullscreen,
})}
>
-
-
- {title ||
- i18n.translate('xpack.lens.chartTitle.unsaved', {
- defaultMessage: 'Unsaved visualization',
- })}
-
-
+
{children}
>
diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
index 1762e7ff20fab..ff0d81c7fa277 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
@@ -5,105 +5,14 @@
* 2.0.
*/
-import React from 'react';
import { PaletteDefinition } from 'src/plugins/charts/public';
-import {
- ReactExpressionRendererProps,
- ExpressionsSetup,
- ExpressionsStart,
-} from '../../../../../src/plugins/expressions/public';
+import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public';
import { embeddablePluginMock } from '../../../../../src/plugins/embeddable/public/mocks';
import { expressionsPluginMock } from '../../../../../src/plugins/expressions/public/mocks';
-import { DatasourcePublicAPI, FramePublicAPI, Datasource, Visualization } from '../types';
import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './service';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
-export function createMockVisualization(): jest.Mocked {
- return {
- id: 'TEST_VIS',
- clearLayer: jest.fn((state, _layerId) => state),
- removeLayer: jest.fn(),
- getLayerIds: jest.fn((_state) => ['layer1']),
- visualizationTypes: [
- {
- icon: 'empty',
- id: 'TEST_VIS',
- label: 'TEST',
- groupLabel: 'TEST_VISGroup',
- },
- ],
- getVisualizationTypeId: jest.fn((_state) => 'empty'),
- getDescription: jest.fn((_state) => ({ label: '' })),
- switchVisualizationType: jest.fn((_, x) => x),
- getSuggestions: jest.fn((_options) => []),
- initialize: jest.fn((_frame, _state?) => ({})),
- getConfiguration: jest.fn((props) => ({
- groups: [
- {
- groupId: 'a',
- groupLabel: 'a',
- layerId: 'layer1',
- supportsMoreColumns: true,
- accessors: [],
- filterOperations: jest.fn(() => true),
- dataTestSubj: 'mockVisA',
- },
- ],
- })),
- toExpression: jest.fn((_state, _frame) => null),
- toPreviewExpression: jest.fn((_state, _frame) => null),
-
- setDimension: jest.fn(),
- removeDimension: jest.fn(),
- getErrorMessages: jest.fn((_state) => undefined),
- renderDimensionEditor: jest.fn(),
- };
-}
-
-export type DatasourceMock = jest.Mocked & {
- publicAPIMock: jest.Mocked;
-};
-
-export function createMockDatasource(id: string): DatasourceMock {
- const publicAPIMock: jest.Mocked = {
- datasourceId: id,
- getTableSpec: jest.fn(() => []),
- getOperationForColumnId: jest.fn(),
- };
-
- return {
- id: 'mockindexpattern',
- clearLayer: jest.fn((state, _layerId) => state),
- getDatasourceSuggestionsForField: jest.fn((_state, _item) => []),
- getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []),
- getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []),
- getPersistableState: jest.fn((x) => ({ state: x, savedObjectReferences: [] })),
- getPublicAPI: jest.fn().mockReturnValue(publicAPIMock),
- initialize: jest.fn((_state?) => Promise.resolve()),
- renderDataPanel: jest.fn(),
- renderLayerPanel: jest.fn(),
- toExpression: jest.fn((_frame, _state) => null),
- insertLayer: jest.fn((_state, _newLayerId) => {}),
- removeLayer: jest.fn((_state, _layerId) => {}),
- removeColumn: jest.fn((props) => {}),
- getLayers: jest.fn((_state) => []),
- uniqueLabels: jest.fn((_state) => ({})),
- renderDimensionTrigger: jest.fn(),
- renderDimensionEditor: jest.fn(),
- getDropProps: jest.fn(),
- onDrop: jest.fn(),
-
- // this is an additional property which doesn't exist on real datasources
- // but can be used to validate whether specific API mock functions are called
- publicAPIMock,
- getErrorMessages: jest.fn((_state) => undefined),
- checkIntegrity: jest.fn((_state) => []),
- };
-}
-
-export type FrameMock = jest.Mocked;
-
export function createMockPaletteDefinition(): jest.Mocked {
return {
getCategoricalColors: jest.fn((_) => ['#ff0000', '#00ff00']),
@@ -123,23 +32,6 @@ export function createMockPaletteDefinition(): jest.Mocked {
};
}
-export function createMockFramePublicAPI(): FrameMock {
- const palette = createMockPaletteDefinition();
- return {
- datasourceLayers: {},
- addNewLayer: jest.fn(() => ''),
- removeLayers: jest.fn(),
- dateRange: { fromDate: 'now-7d', toDate: 'now' },
- query: { query: '', language: 'lucene' },
- filters: [],
- availablePalettes: {
- get: () => palette,
- getAll: () => [palette],
- },
- searchSessionId: 'sessionId',
- };
-}
-
type Omit = Pick>;
export type MockedSetupDependencies = Omit & {
@@ -150,13 +42,6 @@ export type MockedStartDependencies = Omit;
};
-export function createExpressionRendererMock(): jest.Mock<
- React.ReactElement,
- [ReactExpressionRendererProps]
-> {
- return jest.fn((_) => );
-}
-
export function createMockSetupDependencies() {
return ({
data: dataPluginMock.createSetupContract(),
diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx
index 6a26f85a64acc..63340795ec6c8 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx
@@ -105,27 +105,25 @@ export class EditorFrameService {
]);
const { EditorFrame } = await import('../async_services');
- const palettes = await plugins.charts.palettes.getPalettes();
return {
- EditorFrameContainer: ({ onError, showNoDataPopover, initialContext }) => {
+ EditorFrameContainer: ({ showNoDataPopover }) => {
return (
);
},
+ datasourceMap: resolvedDatasources,
+ visualizationMap: resolvedVisualizations,
};
};
diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts b/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts
index 3ed82bef06105..eeec6150dc497 100644
--- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts
+++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts
@@ -10,7 +10,7 @@ import {
getHeatmapVisualization,
isCellValueSupported,
} from './visualization';
-import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
+import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
import {
CHART_SHAPES,
FUNCTION_NAME,
@@ -49,8 +49,8 @@ describe('heatmap', () => {
describe('#intialize', () => {
test('returns a default state', () => {
- expect(getHeatmapVisualization({}).initialize(frame)).toEqual({
- layerId: '',
+ expect(getHeatmapVisualization({}).initialize(() => 'l1')).toEqual({
+ layerId: 'l1',
title: 'Empty Heatmap chart',
shape: CHART_SHAPES.HEATMAP,
legend: {
@@ -68,7 +68,9 @@ describe('heatmap', () => {
});
test('returns persisted state', () => {
- expect(getHeatmapVisualization({}).initialize(frame, exampleState())).toEqual(exampleState());
+ expect(getHeatmapVisualization({}).initialize(() => 'test-layer', exampleState())).toEqual(
+ exampleState()
+ );
});
});
diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx
index fce5bf30f47ed..7788e93812b1b 100644
--- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx
@@ -119,10 +119,10 @@ export const getHeatmapVisualization = ({
return CHART_NAMES.heatmap;
},
- initialize(frame, state, mainPalette) {
+ initialize(addNewLayer, state, mainPalette) {
return (
state || {
- layerId: frame.addNewLayer(),
+ layerId: addNewLayer(),
title: 'Empty Heatmap chart',
...getInitialState(),
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
index 2921251babe7f..82c27a76bb483 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
@@ -446,10 +446,13 @@ export async function syncExistingFields({
isFirstExistenceFetch: false,
existenceFetchFailed: false,
existenceFetchTimeout: false,
- existingFields: emptinessInfo.reduce((acc, info) => {
- acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames);
- return acc;
- }, state.existingFields),
+ existingFields: emptinessInfo.reduce(
+ (acc, info) => {
+ acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames);
+ return acc;
+ },
+ { ...state.existingFields }
+ ),
}));
} catch (e) {
// show all fields as available if fetch failed or timed out
@@ -457,10 +460,13 @@ export async function syncExistingFields({
...state,
existenceFetchFailed: e.res?.status !== 408,
existenceFetchTimeout: e.res?.status === 408,
- existingFields: indexPatterns.reduce((acc, pattern) => {
- acc[pattern.title] = booleanMap(pattern.fields.map((field) => field.name));
- return acc;
- }, state.existingFields),
+ existingFields: indexPatterns.reduce(
+ (acc, pattern) => {
+ acc[pattern.title] = booleanMap(pattern.fields.map((field) => field.name));
+ return acc;
+ },
+ { ...state.existingFields }
+ ),
}));
}
}
diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts
index 66e524435ebc8..2882d9c4c0246 100644
--- a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts
+++ b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts
@@ -7,7 +7,7 @@
import { metricVisualization } from './visualization';
import { MetricState } from './types';
-import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
+import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
import { generateId } from '../id_generator';
import { DatasourcePublicAPI, FramePublicAPI } from '../types';
@@ -23,7 +23,6 @@ function exampleState(): MetricState {
function mockFrame(): FramePublicAPI {
return {
...createMockFramePublicAPI(),
- addNewLayer: () => 'l42',
datasourceLayers: {
l1: createMockDatasource('l1').publicAPIMock,
l42: createMockDatasource('l42').publicAPIMock,
@@ -35,19 +34,19 @@ describe('metric_visualization', () => {
describe('#initialize', () => {
it('loads default state', () => {
(generateId as jest.Mock).mockReturnValueOnce('test-id1');
- const initialState = metricVisualization.initialize(mockFrame());
+ const initialState = metricVisualization.initialize(() => 'test-id1');
expect(initialState.accessor).not.toBeDefined();
expect(initialState).toMatchInlineSnapshot(`
- Object {
- "accessor": undefined,
- "layerId": "l42",
- }
- `);
+ Object {
+ "accessor": undefined,
+ "layerId": "test-id1",
+ }
+ `);
});
it('loads from persisted state', () => {
- expect(metricVisualization.initialize(mockFrame(), exampleState())).toEqual(exampleState());
+ expect(metricVisualization.initialize(() => 'l1', exampleState())).toEqual(exampleState());
});
});
diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx
index e0977be7535af..49565f53bda36 100644
--- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx
@@ -85,10 +85,10 @@ export const metricVisualization: Visualization = {
getSuggestions,
- initialize(frame, state) {
+ initialize(addNewLayer, state) {
return (
state || {
- layerId: frame.addNewLayer(),
+ layerId: addNewLayer(),
accessor: undefined,
}
);
diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx
index dcdabac36db3a..fc1b3019df386 100644
--- a/x-pack/plugins/lens/public/mocks.tsx
+++ b/x-pack/plugins/lens/public/mocks.tsx
@@ -15,6 +15,7 @@ import { coreMock } from 'src/core/public/mocks';
import moment from 'moment';
import { Provider } from 'react-redux';
import { act } from 'react-dom/test-utils';
+import { ReactExpressionRendererProps } from 'src/plugins/expressions/public';
import { LensPublicStart } from '.';
import { visualizationTypes } from './xy_visualization/types';
import { navigationPluginMock } from '../../../../src/plugins/navigation/public/mocks';
@@ -37,6 +38,111 @@ import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/publ
import { makeConfigureStore, getPreloadedState, LensAppState } from './state_management/index';
import { getResolvedDateRange } from './utils';
import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks';
+import { DatasourcePublicAPI, Datasource, Visualization, FramePublicAPI } from './types';
+
+export function createMockVisualization(): jest.Mocked {
+ return {
+ id: 'TEST_VIS',
+ clearLayer: jest.fn((state, _layerId) => state),
+ removeLayer: jest.fn(),
+ getLayerIds: jest.fn((_state) => ['layer1']),
+ visualizationTypes: [
+ {
+ icon: 'empty',
+ id: 'TEST_VIS',
+ label: 'TEST',
+ groupLabel: 'TEST_VISGroup',
+ },
+ ],
+ getVisualizationTypeId: jest.fn((_state) => 'empty'),
+ getDescription: jest.fn((_state) => ({ label: '' })),
+ switchVisualizationType: jest.fn((_, x) => x),
+ getSuggestions: jest.fn((_options) => []),
+ initialize: jest.fn((_frame, _state?) => ({})),
+ getConfiguration: jest.fn((props) => ({
+ groups: [
+ {
+ groupId: 'a',
+ groupLabel: 'a',
+ layerId: 'layer1',
+ supportsMoreColumns: true,
+ accessors: [],
+ filterOperations: jest.fn(() => true),
+ dataTestSubj: 'mockVisA',
+ },
+ ],
+ })),
+ toExpression: jest.fn((_state, _frame) => null),
+ toPreviewExpression: jest.fn((_state, _frame) => null),
+
+ setDimension: jest.fn(),
+ removeDimension: jest.fn(),
+ getErrorMessages: jest.fn((_state) => undefined),
+ renderDimensionEditor: jest.fn(),
+ };
+}
+
+export type DatasourceMock = jest.Mocked & {
+ publicAPIMock: jest.Mocked;
+};
+
+export function createMockDatasource(id: string): DatasourceMock {
+ const publicAPIMock: jest.Mocked = {
+ datasourceId: id,
+ getTableSpec: jest.fn(() => []),
+ getOperationForColumnId: jest.fn(),
+ };
+
+ return {
+ id: 'mockindexpattern',
+ clearLayer: jest.fn((state, _layerId) => state),
+ getDatasourceSuggestionsForField: jest.fn((_state, _item) => []),
+ getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []),
+ getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []),
+ getPersistableState: jest.fn((x) => ({
+ state: x,
+ savedObjectReferences: [{ type: 'index-pattern', id: 'mockip', name: 'mockip' }],
+ })),
+ getPublicAPI: jest.fn().mockReturnValue(publicAPIMock),
+ initialize: jest.fn((_state?) => Promise.resolve()),
+ renderDataPanel: jest.fn(),
+ renderLayerPanel: jest.fn(),
+ toExpression: jest.fn((_frame, _state) => null),
+ insertLayer: jest.fn((_state, _newLayerId) => {}),
+ removeLayer: jest.fn((_state, _layerId) => {}),
+ removeColumn: jest.fn((props) => {}),
+ getLayers: jest.fn((_state) => []),
+ uniqueLabels: jest.fn((_state) => ({})),
+ renderDimensionTrigger: jest.fn(),
+ renderDimensionEditor: jest.fn(),
+ getDropProps: jest.fn(),
+ onDrop: jest.fn(),
+
+ // this is an additional property which doesn't exist on real datasources
+ // but can be used to validate whether specific API mock functions are called
+ publicAPIMock,
+ getErrorMessages: jest.fn((_state) => undefined),
+ checkIntegrity: jest.fn((_state) => []),
+ };
+}
+
+export function createExpressionRendererMock(): jest.Mock<
+ React.ReactElement,
+ [ReactExpressionRendererProps]
+> {
+ return jest.fn((_) => );
+}
+
+export type FrameMock = jest.Mocked;
+export function createMockFramePublicAPI(): FrameMock {
+ return {
+ datasourceLayers: {},
+ dateRange: { fromDate: 'now-7d', toDate: 'now' },
+ query: { query: '', language: 'lucene' },
+ filters: [],
+ searchSessionId: 'sessionId',
+ };
+}
export type Start = jest.Mocked;
@@ -66,6 +172,9 @@ export const defaultDoc = ({
state: {
query: 'kuery',
filters: [{ query: { match_phrase: { src: 'test' } } }],
+ datasourceStates: {
+ testDatasource: 'datasource',
+ },
},
references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
} as unknown) as Document;
@@ -257,20 +366,48 @@ export function makeDefaultServices(
};
}
-export function mockLensStore({
+export const defaultState = {
+ searchSessionId: 'sessionId-1',
+ filters: [],
+ query: { language: 'lucene', query: '' },
+ resolvedDateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' },
+ isFullscreenDatasource: false,
+ isSaveable: false,
+ isLoading: false,
+ isLinkedToOriginatingApp: false,
+ activeDatasourceId: 'testDatasource',
+ visualization: {
+ state: {},
+ activeId: 'testVis',
+ },
+ datasourceStates: {
+ testDatasource: {
+ isLoading: false,
+ state: '',
+ },
+ },
+};
+
+export function makeLensStore({
data,
- storePreloadedState,
+ preloadedState,
+ dispatch,
}: {
- data: DataPublicPluginStart;
- storePreloadedState?: Partial;
+ data?: DataPublicPluginStart;
+ preloadedState?: Partial;
+ dispatch?: jest.Mock;
}) {
+ if (!data) {
+ data = mockDataPlugin();
+ }
const lensStore = makeConfigureStore(
getPreloadedState({
+ ...defaultState,
+ searchSessionId: data.search.session.start(),
query: data.query.queryString.getQuery(),
filters: data.query.filterManager.getGlobalFilters(),
- searchSessionId: data.search.session.start(),
resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
- ...storePreloadedState,
+ ...preloadedState,
}),
{
data,
@@ -278,36 +415,52 @@ export function mockLensStore({
);
const origDispatch = lensStore.dispatch;
- lensStore.dispatch = jest.fn(origDispatch);
+ lensStore.dispatch = jest.fn(dispatch || origDispatch);
return lensStore;
}
export const mountWithProvider = async (
component: React.ReactElement,
- data: DataPublicPluginStart,
- storePreloadedState?: Partial,
- extraWrappingComponent?: React.FC<{
- children: React.ReactNode;
- }>
+ store?: {
+ data?: DataPublicPluginStart;
+ preloadedState?: Partial;
+ dispatch?: jest.Mock;
+ },
+ options?: {
+ wrappingComponent?: React.FC<{
+ children: React.ReactNode;
+ }>;
+ attachTo?: HTMLElement;
+ }
) => {
- const lensStore = mockLensStore({ data, storePreloadedState });
+ const lensStore = makeLensStore(store || {});
- const wrappingComponent: React.FC<{
+ let wrappingComponent: React.FC<{
children: React.ReactNode;
- }> = ({ children }) => {
- if (extraWrappingComponent) {
- return extraWrappingComponent({
- children: {children},
- });
- }
- return {children};
+ }> = ({ children }) => {children};
+
+ let restOptions: {
+ attachTo?: HTMLElement | undefined;
};
+ if (options) {
+ const { wrappingComponent: _wrappingComponent, ...rest } = options;
+ restOptions = rest;
+
+ if (_wrappingComponent) {
+ wrappingComponent = ({ children }) => {
+ return _wrappingComponent({
+ children: {children},
+ });
+ };
+ }
+ }
let instance: ReactWrapper = {} as ReactWrapper;
await act(async () => {
instance = mount(component, ({
wrappingComponent,
+ ...restOptions,
} as unknown) as ReactWrapper);
});
return { instance, lensStore };
diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx
index 6e04d1a4ff958..c82fdb2766f7e 100644
--- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx
@@ -91,11 +91,11 @@ export const getPieVisualization = ({
shape: visualizationTypeId as PieVisualizationState['shape'],
}),
- initialize(frame, state, mainPalette) {
+ initialize(addNewLayer, state, mainPalette) {
return (
state || {
shape: 'donut',
- layers: [newLayerState(frame.addNewLayer())],
+ layers: [newLayerState(addNewLayer())],
palette: mainPalette,
}
);
diff --git a/x-pack/plugins/lens/public/state_management/app_slice.ts b/x-pack/plugins/lens/public/state_management/app_slice.ts
deleted file mode 100644
index 29d5b0bee843f..0000000000000
--- a/x-pack/plugins/lens/public/state_management/app_slice.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { createSlice, PayloadAction } from '@reduxjs/toolkit';
-import { isEqual } from 'lodash';
-import { LensAppState } from './types';
-
-export const initialState: LensAppState = {
- searchSessionId: '',
- filters: [],
- query: { language: 'kuery', query: '' },
- resolvedDateRange: { fromDate: '', toDate: '' },
-
- indexPatternsForTopNav: [],
- isSaveable: false,
- isAppLoading: false,
- isLinkedToOriginatingApp: false,
-};
-
-export const appSlice = createSlice({
- name: 'app',
- initialState,
- reducers: {
- setState: (state, { payload }: PayloadAction>) => {
- return {
- ...state,
- ...payload,
- };
- },
- onChangeFromEditorFrame: (state, { payload }: PayloadAction>) => {
- return {
- ...state,
- ...payload,
- };
- },
- onActiveDataChange: (state, { payload }: PayloadAction>) => {
- if (!isEqual(state.activeData, payload?.activeData)) {
- return {
- ...state,
- ...payload,
- };
- }
- return state;
- },
- navigateAway: (state) => state,
- },
-});
-
-export const reducer = {
- app: appSlice.reducer,
-};
diff --git a/x-pack/plugins/lens/public/state_management/external_context_middleware.ts b/x-pack/plugins/lens/public/state_management/external_context_middleware.ts
index 0743dce73eb33..07233b87dd19b 100644
--- a/x-pack/plugins/lens/public/state_management/external_context_middleware.ts
+++ b/x-pack/plugins/lens/public/state_management/external_context_middleware.ts
@@ -27,7 +27,7 @@ export const externalContextMiddleware = (data: DataPublicPluginStart) => (
store.dispatch
);
return (next: Dispatch) => (action: PayloadAction>) => {
- if (action.type === 'app/navigateAway') {
+ if (action.type === 'lens/navigateAway') {
unsubscribeFromExternalContext();
}
next(action);
@@ -44,7 +44,7 @@ function subscribeToExternalContext(
const dispatchFromExternal = (searchSessionId = search.session.start()) => {
const globalFilters = filterManager.getFilters();
- const filters = isEqual(getState().app.filters, globalFilters)
+ const filters = isEqual(getState().lens.filters, globalFilters)
? null
: { filters: globalFilters };
dispatch(
@@ -64,7 +64,7 @@ function subscribeToExternalContext(
.pipe(delay(0))
// then update if it didn't get updated yet
.subscribe((newSessionId?: string) => {
- if (newSessionId && getState().app.searchSessionId !== newSessionId) {
+ if (newSessionId && getState().lens.searchSessionId !== newSessionId) {
debounceDispatchFromExternal(newSessionId);
}
});
diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts
index 429978e60756b..b72c383130208 100644
--- a/x-pack/plugins/lens/public/state_management/index.ts
+++ b/x-pack/plugins/lens/public/state_management/index.ts
@@ -8,8 +8,9 @@
import { configureStore, DeepPartial, getDefaultMiddleware } from '@reduxjs/toolkit';
import logger from 'redux-logger';
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
-import { appSlice, initialState } from './app_slice';
+import { lensSlice, initialState } from './lens_slice';
import { timeRangeMiddleware } from './time_range_middleware';
+import { optimizingMiddleware } from './optimizing_middleware';
import { externalContextMiddleware } from './external_context_middleware';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
@@ -17,19 +18,29 @@ import { LensAppState, LensState } from './types';
export * from './types';
export const reducer = {
- app: appSlice.reducer,
+ lens: lensSlice.reducer,
};
export const {
setState,
navigateAway,
- onChangeFromEditorFrame,
+ setSaveable,
onActiveDataChange,
-} = appSlice.actions;
+ updateState,
+ updateDatasourceState,
+ updateVisualizationState,
+ updateLayer,
+ switchVisualization,
+ selectSuggestion,
+ rollbackSuggestion,
+ submitSuggestion,
+ switchDatasource,
+ setToggleFullscreen,
+} = lensSlice.actions;
export const getPreloadedState = (initializedState: Partial) => {
const state = {
- app: {
+ lens: {
...initialState,
...initializedState,
},
@@ -45,15 +56,9 @@ export const makeConfigureStore = (
) => {
const middleware = [
...getDefaultMiddleware({
- serializableCheck: {
- ignoredActions: [
- 'app/setState',
- 'app/onChangeFromEditorFrame',
- 'app/onActiveDataChange',
- 'app/navigateAway',
- ],
- },
+ serializableCheck: false,
}),
+ optimizingMiddleware(),
timeRangeMiddleware(data),
externalContextMiddleware(data),
];
diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts
new file mode 100644
index 0000000000000..cce0376707143
--- /dev/null
+++ b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts
@@ -0,0 +1,148 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Query } from 'src/plugins/data/public';
+import {
+ switchDatasource,
+ switchVisualization,
+ setState,
+ updateState,
+ updateDatasourceState,
+ updateVisualizationState,
+} from '.';
+import { makeLensStore, defaultState } from '../mocks';
+
+describe('lensSlice', () => {
+ const store = makeLensStore({});
+ const customQuery = { query: 'custom' } as Query;
+
+ // TODO: need to move some initialization logic from mounter
+ // describe('initialization', () => {
+ // })
+
+ describe('state update', () => {
+ it('setState: updates state ', () => {
+ const lensState = store.getState().lens;
+ expect(lensState).toEqual(defaultState);
+ store.dispatch(setState({ query: customQuery }));
+ const changedState = store.getState().lens;
+ expect(changedState).toEqual({ ...defaultState, query: customQuery });
+ });
+
+ it('updateState: updates state with updater', () => {
+ const customUpdater = jest.fn((state) => ({ ...state, query: customQuery }));
+ store.dispatch(updateState({ subType: 'UPDATE', updater: customUpdater }));
+ const changedState = store.getState().lens;
+ expect(changedState).toEqual({ ...defaultState, query: customQuery });
+ });
+ it('should update the corresponding visualization state on update', () => {
+ const newVisState = {};
+ store.dispatch(
+ updateVisualizationState({
+ visualizationId: 'testVis',
+ updater: newVisState,
+ })
+ );
+
+ expect(store.getState().lens.visualization.state).toBe(newVisState);
+ });
+ it('should update the datasource state with passed in reducer', () => {
+ const datasourceUpdater = jest.fn(() => ({ changed: true }));
+ store.dispatch(
+ updateDatasourceState({
+ datasourceId: 'testDatasource',
+ updater: datasourceUpdater,
+ })
+ );
+ expect(store.getState().lens.datasourceStates.testDatasource.state).toStrictEqual({
+ changed: true,
+ });
+ expect(datasourceUpdater).toHaveBeenCalledTimes(1);
+ });
+ it('should update the layer state with passed in reducer', () => {
+ const newDatasourceState = {};
+ store.dispatch(
+ updateDatasourceState({
+ datasourceId: 'testDatasource',
+ updater: newDatasourceState,
+ })
+ );
+ expect(store.getState().lens.datasourceStates.testDatasource.state).toStrictEqual(
+ newDatasourceState
+ );
+ });
+ it('should should switch active visualization', () => {
+ const newVisState = {};
+ store.dispatch(
+ switchVisualization({
+ newVisualizationId: 'testVis2',
+ initialState: newVisState,
+ })
+ );
+
+ expect(store.getState().lens.visualization.state).toBe(newVisState);
+ });
+
+ it('should should switch active visualization and update datasource state', () => {
+ const newVisState = {};
+ const newDatasourceState = {};
+
+ store.dispatch(
+ switchVisualization({
+ newVisualizationId: 'testVis2',
+ initialState: newVisState,
+ datasourceState: newDatasourceState,
+ datasourceId: 'testDatasource',
+ })
+ );
+
+ expect(store.getState().lens.visualization.state).toBe(newVisState);
+ expect(store.getState().lens.datasourceStates.testDatasource.state).toBe(newDatasourceState);
+ });
+
+ it('should switch active datasource and initialize new state', () => {
+ store.dispatch(
+ switchDatasource({
+ newDatasourceId: 'testDatasource2',
+ })
+ );
+
+ expect(store.getState().lens.activeDatasourceId).toEqual('testDatasource2');
+ expect(store.getState().lens.datasourceStates.testDatasource2.isLoading).toEqual(true);
+ });
+
+ it('not initialize already initialized datasource on switch', () => {
+ const datasource2State = {};
+ const customStore = makeLensStore({
+ preloadedState: {
+ datasourceStates: {
+ testDatasource: {
+ state: {},
+ isLoading: false,
+ },
+ testDatasource2: {
+ state: datasource2State,
+ isLoading: false,
+ },
+ },
+ },
+ });
+
+ customStore.dispatch(
+ switchDatasource({
+ newDatasourceId: 'testDatasource2',
+ })
+ );
+
+ expect(customStore.getState().lens.activeDatasourceId).toEqual('testDatasource2');
+ expect(customStore.getState().lens.datasourceStates.testDatasource2.isLoading).toEqual(false);
+ expect(customStore.getState().lens.datasourceStates.testDatasource2.state).toBe(
+ datasource2State
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts
new file mode 100644
index 0000000000000..cb181881a6552
--- /dev/null
+++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts
@@ -0,0 +1,262 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { createSlice, current, PayloadAction } from '@reduxjs/toolkit';
+import { TableInspectorAdapter } from '../editor_frame_service/types';
+import { LensAppState } from './types';
+
+export const initialState: LensAppState = {
+ searchSessionId: '',
+ filters: [],
+ query: { language: 'kuery', query: '' },
+ resolvedDateRange: { fromDate: '', toDate: '' },
+ isFullscreenDatasource: false,
+ isSaveable: false,
+ isLoading: false,
+ isLinkedToOriginatingApp: false,
+ activeDatasourceId: null,
+ datasourceStates: {},
+ visualization: {
+ state: null,
+ activeId: null,
+ },
+};
+
+export const lensSlice = createSlice({
+ name: 'lens',
+ initialState,
+ reducers: {
+ setState: (state, { payload }: PayloadAction>) => {
+ return {
+ ...state,
+ ...payload,
+ };
+ },
+ onActiveDataChange: (state, { payload }: PayloadAction) => {
+ return {
+ ...state,
+ activeData: payload,
+ };
+ },
+ setSaveable: (state, { payload }: PayloadAction) => {
+ return {
+ ...state,
+ isSaveable: payload,
+ };
+ },
+ updateState: (
+ state,
+ action: {
+ payload: {
+ subType: string;
+ updater: (prevState: LensAppState) => LensAppState;
+ };
+ }
+ ) => {
+ return action.payload.updater(current(state) as LensAppState);
+ },
+ updateDatasourceState: (
+ state,
+ {
+ payload,
+ }: {
+ payload: {
+ updater: unknown | ((prevState: unknown) => unknown);
+ datasourceId: string;
+ clearStagedPreview?: boolean;
+ };
+ }
+ ) => {
+ return {
+ ...state,
+ datasourceStates: {
+ ...state.datasourceStates,
+ [payload.datasourceId]: {
+ state:
+ typeof payload.updater === 'function'
+ ? payload.updater(current(state).datasourceStates[payload.datasourceId].state)
+ : payload.updater,
+ isLoading: false,
+ },
+ },
+ stagedPreview: payload.clearStagedPreview ? undefined : state.stagedPreview,
+ };
+ },
+ updateVisualizationState: (
+ state,
+ {
+ payload,
+ }: {
+ payload: {
+ visualizationId: string;
+ updater: unknown | ((state: unknown) => unknown);
+ clearStagedPreview?: boolean;
+ };
+ }
+ ) => {
+ if (!state.visualization.activeId) {
+ throw new Error('Invariant: visualization state got updated without active visualization');
+ }
+ // This is a safeguard that prevents us from accidentally updating the
+ // wrong visualization. This occurs in some cases due to the uncoordinated
+ // way we manage state across plugins.
+ if (state.visualization.activeId !== payload.visualizationId) {
+ return state;
+ }
+ return {
+ ...state,
+ visualization: {
+ ...state.visualization,
+ state:
+ typeof payload.updater === 'function'
+ ? payload.updater(current(state.visualization.state))
+ : payload.updater,
+ },
+ stagedPreview: payload.clearStagedPreview ? undefined : state.stagedPreview,
+ };
+ },
+ updateLayer: (
+ state,
+ {
+ payload,
+ }: {
+ payload: {
+ layerId: string;
+ datasourceId: string;
+ updater: (state: unknown, layerId: string) => unknown;
+ };
+ }
+ ) => {
+ return {
+ ...state,
+ datasourceStates: {
+ ...state.datasourceStates,
+ [payload.datasourceId]: {
+ ...state.datasourceStates[payload.datasourceId],
+ state: payload.updater(
+ current(state).datasourceStates[payload.datasourceId].state,
+ payload.layerId
+ ),
+ },
+ },
+ };
+ },
+
+ switchVisualization: (
+ state,
+ {
+ payload,
+ }: {
+ payload: {
+ newVisualizationId: string;
+ initialState: unknown;
+ datasourceState?: unknown;
+ datasourceId?: string;
+ };
+ }
+ ) => {
+ return {
+ ...state,
+ datasourceStates:
+ 'datasourceId' in payload && payload.datasourceId
+ ? {
+ ...state.datasourceStates,
+ [payload.datasourceId]: {
+ ...state.datasourceStates[payload.datasourceId],
+ state: payload.datasourceState,
+ },
+ }
+ : state.datasourceStates,
+ visualization: {
+ ...state.visualization,
+ activeId: payload.newVisualizationId,
+ state: payload.initialState,
+ },
+ stagedPreview: undefined,
+ };
+ },
+ selectSuggestion: (
+ state,
+ {
+ payload,
+ }: {
+ payload: {
+ newVisualizationId: string;
+ initialState: unknown;
+ datasourceState: unknown;
+ datasourceId: string;
+ };
+ }
+ ) => {
+ return {
+ ...state,
+ datasourceStates:
+ 'datasourceId' in payload && payload.datasourceId
+ ? {
+ ...state.datasourceStates,
+ [payload.datasourceId]: {
+ ...state.datasourceStates[payload.datasourceId],
+ state: payload.datasourceState,
+ },
+ }
+ : state.datasourceStates,
+ visualization: {
+ ...state.visualization,
+ activeId: payload.newVisualizationId,
+ state: payload.initialState,
+ },
+ stagedPreview: state.stagedPreview || {
+ datasourceStates: state.datasourceStates,
+ visualization: state.visualization,
+ },
+ };
+ },
+ rollbackSuggestion: (state) => {
+ return {
+ ...state,
+ ...(state.stagedPreview || {}),
+ stagedPreview: undefined,
+ };
+ },
+ setToggleFullscreen: (state) => {
+ return { ...state, isFullscreenDatasource: !state.isFullscreenDatasource };
+ },
+ submitSuggestion: (state) => {
+ return {
+ ...state,
+ stagedPreview: undefined,
+ };
+ },
+ switchDatasource: (
+ state,
+ {
+ payload,
+ }: {
+ payload: {
+ newDatasourceId: string;
+ };
+ }
+ ) => {
+ return {
+ ...state,
+ datasourceStates: {
+ ...state.datasourceStates,
+ [payload.newDatasourceId]: state.datasourceStates[payload.newDatasourceId] || {
+ state: null,
+ isLoading: true,
+ },
+ },
+ activeDatasourceId: payload.newDatasourceId,
+ };
+ },
+ navigateAway: (state) => state,
+ },
+});
+
+export const reducer = {
+ lens: lensSlice.reducer,
+};
diff --git a/x-pack/plugins/lens/public/state_management/optimizing_middleware.ts b/x-pack/plugins/lens/public/state_management/optimizing_middleware.ts
new file mode 100644
index 0000000000000..63e59221a683a
--- /dev/null
+++ b/x-pack/plugins/lens/public/state_management/optimizing_middleware.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit';
+import { isEqual } from 'lodash';
+import { LensAppState } from './types';
+
+/** cancels updates to the store that don't change the state */
+export const optimizingMiddleware = () => (store: MiddlewareAPI) => {
+ return (next: Dispatch) => (action: PayloadAction>) => {
+ if (action.type === 'lens/onActiveDataChange') {
+ if (isEqual(store.getState().lens.activeData, action.payload)) {
+ return;
+ }
+ }
+ next(action);
+ };
+};
diff --git a/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts
index 4145f8ed5e52c..a3a53a6d380ed 100644
--- a/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts
+++ b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts
@@ -17,10 +17,9 @@ import { timeRangeMiddleware } from './time_range_middleware';
import { Observable, Subject } from 'rxjs';
import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public';
import moment from 'moment';
-import { initialState } from './app_slice';
+import { initialState } from './lens_slice';
import { LensAppState } from './types';
import { PayloadAction } from '@reduxjs/toolkit';
-import { Document } from '../persistence';
const sessionIdSubject = new Subject();
@@ -132,7 +131,7 @@ function makeDefaultData(): jest.Mocked {
const createMiddleware = (data: DataPublicPluginStart) => {
const middleware = timeRangeMiddleware(data);
const store = {
- getState: jest.fn(() => ({ app: initialState })),
+ getState: jest.fn(() => ({ lens: initialState })),
dispatch: jest.fn(),
};
const next = jest.fn();
@@ -157,8 +156,13 @@ describe('timeRangeMiddleware', () => {
});
const { next, invoke, store } = createMiddleware(data);
const action = {
- type: 'app/setState',
- payload: { lastKnownDoc: ('new' as unknown) as Document },
+ type: 'lens/setState',
+ payload: {
+ visualization: {
+ state: {},
+ activeId: 'id2',
+ },
+ },
};
invoke(action);
expect(store.dispatch).toHaveBeenCalledWith({
@@ -169,7 +173,7 @@ describe('timeRangeMiddleware', () => {
},
searchSessionId: 'sessionId-1',
},
- type: 'app/setState',
+ type: 'lens/setState',
});
expect(next).toHaveBeenCalledWith(action);
});
@@ -187,8 +191,39 @@ describe('timeRangeMiddleware', () => {
});
const { next, invoke, store } = createMiddleware(data);
const action = {
- type: 'app/setState',
- payload: { lastKnownDoc: ('new' as unknown) as Document },
+ type: 'lens/setState',
+ payload: {
+ visualization: {
+ state: {},
+ activeId: 'id2',
+ },
+ },
+ };
+ invoke(action);
+ expect(store.dispatch).not.toHaveBeenCalled();
+ expect(next).toHaveBeenCalledWith(action);
+ });
+ it('does not trigger another update when the update already contains searchSessionId', () => {
+ const data = makeDefaultData();
+ (data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000));
+ (data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({
+ from: 'now-2m',
+ to: 'now',
+ });
+ (data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({
+ min: moment(Date.now() - 100000),
+ max: moment(Date.now() - 30000),
+ });
+ const { next, invoke, store } = createMiddleware(data);
+ const action = {
+ type: 'lens/setState',
+ payload: {
+ visualization: {
+ state: {},
+ activeId: 'id2',
+ },
+ searchSessionId: 'searchSessionId',
+ },
};
invoke(action);
expect(store.dispatch).not.toHaveBeenCalled();
diff --git a/x-pack/plugins/lens/public/state_management/time_range_middleware.ts b/x-pack/plugins/lens/public/state_management/time_range_middleware.ts
index a6c868be60565..cc3e46b71fbfc 100644
--- a/x-pack/plugins/lens/public/state_management/time_range_middleware.ts
+++ b/x-pack/plugins/lens/public/state_management/time_range_middleware.ts
@@ -5,27 +5,26 @@
* 2.0.
*/
-import { isEqual } from 'lodash';
import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit';
import moment from 'moment';
-
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { setState, LensDispatch } from '.';
import { LensAppState } from './types';
import { getResolvedDateRange, containsDynamicMath, TIME_LAG_PERCENTAGE_LIMIT } from '../utils';
+/**
+ * checks if TIME_LAG_PERCENTAGE_LIMIT passed to renew searchSessionId
+ * and request new data.
+ */
export const timeRangeMiddleware = (data: DataPublicPluginStart) => (store: MiddlewareAPI) => {
return (next: Dispatch) => (action: PayloadAction>) => {
- // if document was modified or sessionId check if too much time passed to update searchSessionId
- if (
- action.payload?.lastKnownDoc &&
- !isEqual(action.payload?.lastKnownDoc, store.getState().app.lastKnownDoc)
- ) {
+ if (!action.payload?.searchSessionId) {
updateTimeRange(data, store.dispatch);
}
next(action);
};
};
+
function updateTimeRange(data: DataPublicPluginStart, dispatch: LensDispatch) {
const timefilter = data.query.timefilter.timefilter;
const unresolvedTimeRange = timefilter.getTime();
diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts
index 87045d15cc994..1c696a3d79f9d 100644
--- a/x-pack/plugins/lens/public/state_management/types.ts
+++ b/x-pack/plugins/lens/public/state_management/types.ts
@@ -5,24 +5,33 @@
* 2.0.
*/
-import { Filter, IndexPattern, Query, SavedQuery } from '../../../../../src/plugins/data/public';
+import { Filter, Query, SavedQuery } from '../../../../../src/plugins/data/public';
import { Document } from '../persistence';
import { TableInspectorAdapter } from '../editor_frame_service/types';
import { DateRange } from '../../common';
-export interface LensAppState {
+export interface PreviewState {
+ visualization: {
+ activeId: string | null;
+ state: unknown;
+ };
+ datasourceStates: Record;
+}
+export interface EditorFrameState extends PreviewState {
+ activeDatasourceId: string | null;
+ stagedPreview?: PreviewState;
+ isFullscreenDatasource?: boolean;
+}
+export interface LensAppState extends EditorFrameState {
persistedDoc?: Document;
- lastKnownDoc?: Document;
- // index patterns used to determine which filters are available in the top nav.
- indexPatternsForTopNav: IndexPattern[];
// Determines whether the lens editor shows the 'save and return' button, and the originating app breadcrumb.
isLinkedToOriginatingApp?: boolean;
isSaveable: boolean;
activeData?: TableInspectorAdapter;
- isAppLoading: boolean;
+ isLoading: boolean;
query: Query;
filters: Filter[];
savedQuery?: SavedQuery;
@@ -38,5 +47,5 @@ export type DispatchSetState = (
};
export interface LensState {
- app: LensAppState;
+ lens: LensAppState;
}
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index 7baba15f0fac6..cb47dcf6ec388 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -7,7 +7,7 @@
import { IconType } from '@elastic/eui/src/components/icon/icon';
import { CoreSetup } from 'kibana/public';
-import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
+import { PaletteOutput } from 'src/plugins/charts/public';
import { SavedObjectReference } from 'kibana/public';
import { MutableRefObject } from 'react';
import { RowClickContext } from '../../../../src/plugins/ui_actions/public';
@@ -45,13 +45,13 @@ export interface PublicAPIProps {
}
export interface EditorFrameProps {
- onError: ErrorCallback;
- initialContext?: VisualizeFieldContext;
showNoDataPopover: () => void;
}
export interface EditorFrameInstance {
EditorFrameContainer: (props: EditorFrameProps) => React.ReactElement;
+ datasourceMap: Record;
+ visualizationMap: Record;
}
export interface EditorFrameSetup {
@@ -525,20 +525,10 @@ export interface FramePublicAPI {
* If accessing, make sure to check whether expected columns actually exist.
*/
activeData?: Record;
-
dateRange: DateRange;
query: Query;
filters: Filter[];
searchSessionId: string;
-
- /**
- * A map of all available palettes (keys being the ids).
- */
- availablePalettes: PaletteRegistry;
-
- // Adds a new layer. This has a side effect of updating the datasource state
- addNewLayer: () => string;
- removeLayers: (layerIds: string[]) => void;
}
/**
@@ -586,7 +576,7 @@ export interface Visualization {
* - Loadingn from a saved visualization
* - When using suggestions, the suggested state is passed in
*/
- initialize: (frame: FramePublicAPI, state?: T, mainPalette?: PaletteOutput) => T;
+ initialize: (addNewLayer: () => string, state?: T, mainPalette?: PaletteOutput) => T;
getMainPalette?: (state: T) => undefined | PaletteOutput;
diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts
index 1c4b2c67f96fc..a79480d7d9953 100644
--- a/x-pack/plugins/lens/public/utils.ts
+++ b/x-pack/plugins/lens/public/utils.ts
@@ -9,6 +9,12 @@ import { i18n } from '@kbn/i18n';
import { IndexPattern, IndexPatternsContract, TimefilterContract } from 'src/plugins/data/public';
import { IUiSettingsClient } from 'kibana/public';
import moment from 'moment-timezone';
+import { SavedObjectReference } from 'kibana/public';
+import { Filter, Query } from 'src/plugins/data/public';
+import { uniq } from 'lodash';
+import { Document } from './persistence/saved_object_store';
+import { Datasource } from './types';
+import { extractFilterReferences } from './persistence';
export function getVisualizeGeoFieldMessage(fieldType: string) {
return i18n.translate('xpack.lens.visualizeGeoFieldMessage', {
@@ -32,7 +38,105 @@ export function containsDynamicMath(dateMathString: string) {
export const TIME_LAG_PERCENTAGE_LIMIT = 0.02;
-export async function getAllIndexPatterns(
+export function getTimeZone(uiSettings: IUiSettingsClient) {
+ const configuredTimeZone = uiSettings.get('dateFormat:tz');
+ if (configuredTimeZone === 'Browser') {
+ return moment.tz.guess();
+ }
+
+ return configuredTimeZone;
+}
+export function getActiveDatasourceIdFromDoc(doc?: Document) {
+ if (!doc) {
+ return null;
+ }
+
+ const [firstDatasourceFromDoc] = Object.keys(doc.state.datasourceStates);
+ return firstDatasourceFromDoc || null;
+}
+
+export const getInitialDatasourceId = (
+ datasourceMap: Record,
+ doc?: Document
+) => {
+ return (doc && getActiveDatasourceIdFromDoc(doc)) || Object.keys(datasourceMap)[0] || null;
+};
+
+export interface GetIndexPatternsObjects {
+ activeDatasources: Record;
+ datasourceStates: Record;
+ visualization: {
+ activeId: string | null;
+ state: unknown;
+ };
+ filters: Filter[];
+ query: Query;
+ title: string;
+ description?: string;
+ persistedId?: string;
+}
+
+export function getSavedObjectFormat({
+ activeDatasources,
+ datasourceStates,
+ visualization,
+ filters,
+ query,
+ title,
+ description,
+ persistedId,
+}: GetIndexPatternsObjects): Document {
+ const persistibleDatasourceStates: Record = {};
+ const references: SavedObjectReference[] = [];
+ Object.entries(activeDatasources).forEach(([id, datasource]) => {
+ const { state: persistableState, savedObjectReferences } = datasource.getPersistableState(
+ datasourceStates[id].state
+ );
+ persistibleDatasourceStates[id] = persistableState;
+ references.push(...savedObjectReferences);
+ });
+
+ const { persistableFilters, references: filterReferences } = extractFilterReferences(filters);
+
+ references.push(...filterReferences);
+
+ return {
+ savedObjectId: persistedId,
+ title,
+ description,
+ type: 'lens',
+ visualizationType: visualization.activeId,
+ state: {
+ datasourceStates: persistibleDatasourceStates,
+ visualization: visualization.state,
+ query,
+ filters: persistableFilters,
+ },
+ references,
+ };
+}
+
+export function getIndexPatternsIds({
+ activeDatasources,
+ datasourceStates,
+}: {
+ activeDatasources: Record;
+ datasourceStates: Record;
+}): string[] {
+ const references: SavedObjectReference[] = [];
+ Object.entries(activeDatasources).forEach(([id, datasource]) => {
+ const { savedObjectReferences } = datasource.getPersistableState(datasourceStates[id].state);
+ references.push(...savedObjectReferences);
+ });
+
+ const uniqueFilterableIndexPatternIds = uniq(
+ references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
+ );
+
+ return uniqueFilterableIndexPatternIds;
+}
+
+export async function getIndexPatternsObjects(
ids: string[],
indexPatternsService: IndexPatternsContract
): Promise<{ indexPatterns: IndexPattern[]; rejectedIds: string[] }> {
@@ -46,12 +150,3 @@ export async function getAllIndexPatterns(
// return also the rejected ids in case we want to show something later on
return { indexPatterns: fullfilled.map((response) => response.value), rejectedIds };
}
-
-export function getTimeZone(uiSettings: IUiSettingsClient) {
- const configuredTimeZone = uiSettings.get('dateFormat:tz');
- if (configuredTimeZone === 'Browser') {
- return moment.tz.guess();
- }
-
- return configuredTimeZone;
-}
diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
index b88d38e18329c..a7270bdf8f331 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
@@ -10,7 +10,7 @@ import { Position } from '@elastic/charts';
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
import { getXyVisualization } from './xy_visualization';
import { Operation } from '../types';
-import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
+import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
describe('#toExpression', () => {
diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx
index b46ad1940491e..ec0c11a0b1d86 100644
--- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { shallowWithIntl as shallow } from '@kbn/test/jest';
import { Position } from '@elastic/charts';
import { FramePublicAPI } from '../../types';
-import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks';
+import { createMockDatasource, createMockFramePublicAPI } from '../../mocks';
import { State } from '../types';
import { VisualOptionsPopover } from './visual_options_popover';
import { ToolbarPopover } from '../../shared_components';
diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
index dee0e5763dee4..304e323789c14 100644
--- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
@@ -9,7 +9,7 @@ import { getXyVisualization } from './visualization';
import { Position } from '@elastic/charts';
import { Operation } from '../types';
import { State, SeriesType, XYLayerConfig } from './types';
-import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
+import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
import { LensIconChartBar } from '../assets/chart_bar';
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
@@ -132,8 +132,7 @@ describe('xy_visualization', () => {
describe('#initialize', () => {
it('loads default state', () => {
- const mockFrame = createMockFramePublicAPI();
- const initialState = xyVisualization.initialize(mockFrame);
+ const initialState = xyVisualization.initialize(() => 'l1');
expect(initialState.layers).toHaveLength(1);
expect(initialState.layers[0].xAccessor).not.toBeDefined();
@@ -144,7 +143,7 @@ describe('xy_visualization', () => {
"layers": Array [
Object {
"accessors": Array [],
- "layerId": "",
+ "layerId": "l1",
"position": "top",
"seriesType": "bar_stacked",
"showGridlines": false,
@@ -162,9 +161,7 @@ describe('xy_visualization', () => {
});
it('loads from persisted state', () => {
- expect(xyVisualization.initialize(createMockFramePublicAPI(), exampleState())).toEqual(
- exampleState()
- );
+ expect(xyVisualization.initialize(() => 'first', exampleState())).toEqual(exampleState());
});
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
index bd20ed300bf61..199dccdf702f7 100644
--- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
@@ -152,7 +152,7 @@ export const getXyVisualization = ({
getSuggestions,
- initialize(frame, state) {
+ initialize(addNewLayer, state) {
return (
state || {
title: 'Empty XY chart',
@@ -161,7 +161,7 @@ export const getXyVisualization = ({
preferredSeriesType: defaultSeriesType,
layers: [
{
- layerId: frame.addNewLayer(),
+ layerId: addNewLayer(),
accessors: [],
position: Position.Top,
seriesType: defaultSeriesType,
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
index bc10236cf1977..9292a8d87bbc4 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
@@ -13,7 +13,7 @@ import { AxisSettingsPopover } from './axis_settings_popover';
import { FramePublicAPI } from '../types';
import { State } from './types';
import { Position } from '@elastic/charts';
-import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks';
+import { createMockFramePublicAPI, createMockDatasource } from '../mocks';
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
import { EuiColorPicker } from '@elastic/eui';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
index dbe9cd163451d..ded56ec9e817f 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx
@@ -99,7 +99,6 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
{isSaveOpen && lensAttributes && (
setIsSaveOpen(false)}
onSave={() => {}}
diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts
index 4157f31525acf..fce15b34a77e4 100644
--- a/x-pack/test/accessibility/apps/lens.ts
+++ b/x-pack/test/accessibility/apps/lens.ts
@@ -142,8 +142,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.lens.configureDimension(
{
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
- operation: 'terms',
- field: 'ip',
+ operation: 'date_histogram',
+ field: '@timestamp',
},
1
);
diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts
index 844b074e42e74..6e4c20744c5fc 100644
--- a/x-pack/test/functional/apps/lens/dashboard.ts
+++ b/x-pack/test/functional/apps/lens/dashboard.ts
@@ -223,10 +223,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// remove the x dimension to trigger the validation error
await PageObjects.lens.removeDimension('lnsXY_xDimensionPanel');
- await PageObjects.lens.saveAndReturn();
-
- await PageObjects.header.waitUntilLoadingHasFinished();
- await testSubjects.existOrFail('embeddable-lens-failure');
+ await PageObjects.lens.expectSaveAndReturnButtonDisabled();
});
});
}
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index 0fc85f78ac90b..d02bc591a80a2 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -491,6 +491,12 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await testSubjects.click('lnsApp_saveAndReturnButton');
},
+ async expectSaveAndReturnButtonDisabled() {
+ const button = await testSubjects.find('lnsApp_saveAndReturnButton', 10000);
+ const disabledAttr = await button.getAttribute('disabled');
+ expect(disabledAttr).to.be('true');
+ },
+
async editDimensionLabel(label: string) {
await testSubjects.setValue('indexPattern-label-edit', label, { clearWithKeyboard: true });
},
From 6c328cc8e07c193308b1b9d6187345e1a04b489f Mon Sep 17 00:00:00 2001
From: Sergi Massaneda
Date: Thu, 1 Jul 2021 11:12:20 +0200
Subject: [PATCH 29/51] [Security Solutions] Administration breadcrumbs
shortened to be consistent with the rest (#103927)
* Administration breadcrumbs shortened to bbe consistent with the rest
* remove comment
---
.../navigation/breadcrumbs/index.test.ts | 11 +++++++----
.../components/navigation/breadcrumbs/index.ts | 13 +------------
.../public/management/common/breadcrumbs.ts | 17 +----------------
3 files changed, 9 insertions(+), 32 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts
index 6789d8e1d4524..1f7e668b21b98 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts
@@ -13,6 +13,7 @@ import { RouteSpyState, SiemRouteType } from '../../../utils/route/types';
import { TabNavigationProps } from '../tab_navigation/types';
import { NetworkRouteType } from '../../../../network/pages/navigation/types';
import { TimelineTabs } from '../../../../../common/types/timeline';
+import { AdministrationSubTab } from '../../../../management/types';
const setBreadcrumbsMock = jest.fn();
const chromeMock = {
@@ -26,6 +27,8 @@ const mockDefaultTab = (pageName: string): SiemRouteType | undefined => {
return HostsTableType.authentications;
case 'network':
return NetworkRouteType.flows;
+ case 'administration':
+ return AdministrationSubTab.endpoints;
default:
return undefined;
}
@@ -423,16 +426,16 @@ describe('Navigation Breadcrumbs', () => {
},
]);
});
- test('should return Admin breadcrumbs when supplied admin pathname', () => {
+ test('should return Admin breadcrumbs when supplied endpoints pathname', () => {
const breadcrumbs = getBreadcrumbsForRoute(
- getMockObject('administration', '/', undefined),
+ getMockObject('administration', '/endpoints', undefined),
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolution/overview' },
{
- text: 'Administration',
- href: 'securitySolution/endpoints',
+ text: 'Endpoints',
+ href: '',
},
]);
});
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
index 4578e16dc5540..03ee38473e58d 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
@@ -186,18 +186,7 @@ export const getBreadcrumbsForRoute = (
if (spyState.tabName != null) {
urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)];
}
-
- return [
- siemRootBreadcrumb,
- ...getAdminBreadcrumbs(
- spyState,
- urlStateKeys.reduce(
- (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)],
- []
- ),
- getUrlForApp
- ),
- ];
+ return [siemRootBreadcrumb, ...getAdminBreadcrumbs(spyState)];
}
if (
diff --git a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts
index d437c45792766..9c3d781f514e9 100644
--- a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts
+++ b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts
@@ -6,13 +6,9 @@
*/
import { ChromeBreadcrumb } from 'kibana/public';
-import { isEmpty } from 'lodash/fp';
import { AdministrationSubTab } from '../types';
import { ENDPOINTS_TAB, EVENT_FILTERS_TAB, POLICIES_TAB, TRUSTED_APPS_TAB } from './translations';
import { AdministrationRouteSpyState } from '../../common/utils/route/types';
-import { GetUrlForApp } from '../../common/components/navigation/types';
-import { ADMINISTRATION } from '../../app/translations';
-import { APP_ID, SecurityPageName } from '../../../common/constants';
const TabNameMappedToI18nKey: Record = {
[AdministrationSubTab.endpoints]: ENDPOINTS_TAB,
@@ -21,19 +17,8 @@ const TabNameMappedToI18nKey: Record = {
[AdministrationSubTab.eventFilters]: EVENT_FILTERS_TAB,
};
-export function getBreadcrumbs(
- params: AdministrationRouteSpyState,
- search: string[],
- getUrlForApp: GetUrlForApp
-): ChromeBreadcrumb[] {
+export function getBreadcrumbs(params: AdministrationRouteSpyState): ChromeBreadcrumb[] {
return [
- {
- text: ADMINISTRATION,
- href: getUrlForApp(APP_ID, {
- deepLinkId: SecurityPageName.endpoints,
- path: !isEmpty(search[0]) ? search[0] : '',
- }),
- },
...(params?.tabName ? [params?.tabName] : []).map((tabName) => ({
text: TabNameMappedToI18nKey[tabName],
href: '',
From c52e0e12aa6575cbb2f968b76f683eaac2e0d6af Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 1 Jul 2021 12:34:28 +0300
Subject: [PATCH 30/51] [TSVB] Documents the new index pattern mode (#102880)
* [TSVB] Document the new index pattern mode
* Add a callout to TSVB to advertise the new index pattern mode
* Conditionally render the callout, give capability to dismiss it
* Fix i18n
* Update the notification texts
* Update notification text
* Change callout storage key
* add UseIndexPatternModeCallout component
* Update docs/user/dashboard/tsvb.asciidoc
Co-authored-by: Kaarina Tungseth
* Update docs/user/dashboard/tsvb.asciidoc
Co-authored-by: Kaarina Tungseth
* Update docs/user/dashboard/tsvb.asciidoc
Co-authored-by: Kaarina Tungseth
* Update docs/user/dashboard/tsvb.asciidoc
Co-authored-by: Kaarina Tungseth
* Update docs/user/dashboard/tsvb.asciidoc
Co-authored-by: Kaarina Tungseth
* Update docs/user/dashboard/tsvb.asciidoc
Co-authored-by: Kaarina Tungseth
* Update docs/user/dashboard/tsvb.asciidoc
Co-authored-by: Kaarina Tungseth
* Update docs/user/dashboard/tsvb.asciidoc
Co-authored-by: Kaarina Tungseth
* Final docs changes
* Remove TSVB from title
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Alexey Antonov
Co-authored-by: Kaarina Tungseth
---
.../tsvb_index_pattern_selection_mode.png | Bin 0 -> 130582 bytes
docs/user/dashboard/tsvb.asciidoc | 25 ++++++-
.../public/doc_links/doc_links_service.ts | 1 +
.../use_index_patter_mode_callout.tsx | 69 ++++++++++++++++++
.../application/components/vis_editor.tsx | 3 +-
5 files changed, 96 insertions(+), 2 deletions(-)
create mode 100644 docs/user/dashboard/images/tsvb_index_pattern_selection_mode.png
create mode 100644 src/plugins/vis_type_timeseries/public/application/components/use_index_patter_mode_callout.tsx
diff --git a/docs/user/dashboard/images/tsvb_index_pattern_selection_mode.png b/docs/user/dashboard/images/tsvb_index_pattern_selection_mode.png
new file mode 100644
index 0000000000000000000000000000000000000000..ef72f291850e48f9f5c9e943f6ba8e9d4e70e867
GIT binary patch
literal 130582
zcmeFZby!s2_CHPu0uqXVlpqQS2uMpKh;&GINau`nrywc<(mhD$(A_8^jUWv}GxX3y
z&+p*9_kG`=H$Knrzwcjnp64)U&OW==UVFuBt<8HCC20aYN<1_)Gy++f7piDzShZ+q
z7;`tVfhX~K{YPkMcra^8NflX1NjeorkcG9KIU1VG``CCK6}2@of0Wm-q=c+Q7KtZG
z7KymrO*a2PXX4mMtUGeInA68&J4p5MS?JTDs|BHf=FG1ppNx}rz{t&RtBiI867YHj
zNRQ9(Bbq!cR|Za2md4Ds+exknSFjA#O{>v#ko`F~D`>1{p4c#@8R_Z0JK
zJX3UOMg~0^J;^Cz?~>jBJZC_Qw7$tbe0hg^5wtt3zFRkai!=hs
z%XlpQSs|*=;MnheA1W@zuHyxvyxHfsLxWZ?Ht67PVz$1uA3ChLA2Mlwqur-9{X);<
z5a_|>x`(x27cgJYG{OCl@tMKh-d7!;onA9()8T(eG+sNm(ttWMC-b)HubCN2X{L0u
z-vz7h;=e7!B}rlW01;>_FG|>vrLFE}*LYF5P97?W!MKlI=sEEDBHQS;bd?r+7=gxN
ze`=8torU7}7={9yMSP4PMxO=s%R3(aEb)(oTkJ75T!d*GJk4E0UG3QuIDSDH?-EzKgho%;mFe)MUL`0`+3HEs2aEKrpRiRGXX>PP{ZbrK
zB~n`H$V{rA@SinrmH+h!uLIInRR_!hG*Ub(dV|L?hV^2;*nf4PJ
z+u+02kk`?riQ`h^RpVRZo_4I&BvU~&-{-;g&xBHl*za_9biI+PwXY4T<*rSxHJp}i
zq=el=gb{sbTI@KEJ?1(`Kes+t^rrQFn=V^Q=YE?W*SlS!-Lw6^1pd6>m-wd=zvy*u
z`-GN#Q&_-zEl?Kj9&ns+tf{Ejr{1S#AD1L!U#D9)X;LL`Gh+jhxf{jyUQzA=v&@Ff
znM`pOPF9T>*F@B&vJzuY*TxUtC8wp_rHFNP7aW&xK_j0;&uq`kQ^i9qN-P3=g1GPc
zpD;dgeKMsS3-G~(6y~1v@#f7&z*Z57ej{!
zQI(lc6p&u~l96Yj&KE0fAT=Y|B)*kxVy($T$QG?BA@pJ+oxobH(x7Uwirh-e1~Rmr
zk+~Y@Oem7voaP(j8-MBXVgByu-L@EBwe_6ol)dNDMx92|>1su4B~H@t3U*0;8Gd=U
zOo!-g5|PTZV%U==?r>dN@Wtzk%u^x)V@$mus~{=t1_CI>EhW&?fC@ld5jf!k9f%_KzyR6=Ye1Pj5*6Gmc#bL9*|&0tB?eDu}2d|
z-&SKCel{GNbC{#t8)~6Y;at&YVQ(S1@_mK7mppkaNvx7i`zhO7GCj6QHc@R|9Y$?v
zwO#pU%~08bHmUZAu1Gbb-kAu=KiMbRs+o{;cY@znj~sNMQ%u(u>E4?k@YO~40Qz!@|mb(
z#jYRH9<(ee+gl0?6
zh=_cmG0S-6c#g09BS`64b}f~J7dB&Ua^$c@>HGk_-NUTgY}|7cCfs~|?!v>bbKEV|9)&KwaQNVT$DA~l%H=hgtLCqw
zZk>V5)C_Xznobqdl@97q4_YeX2GOHeDK07WP&UJdq72Q9my#0364nx03zgp(-fKl4
zM7@yh>#AKkUTW(4S`lOcN}Nevh*OPmbPFFzx#7e{qFf`lnjU`mLYB!X4Gk!ge)
zT83C`_nt?RCRTCCC#FUkK!OHj`vll?LMy((;Z%J_Pg~SpGciXwFMBRKbkRhG$RoSy
zCa85PQbKOYjolsnfVey3(d-fHLA$@WAHg=LOJ@kS|FYH2i+_d}i0`ZW*6^F&uj+BL
z-47iz^4jvQY{79kZ1#E<)kR<(#9rj`nHI5@S@E%X_G0%?EJBl^$cCBciN4|ZzQGT-
zDyem;p$xCWCN0GI{fnm6vv6K^+v?&I&0daX)dg);%{_BA&{iyMitl68^E!4PgHBK6
zp%9-je<=U-%xcXWzkE&W=hg!`6v>qQ+crEh6eQsi<-Psd6C#utlz@TousN<-vsvd%6S>vw21QJUWQIVR2Ry{~z8q(6cV0jeAd_ND
z-rH1NsK7JT%aIk^o|+v{M67^Z_FLBF@J|?z&Dz%1&>AP+^>)glseGmgeCFlde4oVX
zJlQy`)o)>!!dqb)Jx}u2s?|4#8*@*~pTuLP&l9z-YTw-;GA1VZ`+$_&>Q|Gs@aZa4XP(DM5>#ZXu3|F@Y7eE63v_zJ?PH8$0l@n7^ukIL==a}
zrCQKWJpDGYL&%yI?aje^xORZS`@vjW)L1-{ix9nH-hoUB03&nnr!01t4%GCEFZXk-jm7rLzKqaC3AF>7^gXKh6VK~s=D
zyNMa-wK==HJ@~2}G+}o^;MU&U*@VvB-p;{E&|QT7_Zx!1{ncX*db;1QINOTQYb&bI
zNrD{B>3G?B*q_pi;?dF32|Jou2&%q#`S0q$FA;hxXJ@b=2Zx)R8@n4fJIK+JgHu32
zfa56_2NxF`@CKWchl8_;JDY>k!+$mMpLSlDJDECKgPpBG4s=)Tn!E1?
z=U?MAcenm;PYzE1o)$1cj;j(5PWGo9f3*!%6~1~ZsABDIZm0di+8&@8(1$1|*VAXh
zzbjlT`fr#2s;c2+?kEYe2WmQt{+H_iuKf4He^&h6r_O)-1Z33TyZl?pziSF}TuuFN
zr1%$|e?J8XEs7`1@z;_=bOy#>~h+WLjEI`9o}+0_UAYIPs~>-*|Hf7(8)PX`T6
z98LDcb9Hz0%^BQAg+o;5ZiIo3yeqB_{+G}XQQ3Kq-{w7vl?lE3<#Csx9R6oH+*d#E
z@3GpW$i9>#^hP%)+EHf$W5{RRZWi0qUT{at6&Fu%f6Ad(+U%kQta%pE1wKE1A5Fn+
zLClCnERKf$&p#Qj=)THwOPRJYb7S13^GEyV9~SY}4yWsnZ_*KuU|{LLGVr;3|5}^G
z;{N_0EwA@QEKXN&<7RC}{pn-jfA(|LH#+&iwf=$U;_CRsU@4cwSML8D{MCdo*wg+n
z0JMNSGP=1B>+6|2f0`ARzA)yWp1)!fZzUZWk)FT(2U6Xvb;tfA33dJbSqlmy+8_Rb
zPQ>8dTYn(y%@vK&Kpbj=jseYsisD85flk0easOce|68cP
zD4FrUh58qR{6F1M;89FyOWc##l9n`bzSlVliP_=-=cC@)DzF40belwgG>lmy%YN0zenD8
zU-8p&e(tH<=l0;H!r_j+{!}6Jtw|U39@X)f8tt+S^qR6ef9keYy!VE_XQ6=OQe;_4
z_~&|;5&~birAbdhyiqJXz&-+J<
zIuH_Y>LE)y+BK;|9E8D11Ge@dpxG;$o;V73e&vFtzm`Gy80(sd{FWeIPgGlA^rlYU
z_h6h;r^=>^TG)NifV{uGvCc?tqTIZFf>#mwU`|fWP$~SHhG|qiQA6eCPHt);
zwHSK4;g^WgvbT(q%1Tk+^+fOm%Qdprd)J8L;KFWNe9LYW<
z)slfVdSIjmx?CIjl}35t(`tUm)WWafDKgsWtH4v@=40S-R*d_8)Juz6%vaxHAdQtH
z+(U|tO?PwVuJg%yp^a>bp@@RNb3JRe8(a_6AYzrbJi{@#$G>kOzcxgUGr8nEoWf@x
z(sg)T*9y=r~k>SpdPXrkziU2ho&
zTfTuWPw1~JfHfsL3k>$yRMZS|=rp_`keSK%=%L%CM;2x7keu4+g!*E5A=wi
zv9?+q(M38HjFX}(%Ua8I55AHVkCE2YSTS-4i_y>V8F=h1$>!@JuyyJjHClZNG>Yj5
zA}F`HA77jvQe}C39DR=Sr9@*xE>U!=$scaYa7|Fq5wl@%l7oY7ma_szKA8I*|LU9B
z68xczLOQi_7JqN%d+E@eUp8=gYSI(Orh3tBd#AXDwK*A874zg(yRS1mAGg+ZYm%V`
zHmX`}KYPElXm$X?#2}}?WZ1*rn{ra-ynUxOHm>?ew{eHOfz{~`3&wKeHT&G@O7h9j
z_gnX9?R0A_>dd;L7=Bv#;4e1&oGn58Tzk@*yp~sgW~vsh#J)coCKV(b?J#G-n$BSr
zOX1Y8`IVPuYdlTPsc8Ozc4y(Oqr`zHh
zJSE6BE?V>lYig+CN}5A_kLrX6cfX{sB=Z%;-J;;anz?TigssP_1&z@%qGIB<^tt0a
zMuAZNoWyJUX!3r(KkLu;m__3aZjdMXwKCX56lBhWJ|bcMtZ4TOuQ3H(0TwV{N+E|v
z;R}TYeg>Q3RsStPu$Ss^Tnj30q)@YGkCfFsHW4&l`)U+wz(mDor?9SKd6tM*TRAdO
z@3N7~#covA&b%ndy*;`7!AS}x@IKX{K%Tx$pRBOxkL)SbaH*jc_13oeRYap{`uX4a
zBepG4$7B8LCw{(|HziF^5`pPe>fmH?7-lGB9J)&BV(HiLSTo}L8d-77bJOmqi`Ai;
zy?jO|hyWoSKRvigcE*yF6(u6?#9XxoD=js$ZO;~a-k5kGmF}s?L+Cx#XX*{
zn3W`Qi;oaZTCV8#P1u7y%J=CTIm#UH?iw;5N;lcr4rIoyrQ|E(AYlsIuM0;?PVB`7
zhOi6jFEgvUZMNWSF~b9&$sA4~s|-uvZ0U7!Ff5}7gdcvn_xf!BGqo;$Na
z<))oVIu#bhPmIb`$(|gGSgkJJHzT{!h{9k$90ZG{fI46&)@o(F$)qBoaoQZ`v>t8L
z7#vmi6fgC+-K^c+JKzo?NmbJV%tqxEKW?qPr7jwSt0NjkUU6CUCagM}8v1IaQE>_X^fdSqFzim^sKFgH
zWcgd@Pn0hYA-d4_79&vPJNFv6v|D+-`t|E_FD_hbUhGo~ILZWJA3pk6XQ;!S?gLjM
zsy(Z8aH{Erpi<8!8q$%^=DK_{Ohe3nM7B+2jja#o`V9u`Z0%y{YX%BjllU(R#T9V$
zDOLMU*14Hg==@qw6S@w3X1DXlVCOZYT>mi%%mPo~ZVeQuJpkb}1MvL+J;qGO>?mBqdhQ6*I$eI-@ql~5h
znQYnnnsfu_+<473w|;0vkkq?z`0y66c}p$x(4;XoC@+8McYaSt>8>kKbad!
zZ9&aTTTc|&w=SM*`5w%lsa|~OnHj-^_-cM{5^cMn?w8nIAeUQFNDQhLeK?0F{n_U_
ztBc@&Q)tbe+26MFddCBDE!6jZlW&&T^3*fz7A+^srYX$RsgYvN_e+@X8--R=3BWIC
zEi7qb4@&c4pDTmK4hUbwOiwkZHptCVddyy=nr)BmPrG~}lZ$=A3~J&v1Y|pfvx+wc
zR3*?(@;aXrKifbbn~H|TiYgxvP04kB>yAfwGwW7{-euN*fk)2%XiVQg-e#m=jS!pm
zs7qJ~YS{E0w@&nO>7Z6Ct*?J!i!DiZ3`+DKTl$McS%jqz-AIAvuGlnuA8()R$E$&e
zIF&K{qa*1p=<)R6Z9s~=hB!?U+5EapYsl`AB$9*b*JVld2n7;`DYu%BBwPb9#4GVq
z4*gl1+Gw7E#rf=}A5o2hz}Rv8B@8j`!FYlo-ri(>Lofc*w=J7}WTI{Mse$EYh+
zsOB!$*f1K0eB6@^dzbnRYxal_?YD7O{aV>kg->Ka~Bi-v8+G54kP=KYsb&KKdt5{7+8&zmyXxBmNs_w7xOB
zB#iY3i=9m@fUW8%mOIiOlaJ_$H|ld$GRl7~1z5IYmGyVwQvcMFfU_cF8c;zAR!rnl
z^gLSMf-Rg@u|9HI>D`7`>bkq`5PK|-yy;&n1b{JbzzVQ-$gqtboKmY=R9nOi*)QD#Z;oj+W#J%Rlh__198DQ(9E|3tRmvM1NlY&(Y
zfQeXc^7ZA(ZZ0a?=j=#!g3ElVo3&6LK;3*Cew@u8?=J4aA$|g#OMTJO(5EJv0K_B@
zq?aitHG8&PR*P#w^%<@JJUr5F`4N_QhV|;)GoRfKEysl=T=>wHdqc`xM5kT>
z@HjV$dZGkA@>r=TbD8~^tB62;b(=-ec_Fu5G&P{=vh0ne2}Qb9x@2sIPa0e=9`cqH
zJ%~5-l#N=9Wivc~_k^FVqS|4GrOt6VYN3N8rqE`D%VJ`>M`r^-^nSGFLR`j(s0WR%
z?3I)L0n6zEfy3K25WtbH?VJoADIELJF7c
zFAqT0S8KYBLhYq>=q3&HvrN%3O#cVxVgZ}bzL3Yj`bbE$?@t_S~I
z2l4ZpSv~&pot`Xu%~5*z6)mR<*tgtYI;(gh)C&d(6fJr1kJQbz*5&kY%`q#Yr9lGMJ$Ade||3wo#)jrMWt1*cKzB#vvoCwt4$
z5Z~jM{US(xcoL2`QFdK~WNABNOYdGTN
zdec7s2${%rVvl?(VhLxTSt=}9somI=n3o7PTcBFZQqBJ08q1A@RfO20&Q|mi`4Lo>
z$#EVXKZ7iQGG$pX{>m*u-_D7#$QsN*eexPpMz(~u`-M2OcaQ;L$PX0+jG>kH4
zVp4RissatOHUvKAsDHIw5Y6
z6I50ZMk4IXDxD{|@@MJZtSAI_`)X@`Rm1-3rA4ldgV{_0TuiCa<45-&wi83Sc7
zfBCOzdPzLw1n-@Pz-wKX$_d+%*{M+9fm@#JR!?)-ib5jpEvI??M2HE^5VLCKZ$mc#
z&t(l({P9d+Jdl60?98f%o50zZ|M~3ro5d}g&Sf~HIl9qf|Jla2!&n=W{?MLnAvcYf
z?eWel2XOAp(XFaD*;0M%k=HvvBn7@2guGBs3!LPPGyAQ@%tN$A&j
zPMY#98cST1klibN4zb@qIXEMS%MZL9{m(p>_rzwPG4r?RAiqQ@Ul{1;1bX>y*|;0M
zJKDDN%Y&zgSAxljL_CW&%o?Q!fizA%a-HvFpm=rOO1*thuC;S1*^lACO8$y?lFm>
zU!PY(#U?HI6D_iU$HqCRVfG51T<5(LC~6?ERQKF*EY*H`>&dlvwYVML3#ry@lXoHa
zVfkkdBf8Iq#lV^~-fE^4XSx!tE-1{@<%)q8@Tjc}ujP>?>}$$$T=8T*=BO7YJEH78
zAzmrHO8i$LgaZOtPLno_WVBpoO^>f>KwvG_4VIQ*1yCNe8ZI_c+10n#-ILp3xPJV4
z7={uDpdN!u1l(xaA@-8#y>sjGYk$>@}dX)dYVhDE3(Sz@yg>`GT$X
zQvW=8z!>G}?}}TsSSTW7l|DVZ4SfMOxp56L`4%Uxj-?+x>md(qs;-VWe<(?kxu!Q=OLkS#FF%iz*&k%{%8e
z>zbsv5e1PwFt63)R-gW7#+Nvih#mj4I0=LZmX}u+YWGxN2z?CDl(BD0ly5rc`)@R&
zXCc#E{C#N}di?f4ZP_5rU)zN+AZ%IK#`Vv!?mi%PLf0BFY%=eTu~ynzquX^YbpyGG
zPFKD2-kF!~X8}E1BIv6e8JTfo3?o_=8Ew$7w7Sh~aQfP9qqIApj3W^ON;tPo|r{C+?GFnt#1kQ?NNUwJ|8qwy@}dBz7HWZJ#JL
zBpH46&@%$LZA%pI;nWgSZr)Q131V~qdVfZ`fq7kgkAllA63~WSUkzwKM~DfCRF5;!
z`aRYt)MuO&DRYW<*tRBTKi1iUx3r!Vnb*}wN+$x?e8UVCxY)4i<+{Gs>#%q`y10t
z#_d`|!MxKmH723wu}!||b$||?I!uYzS8dZ@)Ix53t}lN1X`RcyTPTV_o@|$B!Z{h+
zS-@!}#*In&Go;UdAJ#<$&ezKyT5d{~(kolj!{f~xVovel5#B^Ci6eypbnGgcA3f9H
z)&p4A%zmJmb%!$`PPmq_YEydMDqV#re$o08L!;TeTEKJi%HwX?
zEbrdikxWgAGl3DnytXGYlbJWxpmDBZD9&2V;$zmItruGAE!_ls5E>qpZELr9k(BGu
z`M11Q9fI2axB1mJx;4H{OI@jYJon`Kvlb2PXX+J?63Q|imwM;lK8L3`DLE>A&hTk6
ziKyWnKH3s8L%^E)scyRnLsgC-y&&!vwJ9;b>~vQCwLk_Syg(EpbydIU5B{j?L*jjOEgNwEXjvNuv_j)P^y5E3
z4B%&$C6Ev1tStm2jFhTAzVlDDaWhNrcRBC>U->EHw;L@S&T>a8@bG@)y|vPWySUf3
zKCuirkn#8D%oHF=PY%fJQ)TIVbIpJPcGa2$kRfNFjv4%ii4x1?1DSAg%g+d}_b5(h
z006<+A8pc!|I{oqDczi0K6U3cRdYoe{Hv+IBDjn3r)IN(nJ~vLKKrL&2k?LZ=Lh1g
zvFrr_64%V2t7g@KnPfXszq`H|bdl^>H*1HacvWuxso5Ajz(?Kq_CC7Y4P`RnHy27R
z>ZpDxuQ^$Xv7`}T~2T`xYFX)oNxZ$VLih*=@*jTgFxiDEW1N*Ua^OG3N8TX%S*ETpNNhO
zRltlFQBL3c%%vhqch2AR{MHOC%jOuTTj*a?CGG^shNb`J{Mcy&NC1@ve!Wqjm}dEl
z@5Ik>iNUwQ9kXf!G!Om7h4HSCpVlv#Q8Tyjy59cP4!{xC4~Y@zT1R#|3|irK)i%QFzrIkZJevZR^_=OAaGrK
zUOqCFeco~7{KGftzmq>SM!XS6f6Dc~bFh=v}8rOyFwgX=_xm^Mo4FXmr~_#+f`uixbRIDV9FD1mB)Y83edhyHz)WQNYIUx
zmm?30Wmy*e4N76$1S)L|km{u_3gB?Pw3Y95o^
z`~R+%_y>Tmfz5p|2POu3A~pwVywWte`?oBDfF*!nx4{f>4-=!SDeGRP`Btqw=KqVN
zXc#8HUkTn#D8;{OeJ}(Eg3cojv&-Qh%)jv=L_;@Acm)hk9MT0Ifwyq5*w>
z3kKL+U!9x(zY``d`Aw4D(@m#YV4z08uU(DVX|})S0&Jq+Y(DXDV2d8uQr81gyMPP<
z2v4gl=mxaA%MN4}Bb3pQ|4rK^9{$}2=Y_wM6EOBxW?)SB|88f#)&_E@{aHh3K@J4K
zx_|-7=;Qq@8>v65^=D?M*~U?7xydjeAWurEpBcn=nGWH@4mi~trCF$
z$aeIJWw|@2Q!zIMcuUAze=_XSAe>s@_8^B|br@Hjo<>)yh+6BGb(!Oo5(cyqyqt1H
zT2B%{xPo{kJpO}`2HYbb_nV7+a_Ur9q+h|T0rR+*&`luht)IfC6-}KIW7G^q>Ez4}JyNm&`w>}B*XJvB{xZ;gS-z$-+Y@l-I
zKO2HB=SF46D8dgK$*x
zCogVY`=GEI`q;F2gZYojSf!0(isu+bD~xEvn)(44rB59gq#6JX6#6OXYX9c}G{MX4
z4yO@f^t&v3(H+Oi1_ae_F5Yx~juzm4_jrGRVQ(xdyWHUdTL7N2sd`x6ljXWq7?m|%
z`=$bb8(UPN`iYN_<~Ub-gz|>#7K{lpaPmBy=8PkwV<3v5f~UYQc1eiC?Zp6joeLzRCntc*+;5&ICYJCu^ew^F1i(u
zyQ)BIAD17>wl06|{oRknjRpESL65zxTQq!fgw)S|I}m-z`~(dW5wv3L#kw`oYLmVZJ)9+tUooJ+ZMu2)iw)C3-*%p4B*~>ogX8C$5G+x;vIN=Rn=^
zpq9apEw)Py<^~0jTibk<=1q#G%ZbK>%M^F(URy5vNgUCR+S7GiF7I(V%b6@$L>G_;
zwH%1QCyR6-syp+!?4b5oNra~i*dy#1h+I5dXUN_Mqs~sWPL3A`Qq)9-nm!H;l*pt&
zoUY`TEUr~gu{AgRrSNq5*hpld`LJi@QaK&^%I90PhKV(@qA4vMO&?tOd);h`*a&LM
zlb$cl;&X;JhG@GbXzEM%(H&4au2uGg^<~dcZrx>}t2ldPsni-v&31GZbI8!51H{5&
zr?SkJAVG?*&2;UTgN-xyao%drKEb*TDB<=cCeBebkikB
za_QHpwzKX^n{k_)3^ja`$doK-9};%|LM|io6p48F$)N~QgxK53EH`dLL?Ig+_a>OQ
zr-4ub)#LpYpABzz)vQtlKNO9d-Xk)Y_l5xl|26?!T`jkfjoeV1M=IB#@mLvNJ6-1_
z|Nc&gnlmAx1X{o`NvLzpH{(EOty_%IrJ}0nV;mbtT_hk5e2+44JM+{B)?e=pJ<~H{
zIGi2w4BBBYTS@syg|3oHuMS4UzBshrDA|q>W^Xy)OJ0p1;Ad=PsdHjFw~f9p*BXmO
zmmGa{d?I192?=x4m0cT^jb{AOku8S>+kH~BHG07bw|r};E7^5=2#pP+;Fir%py(@k
zJC=@vUt1YG@^Hm6OiASa8aQ
zUOyWr+6lAc>@T$V5CRL%PETIz`jd)q6gr7cNhs)rH2o}d%y5W&k6>Q|>}6T1GMpk(Q%6u|w)d&@#UtqxSD4!k1%HFo?kn(jR>Q>nLFL%Gx!bxed;~6Zw?{&_E3I
zZ!}*>wrbQk(0I72NboD78g2sM27;<<0LZTSwWJToSL%QhG4>b@TYd(3q&h;c-i$Nf
zpir)0fW($b#=bSXxMDKlO+`3xVb2;E7Q}~c((PQrs#&wD7n1HT*@sD+0$yRh+zp7(4`+gv`}sa7f7^@G
zPkUd9!~{U9Qx%qwwNY)A0?lHULxq|&Ap7;0dIR;m6E7J^OfEl;5HlanVJim`o|z-X
zAo`wb45}@chf#?0{d1z*TM{RyZ)=t!JuvlRSz?zb1;;|esjA1r77vms5YjlK90v6<
z>LtE)`Z|jrUUZ(79QWM1dzs3NT2V|kI}106;#a=U?D9s|a=I*vfqidQWCY@(4`Gmx
z@519gKB~4C$}(;{S1o^mTX(YZQt4xz3b~A|j=jP9M~rPA%rCY+7pEH-CmrYADFV^I
z@?II(sFyGXYEU(EWU-ZxW#=G!~U2NPC)yn)6v*T(SJdsd{-ri)f)&k7-w-h?;h
z?|uqza(%96w(T@Av>XM%Kg@=W3BQ`4N`q~SUkPfR6$%DK&s49{LZ_+`k@Ug%a`{mV
z+5J^xGQ0RB^#pJCGV~4+lKK
z(!0Wrj{`JVR{7yB^HRbp_vWnJ75(D~sQ8oMW_9OpY^Wa-MB&~2ftZ#^om?5P8F?#K
z2=kKLc4_Pksc7^*kvm!FndpbAN`4p#9RF2E}M6OYS*sQJjJC8CcUvBLSQZ04vu|m+T3Wiew;|L^Ct#hQ;1x4n-
zlA{{rw7T=7d~D1W8Xerny_8z%o1=^x)FEBH&z&5Qib?RH#1ERXU!whBV-y+>#7Sg6
zX9V}Tn-TPJ>$ZF0#@!aDdwIiov)C=yFL7yRq&_1Y
z`^$Nq^QZG;+je(_r8rcOlow>sm~Z22?Kp9x
zjc`{7H2Y(`!JHyrOLTWOz{gc-0TW&8j=0@bR^#gz{{B7cOP?P~18KC%-2;rFgR(eb
zW19E)BJqwo)RBkS`ic(LcKyRfT1-im8$+SDgR&LYK9blElj4&-iMjpID8^-D6lWUB
zibHgtZp=V$%Cf-??V{RVPqyr}l=+|&-I69jLF1b-N#&FEPqd(6&OWu#!e*Bk
zR8~m`NA7j28TiY%+-S#c`+hQ&JwW2#!-pY6t`bs+m!N07WbJX38mCmtiw=2@>*l$gc5
z#^U*Z6}V&{AW&fyB7=1Mp5dGvEsWe2y|>&64RpF1IUNu7C4I0X5##yERCYH6OD!Xy
zqM(*2N=Id7YB>M_(9J52T^!_UC`qAQe9X<8Us+EqzBo@u*L)@{Q)k
zKmZUPnA=_epk;5Tl#`REs&PrBPY0J!jTfg=h@q;D%m7!H(X|R8%W4E{JdDsfG}q9(L^1u|lW1
zyBwO&R*%4ZJhrfxK&+fmpjS*a%)?r}K$4xHre1hjk}%27{g>a1Gvn_|;19RUd{n8x
z6Wj0S!#-vgH+d@RJUD&XAs*c<@Wy?k{6Xg^9v@6AeLuJ&+@F=2;poZ($}pna#$>-B
z|1PR5yo@b$%5YKFhUp`bZfvAbbqdIGBVFVsY*G03X?6*4q+x~QrJngmYjhJP12L2H
zLM8C)u{H1g6}F-6G@Ppl;-O&rMRvSqByWyO|GI|lrp+#q&lN~%C8SVzKb5Do3#40H
zq|-E-dn;9F7E!71sBA^{D}6DD=wr1^S#;c?+%T84p8p!!nd|zVinR4`PWDB@g2#X~
z1l)J{NEha~+>2L&0ptomwCpv4+q&wL0r(
z&D5GNkgmYumrM*^{61{JRv8ZSffGQ8yQnm+c8oWa2DHk}aEB>3IgpgP*aXgooTv}B
zIP_yKFsEZ2!ukuZ;dZE2Npt+;4s7zjSg2`l
z(w*tppv57`%h$dv+epy4K2rl9LZ;8$6y9W5>qHR5+xxRCB>mK>wYs%CiQ%J%>4vF;
zwk4!#30ofqZ;LD5Gml-GYYW=m&-U}(gv_d-#}_~Jv8Xi9tA2do$2gmOo^!@+vs^H;
z2OM)t?ZK{>w#PkjFOjw@`!@ASU2EN`giRW`IR&R&JADsoTsco1GY4G!P{jan1Jfj#
zzGfSh-^3XAk4Jwm;P}oBO`q6^)Wa;960-R+e>b@`fgpZ4$0a`MRG$Q
z5N?yZkT~fbzL<$q_PnIv7L9@%xlf3!Y}7&Ojp|*NdrHE&pZBR-HiYmE2=8abu>>
zOSb!;0?DS)XWM>1W<)P8+)|cfE~|%~OKgUC9y}WMw=A`0%QcGG%3||=8SeS>Wc*+>
z*{4}Nx4NjBeXA;j(Bq}eSabJDtse?~4YkDWATMf=|2(jIrD8@>E4ft#S@wLW)Ffz$
za`;YyPnRp%l0h)K5nvd*)*fN)guej|K`sFvrOl^-JL$dT(b*hIBhmlWwS
zydC4CSjbH!_=)N)imJ92hrwxnpTi>A@^z?F4EDoQZ+*k!0xAq!w1Nj^1bNQVC?M^C
zj-YGvVA=^Y75WJ{*5{;Y&END+QU1Y30cO=k#b#>byliRBl%$Q6EGW)fBHvEfR@OGKThO5%e{DCg{oDRtwB)U_w4K=Q-k%JiOL3{)yO>NMb
z2mnO?7SalL^5Y45i@@8kI1hS4P{);ks?^I!)r7(xqt52t&N!DERoc9V@?dKy{&&*?
z-(FsL1ymmxnzvuZR4>U9&BwuBKf+-5(ObY=B=iG
z-3gpPdy?;D)))8cyNJq!PeVqUST@ODXmKceKrIk#vRbvQSMxdBa#Q+bZ5#82F|^dE
zwrS+v=EwZ)MS|M(u@66H9
zI(Da<-Z@crt7@3FF8;#CTX`6idX5Q=?~9ica!r_#hBuuFFT)C5TN=j03e9sha)QU>
zE8q!af-TQXl+t&T4XO7F6g)unPCv*YeoMY>)o_`9{mwQ@p2rWKdK^ulTWm7SI>dWI
ze{wX_3pg!B7D5Zwbk|OQ#f^++r!gWS(*OBi0P