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 9cd77ca6e3a3..000000000000 --- 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 bd024095e80b..5109a75ad57f 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 000000000000..fabc89f8c823 --- /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 000000000000..dd686203fa35 --- /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 926d5f405b38..bf044a1fa77d 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 b19ef31ccb9a..b71b4f067ae3 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 48f15e84c930..3632c4cca9e9 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 3df9e6488772..0726d301b34a 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 7cc82a917397..ae214764052d 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 feae110c271f..c99e4e4e0698 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 38479b138447..6ef109ff60e4 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 66cdd22ed6bd..efeb68c8a885 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 53b892475704..24bf736cfa27 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 9b7745bd884f..9adaf0dc3ba1 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 bfc52ec33c35..e3bfe346fbc0 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 750fec4d4d1f..7193a77fd0ec 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 1af739c34b76..8f6fe7fce5cf 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 ba7bdeeda013..6a4c297f2588 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 a85f67f5ba56..7d3e48ce1ae8 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 22ed18f75c65..50f12d21d4db 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 ac91c5a92048..177440c5ea5d 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 ca97b9d2a6b7..ea4808de9684 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 a58df547fa52..37a8881ab520 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 000000000000..a4cbfb11b36f --- /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 e180ca9489df..1ac6834365cd 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 5f093c55e94e..f2dc9ed1ae39 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 000000000000..a9ac144d1f27 --- /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 7e024eda9b79..8965446cc85f 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 4dede8bf5d75..a5c5133dbc70 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 db219fa8b731..270caec2f3f8 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 5bab5ac27f3c..2f6de1be60c9 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 000000000000..28bb9542ab7c --- /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 6e9186e28649..d99675f950ad 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 87f7d76cffaa..d3921ab11457 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 000000000000..922ec36619a4 --- /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 000000000000..dd890b246322 --- /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 000000000000..bd972723c649 --- /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 000000000000..f6a757ad7a18 --- /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 000000000000..3be2a9739837 --- /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 000000000000..76c9abbd4bfb --- /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 000000000000..8cc3e1290653 --- /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 f79a69c9f4ab..b24c0b6983f4 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 b098d6661981..451254efd964 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 330a501a78d3..7f5137812ee3 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 f6de2ba931c5..5bfb175ea0d0 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 25bc93ad38b3..921c2aed0062 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 000000000000..9f4141dbcae7 --- /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 b36643620091..ade78c31211a 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 5419c4135796..e1363be35e2e 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 d6d18848be4d..718304018730 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 43b88915b69d..9326f7e240e3 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(); + 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(); + }); + + 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, + }); + 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); + }); + + 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 + ); + }); }); - 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, - }); - 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); + 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(); }); - // 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); - }); - - 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(); + 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 c524379640df..c616730ff35b 100644 Binary files a/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz and b/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz differ 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 3434e1f80a7c..552142d3b190 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, 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 ef08d6932421..7f416c26cc9a 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 7ea63c5d444b..c99506fec3cf 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 ef08d6932421..4513c07f2778 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 +}