From d67b343ecd1bb9334ee27e3d2de2efd1682d6a13 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 6 Nov 2020 16:07:44 +0100 Subject: [PATCH 1/2] Use saved object references for dashboard drilldowns --- ...ver.embeddablesetup.getattributeservice.md | 11 - ...ugins-embeddable-server.embeddablesetup.md | 3 +- .../embeddable/embeddable_references.test.ts | 87 ++++++ .../embeddable/embeddable_references.ts | 82 +++++ ...embeddable_saved_object_converters.test.ts | 26 +- .../embeddable_saved_object_converters.ts | 7 +- .../saved_dashboard_references.test.ts | 164 +++++----- .../saved_dashboard_references.ts | 75 ++++- src/plugins/dashboard/common/types.ts | 17 ++ .../application/dashboard_app_controller.tsx | 2 +- .../application/dashboard_state_manager.ts | 2 +- .../public/application/embeddable/types.ts | 12 +- src/plugins/dashboard/public/plugin.tsx | 1 + .../public/saved_dashboards/index.ts | 2 +- .../saved_dashboards/saved_dashboard.ts | 22 +- .../saved_dashboards/saved_dashboards.ts | 10 +- src/plugins/dashboard/public/types.ts | 10 +- src/plugins/dashboard/server/plugin.ts | 20 +- .../server/saved_objects/dashboard.ts | 15 +- .../dashboard_migrations.test.ts | 55 +++- .../saved_objects/dashboard_migrations.ts | 62 +++- .../dashboard/server/saved_objects/index.ts | 2 +- .../saved_objects/migrations_730.test.ts | 6 +- src/plugins/embeddable/common/index.ts | 21 ++ src/plugins/embeddable/common/lib/index.ts | 1 + .../lib}/saved_object_embeddable.ts | 2 +- src/plugins/embeddable/common/mocks.ts | 31 ++ src/plugins/embeddable/common/types.ts | 15 +- .../public/lib/containers/container.ts | 2 +- .../public/lib/containers/i_container.ts | 12 +- .../public/lib/embeddables/index.ts | 2 +- src/plugins/embeddable/server/mocks.ts | 30 ++ src/plugins/embeddable/server/plugin.ts | 17 +- src/plugins/embeddable/server/server.api.md | 5 +- .../dashboard_drilldown/constants.ts | 14 + ...hboard_drilldown_persistable_state.test.ts | 48 +++ .../dashboard_drilldown_persistable_state.ts | 75 +++++ .../drilldowns/dashboard_drilldown/index.ts | 9 + .../drilldowns/dashboard_drilldown/types.ts | 12 + .../common/drilldowns/index.ts | 7 + .../dashboard_enhanced/common/index.ts | 7 + x-pack/plugins/dashboard_enhanced/kibana.json | 2 +- .../abstract_dashboard_drilldown.tsx | 8 +- .../abstract_dashboard_drilldown/types.ts | 8 +- ...embeddable_to_dashboard_drilldown.test.tsx | 6 + .../embeddable_to_dashboard_drilldown.tsx | 5 + .../dashboard_enhanced/server/index.ts | 19 ++ .../dashboard_enhanced/server/plugin.ts | 44 +++ .../ui_actions_enhanced/common/index.ts | 7 + .../server/dynamic_action_enhancement.ts | 4 +- .../ui_actions_enhanced/server/index.ts | 6 +- .../ui_actions_enhanced/server/plugin.ts | 4 +- .../dashboard_to_dashboard_drilldown.ts | 279 +++++++++++------- .../reporting/hugedata/data.json.gz | Bin 33744 -> 33744 bytes .../spaces/copy_saved_objects/data.json | 4 +- 55 files changed, 1101 insertions(+), 298 deletions(-) delete mode 100644 docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md create mode 100644 src/plugins/dashboard/common/embeddable/embeddable_references.test.ts create mode 100644 src/plugins/dashboard/common/embeddable/embeddable_references.ts rename src/plugins/dashboard/{public/application/lib => common/embeddable}/embeddable_saved_object_converters.test.ts (82%) rename src/plugins/dashboard/{public/application/lib => common/embeddable}/embeddable_saved_object_converters.ts (91%) rename src/plugins/dashboard/{public/saved_dashboards => common}/saved_dashboard_references.test.ts (52%) rename src/plugins/dashboard/{public/saved_dashboards => common}/saved_dashboard_references.ts (55%) create mode 100644 src/plugins/embeddable/common/index.ts rename src/plugins/embeddable/{public/lib/embeddables => common/lib}/saved_object_embeddable.ts (96%) create mode 100644 src/plugins/embeddable/common/mocks.ts create mode 100644 src/plugins/embeddable/server/mocks.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/server/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/server/plugin.ts create mode 100644 x-pack/plugins/ui_actions_enhanced/common/index.ts diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md deleted file mode 100644 index 9cd77ca6e3a36..0000000000000 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) > [EmbeddableSetup](./kibana-plugin-plugins-embeddable-server.embeddablesetup.md) > [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) - -## EmbeddableSetup.getAttributeService property - -Signature: - -```typescript -getAttributeService: any; -``` diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md index bd024095e80be..5109a75ad57f0 100644 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md @@ -7,14 +7,13 @@ Signature: ```typescript -export interface EmbeddableSetup +export interface EmbeddableSetup extends PersistableStateService ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) | any | | | [registerEmbeddableFactory](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerembeddablefactory.md) | (factory: EmbeddableRegistryDefinition) => void | | | [registerEnhancement](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerenhancement.md) | (enhancement: EnhancementRegistryDefinition) => void | | diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts new file mode 100644 index 0000000000000..fabc89f8c8233 --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + ExtractDeps, + extractPanelsReferences, + InjectDeps, + injectPanelsReferences, +} from './embeddable_references'; +import { createEmbeddablePersistableStateServiceMock } from '../../../embeddable/common/mocks'; +import { SavedDashboardPanel } from '../types'; +import { EmbeddableStateWithType } from '../../../embeddable/common'; + +const embeddablePersistableStateService = createEmbeddablePersistableStateServiceMock(); +const deps: InjectDeps & ExtractDeps = { + embeddablePersistableStateService, +}; + +test('inject/extract panel references', () => { + embeddablePersistableStateService.extract.mockImplementationOnce((state) => { + const { HARDCODED_ID, ...restOfState } = (state as unknown) as Record; + return { + state: restOfState as EmbeddableStateWithType, + references: [{ id: HARDCODED_ID as string, name: 'refName', type: 'type' }], + }; + }); + + embeddablePersistableStateService.inject.mockImplementationOnce((state, references) => { + const ref = references.find((r) => r.name === 'refName'); + return { + ...state, + HARDCODED_ID: ref!.id, + }; + }); + + const savedDashboardPanel: SavedDashboardPanel = { + type: 'search', + embeddableConfig: { + HARDCODED_ID: 'IMPORTANT_HARDCODED_ID', + }, + id: 'savedObjectId', + panelIndex: '123', + gridData: { + x: 0, + y: 0, + h: 15, + w: 15, + i: '123', + }, + version: '7.0.0', + }; + + const [{ panel: extractedPanel, references }] = extractPanelsReferences( + [savedDashboardPanel], + deps + ); + expect(extractedPanel.embeddableConfig).toEqual({}); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "IMPORTANT_HARDCODED_ID", + "name": "refName", + "type": "type", + }, + ] + `); + + const [injectedPanel] = injectPanelsReferences([extractedPanel], references, deps); + + expect(injectedPanel).toEqual(savedDashboardPanel); +}); diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.ts new file mode 100644 index 0000000000000..dd686203fa351 --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/embeddable_references.ts @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { omit } from 'lodash'; +import { + convertSavedDashboardPanelToPanelState, + convertPanelStateToSavedDashboardPanel, +} from './embeddable_saved_object_converters'; +import { SavedDashboardPanel } from '../types'; +import { SavedObjectReference } from '../../../../core/types'; +import { EmbeddablePersistableStateService } from '../../../embeddable/common/types'; + +export interface InjectDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export function injectPanelsReferences( + panels: SavedDashboardPanel[], + references: SavedObjectReference[], + deps: InjectDeps +): SavedDashboardPanel[] { + const result: SavedDashboardPanel[] = []; + for (const panel of panels) { + const embeddableState = convertSavedDashboardPanelToPanelState(panel); + embeddableState.explicitInput = omit( + deps.embeddablePersistableStateService.inject( + { ...embeddableState.explicitInput, type: panel.type }, + references + ), + 'type' + ); + result.push(convertPanelStateToSavedDashboardPanel(embeddableState, panel.version)); + } + return result; +} + +export interface ExtractDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export function extractPanelsReferences( + panels: SavedDashboardPanel[], + deps: ExtractDeps +): Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> { + const result: Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> = []; + + for (const panel of panels) { + const embeddable = convertSavedDashboardPanelToPanelState(panel); + const { + state: embeddableInputWithExtractedReferences, + references, + } = deps.embeddablePersistableStateService.extract({ + ...embeddable.explicitInput, + type: embeddable.type, + }); + embeddable.explicitInput = omit(embeddableInputWithExtractedReferences, 'type'); + + const newPanel = convertPanelStateToSavedDashboardPanel(embeddable, panel.version); + result.push({ + panel: newPanel, + references, + }); + } + + return result; +} diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts similarity index 82% rename from src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts rename to src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts index 926d5f405b384..bf044a1fa77d1 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts @@ -21,9 +21,8 @@ import { convertSavedDashboardPanelToPanelState, convertPanelStateToSavedDashboardPanel, } from './embeddable_saved_object_converters'; -import { SavedDashboardPanel } from '../../types'; -import { DashboardPanelState } from '../embeddable'; -import { EmbeddableInput } from '../../../../embeddable/public'; +import { SavedDashboardPanel, DashboardPanelState } from '../types'; +import { EmbeddableInput } from '../../../embeddable/common/types'; test('convertSavedDashboardPanelToPanelState', () => { const savedDashboardPanel: SavedDashboardPanel = { @@ -135,3 +134,24 @@ test('convertPanelStateToSavedDashboardPanel will not add an undefined id when n const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); expect(converted.hasOwnProperty('id')).toBe(false); }); + +test('convertPanelStateToSavedDashboardPanel will not leave title as part of embeddable config', () => { + const dashboardPanel: DashboardPanelState = { + gridData: { + x: 0, + y: 0, + h: 15, + w: 15, + i: '123', + }, + explicitInput: { + id: '123', + title: 'title', + } as EmbeddableInput, + type: 'search', + }; + + const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); + expect(converted.embeddableConfig.hasOwnProperty('title')).toBe(false); + expect(converted.title).toBe('title'); +}); diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts similarity index 91% rename from src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts rename to src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts index b19ef31ccb9ac..b71b4f067ae33 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts @@ -17,9 +17,8 @@ * under the License. */ import { omit } from 'lodash'; -import { SavedDashboardPanel } from '../../types'; -import { DashboardPanelState } from '../embeddable'; -import { SavedObjectEmbeddableInput } from '../../embeddable_plugin'; +import { DashboardPanelState, SavedDashboardPanel } from '../types'; +import { SavedObjectEmbeddableInput } from '../../../embeddable/common/'; export function convertSavedDashboardPanelToPanelState( savedDashboardPanel: SavedDashboardPanel @@ -49,7 +48,7 @@ export function convertPanelStateToSavedDashboardPanel( type: panelState.type, gridData: panelState.gridData, panelIndex: panelState.explicitInput.id, - embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId']), + embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']), ...(customTitle && { title: customTitle }), ...(savedObjectId !== undefined && { id: savedObjectId }), }; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts b/src/plugins/dashboard/common/saved_dashboard_references.test.ts similarity index 52% rename from src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts rename to src/plugins/dashboard/common/saved_dashboard_references.test.ts index 48f15e84c9307..3632c4cca9e93 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.test.ts @@ -17,8 +17,18 @@ * under the License. */ -import { extractReferences, injectReferences } from './saved_dashboard_references'; -import { SavedObjectDashboard } from './saved_dashboard'; +import { + extractReferences, + injectReferences, + InjectDeps, + ExtractDeps, +} from './saved_dashboard_references'; +import { createEmbeddablePersistableStateServiceMock } from '../../embeddable/common/mocks'; + +const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock(); +const deps: InjectDeps & ExtractDeps = { + embeddablePersistableStateService: embeddablePersistableStateServiceMock, +}; describe('extractReferences', () => { test('extracts references from panelsJSON', () => { @@ -41,28 +51,28 @@ describe('extractReferences', () => { }, references: [], }; - const updatedDoc = extractReferences(doc); + const updatedDoc = extractReferences(doc, deps); expect(updatedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", - }, - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], -} -`); + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", + }, + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + } + `); }); test('fails when "type" attribute is missing from a panel', () => { @@ -79,7 +89,7 @@ Object { }, references: [], }; - expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot( + expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot( `"\\"type\\" attribute is missing from panel \\"0\\""` ); }); @@ -98,21 +108,21 @@ Object { }, references: [], }; - expect(extractReferences(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"title\\":\\"Title 1\\"}]", - }, - "references": Array [], -} -`); + expect(extractReferences(doc, deps)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]", + }, + "references": Array [], + } + `); }); }); describe('injectReferences', () => { - test('injects references into context', () => { - const context = { + test('returns injected attributes', () => { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -125,7 +135,7 @@ describe('injectReferences', () => { title: 'Title 2', }, ]), - } as SavedObjectDashboard; + }; const references = [ { name: 'panel_0', @@ -138,49 +148,49 @@ describe('injectReferences', () => { id: '2', }, ]; - injectReferences(context, references); + const newAttributes = injectReferences({ attributes, references }, deps); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\",\\"type\\":\\"visualization\\"}]", - "title": "test", -} -`); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]", + "title": "test", + } + `); }); test('skips when panelsJSON is missing', () => { - const context = { + const attributes = { id: '1', title: 'test', - } as SavedObjectDashboard; - injectReferences(context, []); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "title": "test", -} -`); + }; + const newAttributes = injectReferences({ attributes, references: [] }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "title": "test", + } + `); }); test('skips when panelsJSON is not an array', () => { - const context = { + const attributes = { id: '1', panelsJSON: '{}', title: 'test', - } as SavedObjectDashboard; - injectReferences(context, []); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "{}", - "title": "test", -} -`); + }; + const newAttributes = injectReferences({ attributes, references: [] }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "{}", + "title": "test", + } + `); }); test('skips a panel when panelRefName is missing', () => { - const context = { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -192,7 +202,7 @@ Object { title: 'Title 2', }, ]), - } as SavedObjectDashboard; + }; const references = [ { name: 'panel_0', @@ -200,18 +210,18 @@ Object { id: '1', }, ]; - injectReferences(context, references); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\"}]", - "title": "test", -} -`); + const newAttributes = injectReferences({ attributes, references }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]", + "title": "test", + } + `); }); test(`fails when it can't find the reference in the array`, () => { - const context = { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -220,9 +230,9 @@ Object { title: 'Title 1', }, ]), - } as SavedObjectDashboard; - expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( - `"Could not find reference \\"panel_0\\""` - ); + }; + expect(() => + injectReferences({ attributes, references: [] }, deps) + ).toThrowErrorMatchingInlineSnapshot(`"Could not find reference \\"panel_0\\""`); }); }); diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts similarity index 55% rename from src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts rename to src/plugins/dashboard/common/saved_dashboard_references.ts index 3df9e64887725..0726d301b34ac 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -17,18 +17,47 @@ * under the License. */ -import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public'; -import { SavedObjectDashboard } from './saved_dashboard'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types'; +import { + extractPanelsReferences, + injectPanelsReferences, +} from './embeddable/embeddable_references'; +import { SavedDashboardPanel730ToLatest } from './types'; +import { EmbeddablePersistableStateService } from '../../embeddable/common/types'; -export function extractReferences({ - attributes, - references = [], -}: { +export interface ExtractDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export interface SavedObjectAttributesAndReferences { attributes: SavedObjectAttributes; references: SavedObjectReference[]; -}) { +} + +export function extractReferences( + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: ExtractDeps +): SavedObjectAttributesAndReferences { + if (typeof attributes.panelsJSON !== 'string') { + return { attributes, references }; + } const panelReferences: SavedObjectReference[] = []; - const panels: Array> = JSON.parse(String(attributes.panelsJSON)); + let panels: Array> = JSON.parse(String(attributes.panelsJSON)); + + const extractedReferencesResult = extractPanelsReferences( + (panels as unknown) as SavedDashboardPanel730ToLatest[], + deps + ); + + panels = (extractedReferencesResult.map((res) => res.panel) as unknown) as Array< + Record + >; + extractedReferencesResult.forEach((res) => { + panelReferences.push(...res.references); + }); + + // TODO: This extraction should be done by EmbeddablePersistableStateService + // https://github.com/elastic/kibana/issues/82830 panels.forEach((panel, i) => { if (!panel.type) { throw new Error(`"type" attribute is missing from panel "${i}"`); @@ -46,6 +75,7 @@ export function extractReferences({ delete panel.type; delete panel.id; }); + return { references: [...references, ...panelReferences], attributes: { @@ -55,21 +85,28 @@ export function extractReferences({ }; } +export interface InjectDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + export function injectReferences( - savedObject: SavedObjectDashboard, - references: SavedObjectReference[] -) { + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: InjectDeps +): SavedObjectAttributes { // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when // importing objects without panelsJSON. At development time of this, there is no guarantee each saved // object has panelsJSON in all previous versions of kibana. - if (typeof savedObject.panelsJSON !== 'string') { - return; + if (typeof attributes.panelsJSON !== 'string') { + return attributes; } - const panels = JSON.parse(savedObject.panelsJSON); + let panels = JSON.parse(attributes.panelsJSON); // Same here, prevent failing saved object import if ever panels aren't an array. if (!Array.isArray(panels)) { - return; + return attributes; } + + // TODO: This injection should be done by EmbeddablePersistableStateService + // https://github.com/elastic/kibana/issues/82830 panels.forEach((panel) => { if (!panel.panelRefName) { return; @@ -84,5 +121,11 @@ export function injectReferences( panel.type = reference.type; delete panel.panelRefName; }); - savedObject.panelsJSON = JSON.stringify(panels); + + panels = injectPanelsReferences(panels, references, deps); + + return { + ...attributes, + panelsJSON: JSON.stringify(panels), + }; } diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts index 7cc82a9173976..ae214764052dc 100644 --- a/src/plugins/dashboard/common/types.ts +++ b/src/plugins/dashboard/common/types.ts @@ -17,6 +17,8 @@ * under the License. */ +import { EmbeddableInput, PanelState } from '../../../../src/plugins/embeddable/common/types'; +import { SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/common/lib/saved_object_embeddable'; import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel610, @@ -26,6 +28,21 @@ import { RawSavedDashboardPanel730ToLatest, } from './bwc/types'; +import { GridData } from './embeddable/types'; +export type PanelId = string; +export type SavedObjectId = string; + +export interface DashboardPanelState< + TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +> extends PanelState { + readonly gridData: GridData; +} + +/** + * This should always represent the latest dashboard panel shape, after all possible migrations. + */ +export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; + export type SavedDashboardPanel640To720 = Pick< RawSavedDashboardPanel640To720, Exclude diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index feae110c271fc..c99e4e4e06987 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -81,7 +81,6 @@ import { getTopNavConfig } from './top_nav/get_top_nav_config'; import { TopNavIds } from './top_nav/top_nav_ids'; import { getDashboardTitle } from './dashboard_strings'; import { DashboardAppScope } from './dashboard_app'; -import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters'; import { RenderDeps } from './application'; import { IKbnUrlStateStorage, @@ -97,6 +96,7 @@ import { subscribeWithScope, } from '../../../kibana_legacy/public'; import { migrateLegacyQuery } from './lib/migrate_legacy_query'; +import { convertSavedDashboardPanelToPanelState } from '../../common/embeddable/embeddable_saved_object_converters'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index 38479b1384477..6ef109ff60e42 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -30,7 +30,6 @@ import { migrateLegacyQuery } from './lib/migrate_legacy_query'; import { ViewMode } from '../embeddable_plugin'; import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib'; -import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters'; import { FilterUtils } from './lib/filter_utils'; import { DashboardAppState, @@ -48,6 +47,7 @@ import { } from '../../../kibana_utils/public'; import { SavedObjectDashboard } from '../saved_dashboards'; import { DashboardContainer } from './embeddable'; +import { convertPanelStateToSavedDashboardPanel } from '../../common/embeddable/embeddable_saved_object_converters'; /** * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the diff --git a/src/plugins/dashboard/public/application/embeddable/types.ts b/src/plugins/dashboard/public/application/embeddable/types.ts index 66cdd22ed6bd4..efeb68c8a885a 100644 --- a/src/plugins/dashboard/public/application/embeddable/types.ts +++ b/src/plugins/dashboard/public/application/embeddable/types.ts @@ -16,14 +16,4 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; -import { GridData } from '../../../common'; -import { PanelState, EmbeddableInput } from '../../embeddable_plugin'; -export type PanelId = string; -export type SavedObjectId = string; - -export interface DashboardPanelState< - TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput -> extends PanelState { - readonly gridData: GridData; -} +export * from '../../../common/types'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 53b892475704f..24bf736cfa274 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -450,6 +450,7 @@ export class DashboardPlugin const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, savedObjects: plugins.savedObjects, + embeddableStart: plugins.embeddable, }); const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE diff --git a/src/plugins/dashboard/public/saved_dashboards/index.ts b/src/plugins/dashboard/public/saved_dashboards/index.ts index 9b7745bd884f7..9adaf0dc3ba15 100644 --- a/src/plugins/dashboard/public/saved_dashboards/index.ts +++ b/src/plugins/dashboard/public/saved_dashboards/index.ts @@ -16,6 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -export * from './saved_dashboard_references'; +export * from '../../common/saved_dashboard_references'; export * from './saved_dashboard'; export * from './saved_dashboards'; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts index bfc52ec33c35c..e3bfe346fbc07 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts @@ -17,10 +17,12 @@ * under the License. */ import { SavedObject, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; -import { extractReferences, injectReferences } from './saved_dashboard_references'; import { Filter, ISearchSource, Query, RefreshInterval } from '../../../../plugins/data/public'; import { createDashboardEditUrl } from '../dashboard_constants'; +import { EmbeddableStart } from '../../../embeddable/public'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../../core/types'; +import { extractReferences, injectReferences } from '../../common/saved_dashboard_references'; export interface SavedObjectDashboard extends SavedObject { id?: string; @@ -41,7 +43,8 @@ export interface SavedObjectDashboard extends SavedObject { // Used only by the savedDashboards service, usually no reason to change this export function createSavedDashboardClass( - savedObjectStart: SavedObjectsStart + savedObjectStart: SavedObjectsStart, + embeddableStart: EmbeddableStart ): new (id: string) => SavedObjectDashboard { class SavedDashboard extends savedObjectStart.SavedObjectClass { // save these objects with the 'dashboard' type @@ -77,8 +80,19 @@ export function createSavedDashboardClass( type: SavedDashboard.type, mapping: SavedDashboard.mapping, searchSource: SavedDashboard.searchSource, - extractReferences, - injectReferences, + extractReferences: (opts: { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; + }) => extractReferences(opts, { embeddablePersistableStateService: embeddableStart }), + injectReferences: (so: SavedObjectDashboard, references: SavedObjectReference[]) => { + const newAttributes = injectReferences( + { attributes: so._serialize().attributes, references }, + { + embeddablePersistableStateService: embeddableStart, + } + ); + Object.assign(so, newAttributes); + }, // if this is null/undefined then the SavedObject will be assigned the defaults id, diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 750fec4d4d1f9..7193a77fd0ec9 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -20,16 +20,22 @@ import { SavedObjectsClientContract } from 'kibana/public'; import { SavedObjectLoader, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; +import { EmbeddableStart } from '../../../embeddable/public'; interface Services { savedObjectsClient: SavedObjectsClientContract; savedObjects: SavedObjectsStart; + embeddableStart: EmbeddableStart; } /** * @param services */ -export function createSavedDashboardLoader({ savedObjects, savedObjectsClient }: Services) { - const SavedDashboard = createSavedDashboardClass(savedObjects); +export function createSavedDashboardLoader({ + savedObjects, + savedObjectsClient, + embeddableStart, +}: Services) { + const SavedDashboard = createSavedDashboardClass(savedObjects, embeddableStart); return new SavedObjectLoader(SavedDashboard, savedObjectsClient); } diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index 1af739c34b76a..8f6fe7fce5cfe 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -19,9 +19,12 @@ import { Query, Filter } from 'src/plugins/data/public'; import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/public'; -import { SavedDashboardPanel730ToLatest } from '../common'; + import { ViewMode } from './embeddable_plugin'; +import { SavedDashboardPanel } from '../common/types'; +export { SavedDashboardPanel }; + export interface DashboardCapabilities { showWriteControls: boolean; createNew: boolean; @@ -71,11 +74,6 @@ export interface Field { export type NavAction = (anchorElement?: any) => void; -/** - * This should always represent the latest dashboard panel shape, after all possible migrations. - */ -export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; - export interface DashboardAppState { panels: SavedDashboardPanel[]; fullScreenMode: boolean; diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts index ba7bdeeda0133..6a4c297f25881 100644 --- a/src/plugins/dashboard/server/plugin.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -25,22 +25,34 @@ import { Logger, } from '../../../core/server'; -import { dashboardSavedObjectType } from './saved_objects'; +import { createDashboardSavedObjectType } from './saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; import { DashboardPluginSetup, DashboardPluginStart } from './types'; +import { EmbeddableSetup } from '../../embeddable/server'; -export class DashboardPlugin implements Plugin { +interface SetupDeps { + embeddable: EmbeddableSetup; +} + +export class DashboardPlugin + implements Plugin { private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, plugins: SetupDeps) { this.logger.debug('dashboard: Setup'); - core.savedObjects.registerType(dashboardSavedObjectType); + core.savedObjects.registerType( + createDashboardSavedObjectType({ + migrationDeps: { + embeddable: plugins.embeddable, + }, + }) + ); core.capabilities.registerProvider(capabilitiesProvider); return {}; diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts index a85f67f5ba56a..7d3e48ce1ae8b 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -18,9 +18,16 @@ */ import { SavedObjectsType } from 'kibana/server'; -import { dashboardSavedObjectTypeMigrations } from './dashboard_migrations'; +import { + createDashboardSavedObjectTypeMigrations, + DashboardSavedObjectTypeMigrationsDeps, +} from './dashboard_migrations'; -export const dashboardSavedObjectType: SavedObjectsType = { +export const createDashboardSavedObjectType = ({ + migrationDeps, +}: { + migrationDeps: DashboardSavedObjectTypeMigrationsDeps; +}): SavedObjectsType => ({ name: 'dashboard', hidden: false, namespaceType: 'single', @@ -65,5 +72,5 @@ export const dashboardSavedObjectType: SavedObjectsType = { version: { type: 'integer' }, }, }, - migrations: dashboardSavedObjectTypeMigrations, -}; + migrations: createDashboardSavedObjectTypeMigrations(migrationDeps), +}); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index 22ed18f75c652..50f12d21d4db9 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -19,7 +19,14 @@ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; import { savedObjectsServiceMock } from '../../../../core/server/mocks'; -import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks'; +import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; +import { DashboardDoc730ToLatest } from '../../common'; + +const embeddableSetupMock = createEmbeddableSetupMock(); +const migrations = createDashboardSavedObjectTypeMigrations({ + embeddable: embeddableSetupMock, +}); const contextMock = savedObjectsServiceMock.createMigrationContext(); @@ -448,4 +455,50 @@ Object { `); }); }); + + describe('7.11.0 - embeddable persistable state extraction', () => { + const migration = migrations['7.11.0']; + const doc: DashboardDoc730ToLatest = { + attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"query":{"language":"kuery","query":""},"filter":[{"query":{"match_phrase":{"machine.os.keyword":"osx"}},"$state":{"store":"appState"},"meta":{"type":"phrase","key":"machine.os.keyword","params":{"query":"osx"},"disabled":false,"negate":false,"alias":null,"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"}}]}', + }, + optionsJSON: '{"useMargins":true,"hidePanelTitles":false}', + panelsJSON: + '[{"version":"7.9.3","gridData":{"x":0,"y":0,"w":24,"h":15,"i":"82fa0882-9f9e-476a-bbb9-03555e5ced91"},"panelIndex":"82fa0882-9f9e-476a-bbb9-03555e5ced91","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[]}}},"panelRefName":"panel_0"}]', + timeRestore: false, + title: 'Dashboard A', + version: 1, + }, + id: '376e6260-1f5e-11eb-91aa-7b6d5f8a61d6', + references: [ + { + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + type: 'index-pattern', + }, + { id: '14e2e710-4258-11e8-b3aa-73fdaf54bfc9', name: 'panel_0', type: 'visualization' }, + ], + type: 'dashboard', + }; + + test('should migrate 7.3.0 doc without embeddable state to extract', () => { + const newDoc = migration(doc, contextMock); + expect(newDoc).toEqual(doc); + }); + + test('should migrate 7.3.0 doc and extract embeddable state', () => { + embeddableSetupMock.extract.mockImplementationOnce((state) => ({ + state: { ...state, __extracted: true }, + references: [{ id: '__new', name: '__newRefName', type: '__newType' }], + })); + + const newDoc = migration(doc, contextMock); + expect(newDoc).not.toEqual(doc); + expect(newDoc.references).toHaveLength(doc.references.length + 1); + expect(JSON.parse(newDoc.attributes.panelsJSON)[0].embeddableConfig.__extracted).toBe(true); + }); + }); }); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index ac91c5a92048a..177440c5ea5d1 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -18,11 +18,12 @@ */ import { get, flow } from 'lodash'; - -import { SavedObjectMigrationFn } from 'kibana/server'; +import { SavedObjectAttributes, SavedObjectMigrationFn } from 'kibana/server'; import { migrations730 } from './migrations_730'; import { migrateMatchAllQuery } from './migrate_match_all_query'; -import { DashboardDoc700To720 } from '../../common'; +import { DashboardDoc700To720, DashboardDoc730ToLatest } from '../../common'; +import { EmbeddableSetup } from '../../../embeddable/server'; +import { injectReferences, extractReferences } from '../../common/saved_dashboard_references'; function migrateIndexPattern(doc: DashboardDoc700To720) { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); @@ -100,7 +101,57 @@ const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To return doc as DashboardDoc700To720; }; -export const dashboardSavedObjectTypeMigrations = { +/** + * In 7.8.0 we introduced dashboard drilldowns which are stored inside dashboard saved object as part of embeddable state + * In 7.11.0 we created an embeddable references/migrations system that allows to properly extract embeddable persistable state + * https://github.com/elastic/kibana/issues/71409 + * The idea of this migration is to inject all the embeddable panel references and then run the extraction again. + * As the result of the extraction: + * 1. In addition to regular `panel_` we will get new references which are extracted by `embeddablePersistableStateService` (dashboard drilldown references) + * 2. `panel_` references will be regenerated + * All other references like index-patterns are forwarded non touched + * @param deps + */ +function createExtractPanelReferencesMigration( + deps: DashboardSavedObjectTypeMigrationsDeps +): SavedObjectMigrationFn { + return (doc) => { + const references = doc.references ?? []; + + /** + * Remembering this because dashboard's extractReferences won't return those + * All other references like `panel_` will be overwritten + */ + const oldNonPanelReferences = references.filter((ref) => !ref.name.startsWith('panel_')); + + const injectedAttributes = injectReferences( + { + attributes: (doc.attributes as unknown) as SavedObjectAttributes, + references, + }, + { embeddablePersistableStateService: deps.embeddable } + ); + + const { attributes, references: newPanelReferences } = extractReferences( + { attributes: injectedAttributes, references: [] }, + { embeddablePersistableStateService: deps.embeddable } + ); + + return { + ...doc, + references: [...oldNonPanelReferences, ...newPanelReferences], + attributes, + }; + }; +} + +export interface DashboardSavedObjectTypeMigrationsDeps { + embeddable: EmbeddableSetup; +} + +export const createDashboardSavedObjectTypeMigrations = ( + deps: DashboardSavedObjectTypeMigrationsDeps +) => ({ /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already @@ -115,4 +166,5 @@ export const dashboardSavedObjectTypeMigrations = { '7.0.0': flow(migrations700), '7.3.0': flow(migrations730), '7.9.3': flow(migrateMatchAllQuery), -}; + '7.11.0': flow(createExtractPanelReferencesMigration(deps)), +}); diff --git a/src/plugins/dashboard/server/saved_objects/index.ts b/src/plugins/dashboard/server/saved_objects/index.ts index ca97b9d2a6b70..ea4808de96848 100644 --- a/src/plugins/dashboard/server/saved_objects/index.ts +++ b/src/plugins/dashboard/server/saved_objects/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { dashboardSavedObjectType } from './dashboard'; +export { createDashboardSavedObjectType } from './dashboard'; diff --git a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts index a58df547fa522..37a8881ab520b 100644 --- a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts @@ -18,12 +18,16 @@ */ import { savedObjectsServiceMock } from '../../../../core/server/mocks'; -import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; import { migrations730 } from './migrations_730'; import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; import { RawSavedDashboardPanel730ToLatest } from '../../common'; +import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks'; const mockContext = savedObjectsServiceMock.createMigrationContext(); +const migrations = createDashboardSavedObjectTypeMigrations({ + embeddable: createEmbeddableSetupMock(), +}); test('dashboard migration 7.3.0 migrates filters to query on search source', () => { const doc: DashboardDoc700To720 = { diff --git a/src/plugins/embeddable/common/index.ts b/src/plugins/embeddable/common/index.ts new file mode 100644 index 0000000000000..a4cbfb11b36f8 --- /dev/null +++ b/src/plugins/embeddable/common/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './types'; +export * from './lib'; diff --git a/src/plugins/embeddable/common/lib/index.ts b/src/plugins/embeddable/common/lib/index.ts index e180ca9489df0..1ac6834365cd1 100644 --- a/src/plugins/embeddable/common/lib/index.ts +++ b/src/plugins/embeddable/common/lib/index.ts @@ -22,3 +22,4 @@ export * from './inject'; export * from './migrate'; export * from './migrate_base_input'; export * from './telemetry'; +export * from './saved_object_embeddable'; diff --git a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts b/src/plugins/embeddable/common/lib/saved_object_embeddable.ts similarity index 96% rename from src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts rename to src/plugins/embeddable/common/lib/saved_object_embeddable.ts index 5f093c55e94e4..f2dc9ed1ae395 100644 --- a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts +++ b/src/plugins/embeddable/common/lib/saved_object_embeddable.ts @@ -17,7 +17,7 @@ * under the License. */ -import { EmbeddableInput } from '..'; +import { EmbeddableInput } from '../types'; export interface SavedObjectEmbeddableInput extends EmbeddableInput { savedObjectId: string; diff --git a/src/plugins/embeddable/common/mocks.ts b/src/plugins/embeddable/common/mocks.ts new file mode 100644 index 0000000000000..a9ac144d1f276 --- /dev/null +++ b/src/plugins/embeddable/common/mocks.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EmbeddablePersistableStateService } from './types'; + +export const createEmbeddablePersistableStateServiceMock = (): jest.Mocked< + EmbeddablePersistableStateService +> => { + return { + inject: jest.fn((state, references) => state), + extract: jest.fn((state) => ({ state, references: [] })), + migrate: jest.fn((state, version) => state), + telemetry: jest.fn((state, collector) => ({})), + }; +}; diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index 7e024eda9b793..8965446cc85fa 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; import { Query, TimeRange } from '../../data/common/query'; import { Filter } from '../../data/common/es_query/filters'; @@ -74,8 +74,21 @@ export type EmbeddableInput = { searchSessionId?: string; }; +export interface PanelState { + // The type of embeddable in this panel. Will be used to find the factory in which to + // load the embeddable. + type: string; + + // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input + // will be derived from the container's input. **Any state in here will override any state derived from + // the container.** + explicitInput: Partial & { id: string }; +} + export type EmbeddableStateWithType = EmbeddableInput & { type: string }; +export type EmbeddablePersistableStateService = PersistableStateService; + export interface CommonEmbeddableStartContract { getEmbeddableFactory: (embeddableFactoryId: string) => any; getEnhancement: (enhancementId: string) => any; diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 4dede8bf5d752..a5c5133dbc702 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -31,7 +31,7 @@ import { import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors'; import { EmbeddableStart } from '../../plugin'; -import { isSavedObjectEmbeddableInput } from '../embeddables/saved_object_embeddable'; +import { isSavedObjectEmbeddableInput } from '../../../common/lib/saved_object_embeddable'; const getKeys = (o: T): Array => Object.keys(o) as Array; diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts index db219fa8b7314..270caec2f3f84 100644 --- a/src/plugins/embeddable/public/lib/containers/i_container.ts +++ b/src/plugins/embeddable/public/lib/containers/i_container.ts @@ -24,17 +24,9 @@ import { ErrorEmbeddable, IEmbeddable, } from '../embeddables'; +import { PanelState } from '../../../common/types'; -export interface PanelState { - // The type of embeddable in this panel. Will be used to find the factory in which to - // load the embeddable. - type: string; - - // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input - // will be derived from the container's input. **Any state in here will override any state derived from - // the container.** - explicitInput: Partial & { id: string }; -} +export { PanelState }; export interface ContainerOutput extends EmbeddableOutput { embeddableLoaded: { [key: string]: boolean }; diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 5bab5ac27f3cc..2f6de1be60c9c 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -24,5 +24,5 @@ export * from './default_embeddable_factory_provider'; export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableRoot } from './embeddable_root'; -export * from './saved_object_embeddable'; +export * from '../../../common/lib/saved_object_embeddable'; export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer'; diff --git a/src/plugins/embeddable/server/mocks.ts b/src/plugins/embeddable/server/mocks.ts new file mode 100644 index 0000000000000..28bb9542ab7cb --- /dev/null +++ b/src/plugins/embeddable/server/mocks.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createEmbeddablePersistableStateServiceMock } from '../common/mocks'; +import { EmbeddableSetup, EmbeddableStart } from './plugin'; + +export const createEmbeddableSetupMock = (): jest.Mocked => ({ + ...createEmbeddablePersistableStateServiceMock(), + registerEmbeddableFactory: jest.fn(), + registerEnhancement: jest.fn(), +}); + +export const createEmbeddableStartMock = (): jest.Mocked => + createEmbeddablePersistableStateServiceMock(); diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts index 6e9186e286491..d99675f950ad0 100644 --- a/src/plugins/embeddable/server/plugin.ts +++ b/src/plugins/embeddable/server/plugin.ts @@ -32,23 +32,32 @@ import { getMigrateFunction, getTelemetryFunction, } from '../common/lib'; -import { SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; import { EmbeddableStateWithType } from '../common/types'; -export interface EmbeddableSetup { - getAttributeService: any; +export interface EmbeddableSetup extends PersistableStateService { registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; } -export class EmbeddableServerPlugin implements Plugin { +export type EmbeddableStart = PersistableStateService; + +export class EmbeddableServerPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); private readonly enhancements: EnhancementsRegistry = new Map(); public setup(core: CoreSetup) { + const commonContract = { + getEmbeddableFactory: this.getEmbeddableFactory, + getEnhancement: this.getEnhancement, + }; return { registerEmbeddableFactory: this.registerEmbeddableFactory, registerEnhancement: this.registerEnhancement, + telemetry: getTelemetryFunction(commonContract), + extract: getExtractFunction(commonContract), + inject: getInjectFunction(commonContract), + migrate: getMigrateFunction(commonContract), }; } diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index 87f7d76cffaa8..d3921ab11457c 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -18,12 +18,11 @@ export interface EmbeddableRegistryDefinition

{ // (undocumented) registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; // (undocumented) diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts new file mode 100644 index 0000000000000..922ec36619a4b --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * NOTE: DO NOT CHANGE THIS STRING WITHOUT CAREFUL CONSIDERATOIN, BECAUSE IT IS + * STORED IN SAVED OBJECTS. + * + * Also temporary dashboard drilldown migration code inside embeddable plugin relies on it + * x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts + */ +export const EMBEDDABLE_TO_DASHBOARD_DRILLDOWN = 'DASHBOARD_TO_DASHBOARD_DRILLDOWN'; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts new file mode 100644 index 0000000000000..dd890b2463226 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createExtract, createInject } from './dashboard_drilldown_persistable_state'; +import { SerializedEvent } from '../../../../ui_actions_enhanced/common'; + +const drilldownId = 'test_id'; +const extract = createExtract({ drilldownId }); +const inject = createInject({ drilldownId }); + +const state: SerializedEvent = { + eventId: 'event_id', + triggers: [], + action: { + factoryId: drilldownId, + name: 'name', + config: { + dashboardId: 'dashboardId_1', + }, + }, +}; + +test('should extract and injected dashboard reference', () => { + const { state: extractedState, references } = extract(state); + expect(extractedState).not.toEqual(state); + expect(extractedState.action.config.dashboardId).toBeUndefined(); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "dashboardId_1", + "name": "drilldown:test_id:event_id:dashboardId", + "type": "dashboard", + }, + ] + `); + + let injectedState = inject(extractedState, references); + expect(injectedState).toEqual(state); + + references[0].id = 'dashboardId_2'; + + injectedState = inject(extractedState, references); + expect(injectedState).not.toEqual(extractedState); + expect(injectedState.action.config.dashboardId).toBe('dashboardId_2'); +}); diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts new file mode 100644 index 0000000000000..bd972723c649b --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectReference } from '../../../../../../src/core/types'; +import { PersistableStateService } from '../../../../../../src/plugins/kibana_utils/common'; +import { SerializedAction, SerializedEvent } from '../../../../ui_actions_enhanced/common'; +import { DrilldownConfig } from './types'; + +type DashboardDrilldownPersistableState = PersistableStateService; + +const generateRefName = (state: SerializedEvent, id: string) => + `drilldown:${id}:${state.eventId}:dashboardId`; + +const injectDashboardId = (state: SerializedEvent, dashboardId: string): SerializedEvent => { + return { + ...state, + action: { + ...state.action, + config: { + ...state.action.config, + dashboardId, + }, + }, + }; +}; + +export const createInject = ({ + drilldownId, +}: { + drilldownId: string; +}): DashboardDrilldownPersistableState['inject'] => { + return (state: SerializedEvent, references: SavedObjectReference[]) => { + const action = state.action as SerializedAction; + const refName = generateRefName(state, drilldownId); + const ref = references.find((r) => r.name === refName); + if (!ref) return state; + if (ref.id && ref.id === action.config.dashboardId) return state; + return injectDashboardId(state, ref.id); + }; +}; + +export const createExtract = ({ + drilldownId, +}: { + drilldownId: string; +}): DashboardDrilldownPersistableState['extract'] => { + return (state: SerializedEvent) => { + const action = state.action as SerializedAction; + const references: SavedObjectReference[] = action.config.dashboardId + ? [ + { + name: generateRefName(state, drilldownId), + type: 'dashboard', + id: action.config.dashboardId, + }, + ] + : []; + + const { dashboardId, ...restOfConfig } = action.config; + + return { + state: { + ...state, + action: ({ + ...state.action, + config: restOfConfig, + } as unknown) as SerializedAction, + }, + references, + }; + }; +}; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts new file mode 100644 index 0000000000000..f6a757ad7a180 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createExtract, createInject } from './dashboard_drilldown_persistable_state'; +export { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; +export { DrilldownConfig } from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts new file mode 100644 index 0000000000000..3be2a9739837e --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type DrilldownConfig = { + dashboardId?: string; + useCurrentFilters: boolean; + useCurrentDateRange: boolean; +}; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts new file mode 100644 index 0000000000000..76c9abbd4bfbe --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './dashboard_drilldown'; diff --git a/x-pack/plugins/dashboard_enhanced/common/index.ts b/x-pack/plugins/dashboard_enhanced/common/index.ts new file mode 100644 index 0000000000000..8cc3e12906531 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './drilldowns'; diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json index f79a69c9f4aba..b24c0b6983f40 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.json +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -1,7 +1,7 @@ { "id": "dashboardEnhanced", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"], "configPath": ["xpack", "dashboardEnhanced"], diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx index b098d66619814..451254efd9648 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx @@ -9,19 +9,19 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DashboardStart } from 'src/plugins/dashboard/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; import { - TriggerId, TriggerContextMapping, + TriggerId, } from '../../../../../../../src/plugins/ui_actions/public'; import { CollectConfigContainer } from './components'; import { - UiActionsEnhancedDrilldownDefinition as Drilldown, - UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, AdvancedUiActionsStart, + UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, + UiActionsEnhancedDrilldownDefinition as Drilldown, } from '../../../../../ui_actions_enhanced/public'; import { txtGoToDashboard } from './i18n'; import { - StartServicesGetter, CollectConfigProps, + StartServicesGetter, } from '../../../../../../../src/plugins/kibana_utils/public'; import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { Config } from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts index 330a501a78d39..7f5137812ee32 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts @@ -6,12 +6,8 @@ import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public'; import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public'; +import { DrilldownConfig } from '../../../../common'; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type Config = { - dashboardId?: string; - useCurrentFilters: boolean; - useCurrentDateRange: boolean; -}; +export type Config = DrilldownConfig; export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx index f6de2ba931c58..5bfb175ea0d00 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx @@ -65,6 +65,12 @@ test('getHref is defined', () => { expect(drilldown.getHref).toBeDefined(); }); +test('inject/extract are defined', () => { + const drilldown = new EmbeddableToDashboardDrilldown({} as any); + expect(drilldown.extract).toBeDefined(); + expect(drilldown.inject).toBeDefined(); +}); + describe('.execute() & getHref', () => { /** * A convenience test setup helper diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx index 25bc93ad38b36..921c2aed00624 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx @@ -22,6 +22,7 @@ import { } from '../abstract_dashboard_drilldown'; import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; +import { createExtract, createInject } from '../../../../common'; type Trigger = typeof APPLY_FILTER_TRIGGER; type Context = TriggerContextMapping[Trigger]; @@ -80,4 +81,8 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown { + constructor(protected readonly context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + plugins.uiActionsEnhanced.registerActionFactory({ + id: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN, + inject: createInject({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }), + extract: createExtract({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }), + }); + + return {}; + } + + public start(core: CoreStart, plugins: StartDependencies): StartContract { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/ui_actions_enhanced/common/index.ts b/x-pack/plugins/ui_actions_enhanced/common/index.ts new file mode 100644 index 0000000000000..9f4141dbcae7d --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './types'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index b366436200914..ade78c31211ab 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -7,11 +7,11 @@ import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server'; import { SavedObjectReference } from '../../../../src/core/types'; import { DynamicActionsState, SerializedEvent } from './types'; -import { AdvancedUiActionsPublicPlugin } from './plugin'; +import { AdvancedUiActionsServerPlugin } from './plugin'; import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; export const dynamicActionEnhancement = ( - uiActionsEnhanced: AdvancedUiActionsPublicPlugin + uiActionsEnhanced: AdvancedUiActionsServerPlugin ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', diff --git a/x-pack/plugins/ui_actions_enhanced/server/index.ts b/x-pack/plugins/ui_actions_enhanced/server/index.ts index 5419c4135796d..e1363be35e2e9 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AdvancedUiActionsPublicPlugin } from './plugin'; +import { AdvancedUiActionsServerPlugin } from './plugin'; export function plugin() { - return new AdvancedUiActionsPublicPlugin(); + return new AdvancedUiActionsServerPlugin(); } -export { AdvancedUiActionsPublicPlugin as Plugin }; +export { AdvancedUiActionsServerPlugin as Plugin }; export { SetupContract as AdvancedUiActionsSetup, StartContract as AdvancedUiActionsStart, diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts index d6d18848be4de..718304018730d 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts @@ -16,7 +16,7 @@ import { } from './types'; export interface SetupContract { - registerActionFactory: any; + registerActionFactory: (definition: ActionFactoryDefinition) => void; } export type StartContract = void; @@ -25,7 +25,7 @@ interface SetupDependencies { embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. } -export class AdvancedUiActionsPublicPlugin +export class AdvancedUiActionsServerPlugin implements Plugin { protected readonly actionFactories: ActionFactoryRegistry = new Map(); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts index 43b88915b69d9..9326f7e240e3e 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -14,7 +14,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); - const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker']); + const PageObjects = getPageObjects([ + 'dashboard', + 'common', + 'header', + 'timePicker', + 'settings', + 'copySavedObjectsToSpace', + ]); const pieChart = getService('pieChart'); const log = getService('log'); const browser = getService('browser'); @@ -22,120 +29,188 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); const security = getService('security'); + const spaces = getService('spaces'); describe('Dashboard to dashboard drilldown', function () { - before(async () => { - log.debug('Dashboard Drilldowns:initTests'); - await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.preserveCrossAppState(); - }); - - after(async () => { - await security.testUser.restoreDefaults(); - }); - - it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { - await PageObjects.dashboard.gotoDashboardEditMode( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME - ); - - // create drilldown - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); - await dashboardDrilldownPanelActions.clickCreateDrilldown(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); - await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ - drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, - destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + describe('Create & use drilldowns', () => { + before(async () => { + log.debug('Dashboard Drilldowns:initTests'); + await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); }); - await dashboardDrilldownsManage.saveChanges(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); - - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); - - // save dashboard, navigate to view mode - await PageObjects.dashboard.saveDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, - { - saveAsNew: false, - waitDialogIsClosed: true, - } - ); - - // trigger drilldown action by clicking on a pie and picking drilldown action by it's name - await pieChart.clickOnPieSlice('40,000'); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - const href = await dashboardDrilldownPanelActions.getActionHrefByText( - DRILLDOWN_TO_AREA_CHART_NAME - ); - expect(typeof href).to.be('string'); // checking that action has a href - const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); - }); - // checking that href is at least pointing to the same dashboard that we are navigated to by regular click - expect(dashboardIdFromHref).to.be(await PageObjects.dashboard.getDashboardIdFromCurrentUrl()); - - // check that we drilled-down with filter from pie chart - expect(await filterBar.getFilterCount()).to.be(1); - - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - // brush area chart and drilldown back to pie chat dashboard - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + after(async () => { + await security.testUser.restoreDefaults(); }); - // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) - expect(await filterBar.getFilterCount()).to.be(1); - await pieChart.expectPieSliceCount(1); - - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - - // delete drilldown - await PageObjects.dashboard.switchToEditMode(); - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); - await dashboardDrilldownPanelActions.clickManageDrilldowns(); - await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); - - await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); - await dashboardDrilldownsManage.closeFlyout(); + it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { + await PageObjects.dashboard.gotoDashboardEditMode( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME + ); + + // create drilldown + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); + await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ + drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, + destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + }); + await dashboardDrilldownsManage.saveChanges(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); + + // save dashboard, navigate to view mode + await PageObjects.dashboard.saveDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, + { + saveAsNew: false, + waitDialogIsClosed: true, + } + ); + + // trigger drilldown action by clicking on a pie and picking drilldown action by it's name + await pieChart.clickOnPieSlice('40,000'); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + const href = await dashboardDrilldownPanelActions.getActionHrefByText( + DRILLDOWN_TO_AREA_CHART_NAME + ); + expect(typeof href).to.be('string'); // checking that action has a href + const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); + }); + // checking that href is at least pointing to the same dashboard that we are navigated to by regular click + expect(dashboardIdFromHref).to.be( + await PageObjects.dashboard.getDashboardIdFromCurrentUrl() + ); + + // check that we drilled-down with filter from pie chart + expect(await filterBar.getFilterCount()).to.be(1); + + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + + // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) + expect(await filterBar.getFilterCount()).to.be(1); + await pieChart.expectPieSliceCount(1); + + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + // delete drilldown + await PageObjects.dashboard.switchToEditMode(); + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); + await dashboardDrilldownPanelActions.clickManageDrilldowns(); + await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); + + await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); + await dashboardDrilldownsManage.closeFlyout(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + }); - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + it('browser back/forward navigation works after drilldown navigation', async () => { + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + await navigateWithinDashboard(async () => { + await browser.goBack(); + }); + + expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( + originalTimeRangeDurationHours + ); + }); }); - it('browser back/forward navigation works after drilldown navigation', async () => { - await PageObjects.dashboard.loadSavedDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME - ); - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + describe('Copy to space', () => { + const destinationSpaceId = 'custom_space'; + before(async () => { + await spaces.create({ + id: destinationSpaceId, + name: 'custom_space', + disabledFeatures: [], + }); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); }); - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - await navigateWithinDashboard(async () => { - await browser.goBack(); + after(async () => { + await spaces.delete(destinationSpaceId); }); - expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( - originalTimeRangeDurationHours - ); + it('Dashboards linked by a drilldown are both copied to a space', async () => { + await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + await PageObjects.copySavedObjectsToSpace.setupForm({ + destinationSpaceId, + }); + await PageObjects.copySavedObjectsToSpace.startCopy(); + + // Wait for successful copy + await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`); + await testSubjects.existOrFail(`cts-summary-indicator-success-${destinationSpaceId}`); + + const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(); + + expect(summaryCounts).to.eql({ + success: 5, // 2 dashboards (linked by a drilldown) + 2 visualizations + 1 index pattern + pending: 0, + skipped: 0, + errors: 0, + }); + + await PageObjects.copySavedObjectsToSpace.finishCopy(); + + // Actually use copied dashboards in a new space: + + await PageObjects.common.navigateToApp('dashboard', { + basePath: `/s/${destinationSpaceId}`, + }); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + await pieChart.expectPieSliceCount(10); + }); }); }); diff --git a/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz b/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz index c524379640df7e2c11cd0f86603cf4048a826bd3..c616730ff35b64074b036f0bf30b77d0e45b6c75 100644 GIT binary patch delta 32038 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 NJ4CM=j|g~({|Afl)R_PP diff --git a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json index 3434e1f80a7ce..552142d3b190a 100644 --- a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json +++ b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json @@ -126,7 +126,7 @@ "title": "Dashboard Foo", "hits": 0, "description": "", - "panelsJSON": "[{}]", + "panelsJSON": "[]", "optionsJSON": "{}", "version": 1, "timeRestore": false, @@ -156,7 +156,7 @@ "title": "Dashboard Bar", "hits": 0, "description": "", - "panelsJSON": "[{}]", + "panelsJSON": "[]", "optionsJSON": "{}", "version": 1, "timeRestore": false, From 4441cc11f07877fd591fb8e4edfc699b6eed9aec Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 6 Nov 2020 22:28:05 +0100 Subject: [PATCH 2/2] fix corrupted dashboard object --- .../0.1.0/kibana/dashboard/sample_dashboard.json | 8 +++++++- .../0.1.0/kibana/dashboard/sample_dashboard2.json | 8 +++++++- .../0.2.0/kibana/dashboard/sample_dashboard.json | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json index ef08d69324210..7f416c26cc9aa 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json @@ -11,6 +11,12 @@ "title": "[Logs Sample] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search", "name": "panel_1", "type": "search" }, + { "id": "sample_search", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard", "type": "dashboard" -} \ No newline at end of file +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json index 7ea63c5d444ba..c99506fec3cf5 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json @@ -11,6 +11,12 @@ "title": "[Logs Sample2] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search", "name": "panel_1", "type": "search" }, + { "id": "sample_search", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard2", "type": "dashboard" -} \ No newline at end of file +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json index ef08d69324210..4513c07f27786 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json @@ -11,6 +11,12 @@ "title": "[Logs Sample] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search2", "name": "panel_1", "type": "search" }, + { "id": "sample_search2", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard", "type": "dashboard" -} \ No newline at end of file +}