diff --git a/examples/portable_dashboards_example/public/static_by_value_example.tsx b/examples/portable_dashboards_example/public/static_by_value_example.tsx index 9a8de70da16d..0ceea84935ee 100644 --- a/examples/portable_dashboards_example/public/static_by_value_example.tsx +++ b/examples/portable_dashboards_example/public/static_by_value_example.tsx @@ -10,10 +10,10 @@ import React from 'react'; import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -import type { DashboardPanelMap } from '@kbn/dashboard-plugin/common'; +import type { DashboardState } from '@kbn/dashboard-plugin/common'; import { DashboardRenderer } from '@kbn/dashboard-plugin/public'; -import panelsJson from './static_by_value_example_panels.json'; +import panels from './static_by_value_example_panels.json'; export const StaticByValueExample = () => { return ( @@ -32,7 +32,7 @@ export const StaticByValueExample = () => { getInitialInput: () => ({ timeRange: { from: 'now-30d', to: 'now' }, viewMode: 'view', - panels: panelsJson as DashboardPanelMap, + panels: panels as DashboardState['panels'], }), }; }} diff --git a/examples/portable_dashboards_example/public/static_by_value_example_panels.json b/examples/portable_dashboards_example/public/static_by_value_example_panels.json index 7df37e523919..6ee04e2f2fb1 100644 --- a/examples/portable_dashboards_example/public/static_by_value_example_panels.json +++ b/examples/portable_dashboards_example/public/static_by_value_example_panels.json @@ -1,5 +1,5 @@ -{ - "a514e5f6-1d0d-4fe9-85a9-f7ba40665033": { +[ + { "type": "visualization", "gridData": { "x": 0, @@ -8,8 +8,8 @@ "h": 10, "i": "a514e5f6-1d0d-4fe9-85a9-f7ba40665033" }, - "explicitInput": { - "id": "a514e5f6-1d0d-4fe9-85a9-f7ba40665033", + "panelIndex": "a514e5f6-1d0d-4fe9-85a9-f7ba40665033", + "panelConfig": { "savedVis": { "id": "", "title": "", @@ -35,7 +35,7 @@ "enhancements": {} } }, - "b06b849e-f4fd-423c-a582-5c4bfec812c9": { + { "type": "lens", "gridData": { "x": 30, @@ -44,8 +44,8 @@ "h": 21, "i": "b06b849e-f4fd-423c-a582-5c4bfec812c9" }, - "explicitInput": { - "id": "b06b849e-f4fd-423c-a582-5c4bfec812c9", + "panelIndex": "b06b849e-f4fd-423c-a582-5c4bfec812c9", + "panelConfig": { "title": "Destinations", "attributes": { "title": "", @@ -142,7 +142,7 @@ "enhancements": {} } }, - "a4121cab-b6f2-4de3-af71-ec9b5a6f0a2a": { + { "type": "lens", "gridData": { "x": 0, @@ -151,8 +151,8 @@ "h": 11, "i": "a4121cab-b6f2-4de3-af71-ec9b5a6f0a2a" }, + "panelIndex": "a4121cab-b6f2-4de3-af71-ec9b5a6f0a2a", "explicitInput": { - "id": "a4121cab-b6f2-4de3-af71-ec9b5a6f0a2a", "enhancements": {}, "attributes": { "visualizationType": "lnsXY", @@ -332,4 +332,4 @@ "title": "[Logs] Bytes distribution" } } -} +] diff --git a/src/platform/plugins/shared/dashboard/common/content_management/v2/types.ts b/src/platform/plugins/shared/dashboard/common/content_management/v2/types.ts index 80033007232e..c1fd8cc28465 100644 --- a/src/platform/plugins/shared/dashboard/common/content_management/v2/types.ts +++ b/src/platform/plugins/shared/dashboard/common/content_management/v2/types.ts @@ -19,7 +19,6 @@ import { GridData as GridDataV1, SavedDashboardPanel as SavedDashboardPanelV1, } from '../v1/types'; -import { DashboardSectionState } from '../..'; export type GridData = GridDataV1 & { sectionId?: string; @@ -33,6 +32,13 @@ export type ControlGroupAttributes = ControlGroupAttributesV1 & { showApplySelections?: boolean; }; +interface DashboardSectionState { + title: string; + collapsed?: boolean; // if undefined, then collapsed is false + readonly gridData: Pick; + id: string; +} + export type DashboardAttributes = Omit & { controlGroupInput?: ControlGroupAttributes; sections?: DashboardSectionState[]; diff --git a/src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.test.ts b/src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.test.ts deleted file mode 100644 index aa00108929b2..000000000000 --- a/src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { createExtract, createInject } from './dashboard_container_references'; -import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks'; -import { ParsedDashboardAttributesWithType } from '../../types'; - -const persistableStateService = createEmbeddablePersistableStateServiceMock(); - -const dashboardWithExtractedPanel: ParsedDashboardAttributesWithType = { - id: 'id', - type: 'dashboard', - panels: { - panel_1: { - type: 'panel_type', - gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' }, - panelRefName: 'panel_panel_1', - explicitInput: { - id: 'panel_1', - }, - }, - }, - sections: {}, -}; - -const extractedSavedObjectPanelRef = { - name: 'panel_1:panel_panel_1', - type: 'panel_type', - id: 'object-id', -}; - -const unextractedDashboardState: ParsedDashboardAttributesWithType = { - id: 'id', - type: 'dashboard', - panels: { - panel_1: { - type: 'panel_type', - gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' }, - explicitInput: { - id: 'panel_1', - savedObjectId: 'object-id', - }, - }, - }, - sections: {}, -}; - -describe('inject/extract by reference panel', () => { - it('should inject the extracted saved object panel', () => { - const inject = createInject(persistableStateService); - const references = [extractedSavedObjectPanelRef]; - - const injected = inject( - dashboardWithExtractedPanel, - references - ) as ParsedDashboardAttributesWithType; - - expect(injected).toEqual(unextractedDashboardState); - }); - - it('should extract the saved object panel', () => { - const extract = createExtract(persistableStateService); - const { state: extractedState, references: extractedReferences } = - extract(unextractedDashboardState); - - expect(extractedState).toEqual(dashboardWithExtractedPanel); - expect(extractedReferences[0]).toEqual(extractedSavedObjectPanelRef); - }); -}); - -const dashboardWithExtractedByValuePanel: ParsedDashboardAttributesWithType = { - id: 'id', - type: 'dashboard', - panels: { - panel_1: { - type: 'panel_type', - gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' }, - explicitInput: { - id: 'panel_1', - extracted_reference: 'ref', - }, - }, - }, - sections: {}, -}; - -const extractedByValueRef = { - id: 'id', - name: 'panel_1:ref', - type: 'panel_type', -}; - -const unextractedDashboardByValueState: ParsedDashboardAttributesWithType = { - id: 'id', - type: 'dashboard', - panels: { - panel_1: { - type: 'panel_type', - gridData: { w: 0, h: 0, x: 0, y: 0, i: '0' }, - explicitInput: { - id: 'panel_1', - value: 'id', - }, - }, - }, - sections: {}, -}; - -describe('inject/extract by value panels', () => { - it('should inject the extracted references', () => { - const inject = createInject(persistableStateService); - - persistableStateService.inject.mockImplementationOnce((state, references) => { - const ref = references.find((r) => r.name === 'ref'); - if (!ref) { - return state; - } - - if (('extracted_reference' in state) as any) { - (state as any).value = ref.id; - delete (state as any).extracted_reference; - } - - return state; - }); - - const injectedState = inject(dashboardWithExtractedByValuePanel, [extractedByValueRef]); - - expect(injectedState).toEqual(unextractedDashboardByValueState); - }); - - it('should extract references using persistable state', () => { - const extract = createExtract(persistableStateService); - - persistableStateService.extract.mockImplementationOnce((state) => { - if ((state as any).value === 'id') { - delete (state as any).value; - (state as any).extracted_reference = 'ref'; - - return { - state, - references: [{ id: extractedByValueRef.id, name: 'ref', type: extractedByValueRef.type }], - }; - } - - return { state, references: [] }; - }); - - const { state: extractedState, references: extractedReferences } = extract( - unextractedDashboardByValueState - ); - - expect(extractedState).toEqual(dashboardWithExtractedByValuePanel); - expect(extractedReferences).toEqual([extractedByValueRef]); - }); -}); diff --git a/src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.ts b/src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.ts deleted file mode 100644 index 73bf49a68759..000000000000 --- a/src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { Reference } from '@kbn/content-management-utils'; -import { - EmbeddablePersistableStateService, - EmbeddableStateWithType, -} from '@kbn/embeddable-plugin/common'; -import { ParsedDashboardAttributesWithType } from '../../types'; - -export const getReferencesForPanelId = (id: string, references: Reference[]): Reference[] => { - const prefix = `${id}:`; - const filteredReferences = references - .filter((reference) => reference.name.indexOf(prefix) === 0) - .map((reference) => ({ ...reference, name: reference.name.replace(prefix, '') })); - return filteredReferences; -}; - -export const getReferencesForControls = (references: Reference[]): Reference[] => { - return references.filter((reference) => reference.name.startsWith(controlGroupReferencePrefix)); -}; - -export const prefixReferencesFromPanel = (id: string, references: Reference[]): Reference[] => { - const prefix = `${id}:`; - return references - .filter((reference) => reference.type !== 'tag') // panel references should never contain tags. If they do, they must be removed - .map((reference) => ({ - ...reference, - name: `${prefix}${reference.name}`, - })); -}; - -const controlGroupReferencePrefix = 'controlGroup_'; - -export const createInject = ( - persistableStateService: EmbeddablePersistableStateService -): EmbeddablePersistableStateService['inject'] => { - return (state: EmbeddableStateWithType, references: Reference[]) => { - const workingState = { ...state } as - | EmbeddableStateWithType - | ParsedDashboardAttributesWithType; - - if ('panels' in workingState) { - workingState.panels = { ...workingState.panels }; - - for (const [key, panel] of Object.entries(workingState.panels)) { - workingState.panels[key] = { ...panel }; - const filteredReferences = getReferencesForPanelId(key, references); - const panelReferences = filteredReferences.length === 0 ? references : filteredReferences; - - /** - * Inject saved object ID back into the explicit input. - * - * TODO move this logic into the persistable state service inject method for each panel type - * that could be by value or by reference - */ - if (panel.panelRefName !== undefined) { - const matchingReference = panelReferences.find( - (reference) => reference.name === panel.panelRefName - ); - - if (!matchingReference) { - throw new Error(`Could not find reference "${panel.panelRefName}"`); - } - - if (matchingReference !== undefined) { - workingState.panels[key] = { - ...panel, - type: matchingReference.type, - explicitInput: { - ...workingState.panels[key].explicitInput, - savedObjectId: matchingReference.id, - }, - }; - - delete workingState.panels[key].panelRefName; - } - } - - const { type, ...injectedState } = persistableStateService.inject( - { ...workingState.panels[key].explicitInput, type: workingState.panels[key].type }, - panelReferences - ); - - workingState.panels[key].explicitInput = injectedState; - } - } - - return workingState as EmbeddableStateWithType; - }; -}; - -export const createExtract = ( - persistableStateService: EmbeddablePersistableStateService -): EmbeddablePersistableStateService['extract'] => { - return (state: EmbeddableStateWithType) => { - const workingState = { ...state } as - | EmbeddableStateWithType - | ParsedDashboardAttributesWithType; - - const references: Reference[] = []; - - if ('panels' in workingState) { - workingState.panels = { ...workingState.panels }; - - // Run every panel through the state service to get the nested references - for (const [id, panel] of Object.entries(workingState.panels)) { - /** - * Extract saved object ID reference from the explicit input. - * - * TODO move this logic into the persistable state service extract method for each panel type - * that could be by value or by reference. - */ - const savedObjectId = (panel.explicitInput as { savedObjectId?: string }).savedObjectId; - if (savedObjectId) { - panel.panelRefName = `panel_${id}`; - - references.push({ - name: `${id}:panel_${id}`, - type: panel.type, - id: savedObjectId, - }); - - delete (panel.explicitInput as { savedObjectId?: string }).savedObjectId; - } - - const { state: panelState, references: panelReferences } = persistableStateService.extract({ - ...panel.explicitInput, - type: panel.type, - }); - - references.push(...prefixReferencesFromPanel(id, panelReferences)); - - const { type, ...restOfState } = panelState; - workingState.panels[id].explicitInput = restOfState; - } - } - - return { state: workingState as EmbeddableStateWithType, references }; - }; -}; diff --git a/src/platform/plugins/shared/dashboard/common/dashboard_container/types.ts b/src/platform/plugins/shared/dashboard/common/dashboard_container/types.ts deleted file mode 100644 index a70c171124fd..000000000000 --- a/src/platform/plugins/shared/dashboard/common/dashboard_container/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { Reference } from '@kbn/content-management-utils'; - -import type { GridData } from '../../server/content_management'; - -export interface DashboardSectionMap { - [id: string]: DashboardSectionState; -} - -export interface DashboardSectionState { - title: string; - collapsed?: boolean; // if undefined, then collapsed is false - readonly gridData: Pick; - id: string; -} - -export interface DashboardPanelMap { - [key: string]: DashboardPanelState; -} - -export interface DashboardPanelState { - type: string; - explicitInput: PanelState; - readonly gridData: GridData & { sectionId?: string }; - panelRefName?: string; - - /** - * This version key was used to store Kibana version information from versions 7.3.0 -> 8.11.0. - * As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the - * embeddable's input. This key is needed for BWC, but its value will be removed on Dashboard save. - */ - version?: string; - /** - * React embeddables are serialized and may pass references that are later used in factory's deserialize method. - */ - references?: Reference[]; -} diff --git a/src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.test.ts b/src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.test.ts deleted file mode 100644 index e9bd6aff0fe1..000000000000 --- a/src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.test.ts +++ /dev/null @@ -1,454 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - extractReferences, - injectReferences, - InjectExtractDeps, -} from './dashboard_saved_object_references'; - -import { - createExtract, - createInject, -} from '../../dashboard_container/persistable_state/dashboard_container_references'; -import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks'; -import type { DashboardAttributes, DashboardItem } from '../../../server/content_management'; -import { DashboardAttributesAndReferences } from '../../types'; - -const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock(); -const dashboardInject = createInject(embeddablePersistableStateServiceMock); -const dashboardExtract = createExtract(embeddablePersistableStateServiceMock); - -embeddablePersistableStateServiceMock.extract.mockImplementation((state) => { - if (state.type === 'dashboard') { - return dashboardExtract(state); - } - - return { state, references: [] }; -}); - -embeddablePersistableStateServiceMock.inject.mockImplementation((state, references) => { - if (state.type === 'dashboard') { - return dashboardInject(state, references); - } - - return state; -}); -const deps: InjectExtractDeps = { - embeddablePersistableStateService: embeddablePersistableStateServiceMock, -}; - -const commonAttributes: DashboardAttributes = { - kibanaSavedObjectMeta: { searchSource: {} }, - timeRestore: false, - version: 1, - options: { - hidePanelTitles: false, - useMargins: true, - syncColors: true, - syncCursor: true, - syncTooltips: true, - }, - panels: [], - description: '', - title: '', -}; - -describe('extractReferences', () => { - test('extracts references from panels', () => { - const doc = { - id: '1', - attributes: { - ...commonAttributes, - foo: true, - panels: [ - { - panelIndex: 'panel-1', - type: 'visualization', - id: '1', - title: 'Title 1', - version: '7.9.1', - gridData: { x: 0, y: 0, w: 1, h: 1, i: 'panel-1' }, - panelConfig: {}, - }, - { - panelIndex: 'panel-2', - type: 'visualization', - id: '2', - title: 'Title 2', - version: '7.9.1', - gridData: { x: 1, y: 1, w: 2, h: 2, i: 'panel-2' }, - panelConfig: {}, - }, - ], - }, - references: [], - }; - const updatedDoc = extractReferences(doc, deps); - - expect(updatedDoc).toMatchInlineSnapshot(` - Object { - "attributes": Object { - "description": "", - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSource": Object {}, - }, - "options": Object { - "hidePanelTitles": false, - "syncColors": true, - "syncCursor": true, - "syncTooltips": true, - "useMargins": true, - }, - "panels": Array [ - Object { - "gridData": Object { - "h": 1, - "i": "panel-1", - "w": 1, - "x": 0, - "y": 0, - }, - "panelConfig": Object {}, - "panelIndex": "panel-1", - "panelRefName": "panel_panel-1", - "title": "Title 1", - "type": "visualization", - "version": "7.9.1", - }, - Object { - "gridData": Object { - "h": 2, - "i": "panel-2", - "w": 2, - "x": 1, - "y": 1, - }, - "panelConfig": Object {}, - "panelIndex": "panel-2", - "panelRefName": "panel_panel-2", - "title": "Title 2", - "type": "visualization", - "version": "7.9.1", - }, - ], - "timeRestore": false, - "title": "", - "version": 1, - }, - "references": Array [ - Object { - "id": "1", - "name": "panel-1:panel_panel-1", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel-2:panel_panel-2", - "type": "visualization", - }, - ], - } - `); - }); - - test('fails when "type" attribute is missing from a panel', () => { - const doc = { - id: '1', - attributes: { - ...commonAttributes, - foo: true, - panels: [ - { - id: '1', - title: 'Title 1', - version: '7.9.1', - }, - ], - }, - references: [], - } as unknown as DashboardAttributesAndReferences; - expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot( - `"\\"type\\" attribute is missing from panel \\"0\\""` - ); - }); - - test('passes when "id" attribute is missing from a panel', () => { - const doc = { - id: '1', - attributes: { - ...commonAttributes, - foo: true, - panels: [ - { - type: 'visualization', - title: 'Title 1', - version: '7.9.1', - gridData: { x: 0, y: 0, w: 1, h: 1, i: 'panel-1' }, - panelConfig: {}, - }, - ], - }, - references: [], - }; - expect(extractReferences(doc as unknown as DashboardItem, deps)).toMatchInlineSnapshot(` - Object { - "attributes": Object { - "description": "", - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSource": Object {}, - }, - "options": Object { - "hidePanelTitles": false, - "syncColors": true, - "syncCursor": true, - "syncTooltips": true, - "useMargins": true, - }, - "panels": Array [ - Object { - "gridData": Object { - "h": 1, - "i": "panel-1", - "w": 1, - "x": 0, - "y": 0, - }, - "panelConfig": Object {}, - "panelIndex": "0", - "title": "Title 1", - "type": "visualization", - "version": "7.9.1", - }, - ], - "timeRestore": false, - "title": "", - "version": 1, - }, - "references": Array [], - } - `); - }); -}); - -describe('injectReferences', () => { - test('returns injected attributes', () => { - const attributes = { - ...commonAttributes, - id: '1', - title: 'test', - panels: [ - { - type: 'visualization', - panelRefName: 'panel_0', - panelIndex: '0', - title: 'Title 1', - version: '7.9.0', - gridData: { x: 0, y: 0, w: 1, h: 1, i: '0' }, - panelConfig: {}, - }, - { - type: 'visualization', - panelRefName: 'panel_1', - panelIndex: '1', - title: 'Title 2', - version: '7.9.0', - gridData: { x: 1, y: 1, w: 2, h: 2, i: '1' }, - panelConfig: {}, - }, - ], - }; - const references = [ - { - name: 'panel_0', - type: 'visualization', - id: '1', - }, - { - name: 'panel_1', - type: 'visualization', - id: '2', - }, - ]; - const newAttributes = injectReferences({ attributes, references }, deps); - - expect(newAttributes).toMatchInlineSnapshot(` - Object { - "description": "", - "id": "1", - "kibanaSavedObjectMeta": Object { - "searchSource": Object {}, - }, - "options": Object { - "hidePanelTitles": false, - "syncColors": true, - "syncCursor": true, - "syncTooltips": true, - "useMargins": true, - }, - "panels": Array [ - Object { - "gridData": Object { - "h": 1, - "i": "0", - "w": 1, - "x": 0, - "y": 0, - }, - "id": "1", - "panelConfig": Object {}, - "panelIndex": "0", - "title": "Title 1", - "type": "visualization", - "version": "7.9.0", - }, - Object { - "gridData": Object { - "h": 2, - "i": "1", - "w": 2, - "x": 1, - "y": 1, - }, - "id": "2", - "panelConfig": Object {}, - "panelIndex": "1", - "title": "Title 2", - "type": "visualization", - "version": "7.9.0", - }, - ], - "timeRestore": false, - "title": "test", - "version": 1, - } - `); - }); - - test('skips when panels is missing', () => { - const attributes = { - id: '1', - title: 'test', - } as unknown as DashboardAttributes; - const newAttributes = injectReferences({ attributes, references: [] }, deps); - expect(newAttributes).toMatchInlineSnapshot(` - Object { - "id": "1", - "panels": Array [], - "title": "test", - } - `); - }); - - test('skips a panel when panelRefName is missing', () => { - const attributes = { - ...commonAttributes, - id: '1', - title: 'test', - panels: [ - { - type: 'visualization', - panelRefName: 'panel_0', - panelIndex: '0', - title: 'Title 1', - gridData: { x: 0, y: 0, w: 1, h: 1, i: '0' }, - panelConfig: {}, - }, - { - type: 'visualization', - panelIndex: '1', - title: 'Title 2', - gridData: { x: 1, y: 1, w: 2, h: 2, i: '1' }, - panelConfig: {}, - }, - ], - }; - const references = [ - { - name: 'panel_0', - type: 'visualization', - id: '1', - }, - ]; - const newAttributes = injectReferences({ attributes, references }, deps); - expect(newAttributes).toMatchInlineSnapshot(` - Object { - "description": "", - "id": "1", - "kibanaSavedObjectMeta": Object { - "searchSource": Object {}, - }, - "options": Object { - "hidePanelTitles": false, - "syncColors": true, - "syncCursor": true, - "syncTooltips": true, - "useMargins": true, - }, - "panels": Array [ - Object { - "gridData": Object { - "h": 1, - "i": "0", - "w": 1, - "x": 0, - "y": 0, - }, - "id": "1", - "panelConfig": Object {}, - "panelIndex": "0", - "title": "Title 1", - "type": "visualization", - "version": undefined, - }, - Object { - "gridData": Object { - "h": 2, - "i": "1", - "w": 2, - "x": 1, - "y": 1, - }, - "panelConfig": Object {}, - "panelIndex": "1", - "title": "Title 2", - "type": "visualization", - "version": undefined, - }, - ], - "timeRestore": false, - "title": "test", - "version": 1, - } - `); - }); - - test(`fails when it can't find the reference in the array`, () => { - const attributes = { - ...commonAttributes, - id: '1', - title: 'test', - panels: [ - { - panelIndex: '0', - panelRefName: 'panel_0', - title: 'Title 1', - type: 'visualization', - gridData: { x: 0, y: 0, w: 1, h: 1, i: '0' }, - panelConfig: {}, - }, - ], - }; - expect(() => - injectReferences({ attributes, references: [] }, deps) - ).toThrowErrorMatchingInlineSnapshot(`"Could not find reference \\"panel_0\\""`); - }); -}); diff --git a/src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts b/src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts deleted file mode 100644 index a01acc5dd318..000000000000 --- a/src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { Reference } from '@kbn/content-management-utils'; -import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common'; - -import { - convertPanelSectionMapsToPanelsArray, - convertPanelsArrayToPanelSectionMaps, -} from '../../lib/dashboard_panel_converters'; -import { DashboardAttributesAndReferences, ParsedDashboardAttributesWithType } from '../../types'; -import type { DashboardAttributes } from '../../../server/content_management'; -import { - createExtract, - createInject, -} from '../../dashboard_container/persistable_state/dashboard_container_references'; - -export interface InjectExtractDeps { - embeddablePersistableStateService: EmbeddablePersistableStateService; -} - -function parseDashboardAttributesWithType({ - panels, -}: DashboardAttributes): ParsedDashboardAttributesWithType { - const { panels: panelsMap, sections } = convertPanelsArrayToPanelSectionMaps(panels); // drop sections - return { - type: 'dashboard', - panels: panelsMap, - sections, - } as ParsedDashboardAttributesWithType; -} - -export function injectReferences( - { attributes, references = [] }: DashboardAttributesAndReferences, - deps: InjectExtractDeps -): DashboardAttributes { - const parsedAttributes = parseDashboardAttributesWithType(attributes); - - // inject references back into panels via the Embeddable persistable state service. - const inject = createInject(deps.embeddablePersistableStateService); - const injectedState = inject(parsedAttributes, references) as ParsedDashboardAttributesWithType; - const injectedPanels = convertPanelSectionMapsToPanelsArray( - injectedState.panels, - parsedAttributes.sections - ); // sections don't have references - - const newAttributes = { - ...attributes, - panels: injectedPanels, - }; - - return newAttributes; -} - -export function extractReferences( - { attributes, references = [] }: DashboardAttributesAndReferences, - deps: InjectExtractDeps -): DashboardAttributesAndReferences { - const parsedAttributes = parseDashboardAttributesWithType(attributes); - const panels = parsedAttributes.panels; - - const panelMissingType = Object.entries(panels).find( - ([panelId, panel]) => panel.type === undefined - ); - if (panelMissingType) { - throw new Error(`"type" attribute is missing from panel "${panelMissingType[0]}"`); - } - - const extract = createExtract(deps.embeddablePersistableStateService); - const { references: extractedReferences, state: extractedState } = extract(parsedAttributes) as { - references: Reference[]; - state: ParsedDashboardAttributesWithType; - }; - const extractedPanels = convertPanelSectionMapsToPanelsArray( - extractedState.panels, - parsedAttributes.sections - ); // sections don't have references - const newAttributes = { - ...attributes, - panels: extractedPanels, - }; - - return { - references: [...references, ...extractedReferences], - attributes: newAttributes, - }; -} diff --git a/src/platform/plugins/shared/dashboard/common/index.ts b/src/platform/plugins/shared/dashboard/common/index.ts index 4d3dd779d1e2..d54b0794063c 100644 --- a/src/platform/plugins/shared/dashboard/common/index.ts +++ b/src/platform/plugins/shared/dashboard/common/index.ts @@ -14,12 +14,10 @@ export type { DashboardState, } from './types'; -export type { - DashboardPanelMap, - DashboardPanelState, - DashboardSectionMap, - DashboardSectionState, -} from './dashboard_container/types'; +export { + getReferencesForPanelId, + getReferencesForControls, + prefixReferencesFromPanel, +} from './reference_utils'; -export { type InjectExtractDeps } from './dashboard_saved_object/persistable_state/dashboard_saved_object_references'; -export { isDashboardSection } from './lib/dashboard_panel_converters'; +export { isDashboardSection } from './is_dashboard_section'; diff --git a/src/platform/plugins/shared/dashboard/common/is_dashboard_section.ts b/src/platform/plugins/shared/dashboard/common/is_dashboard_section.ts new file mode 100644 index 000000000000..b04833801520 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/common/is_dashboard_section.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { DashboardAttributes, DashboardSection } from '../server/content_management'; + +export const isDashboardSection = ( + widget: DashboardAttributes['panels'][number] +): widget is DashboardSection => { + return 'panels' in widget; +}; diff --git a/src/platform/plugins/shared/dashboard/common/lib/dashboard_panel_converters.ts b/src/platform/plugins/shared/dashboard/common/lib/dashboard_panel_converters.ts deleted file mode 100644 index 1c58750987b2..000000000000 --- a/src/platform/plugins/shared/dashboard/common/lib/dashboard_panel_converters.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { omit } from 'lodash'; -import { v4 } from 'uuid'; - -import type { Reference } from '@kbn/content-management-utils'; -import type { DashboardPanelMap, DashboardSectionMap } from '..'; -import type { - DashboardAttributes, - DashboardPanel, - DashboardSection, -} from '../../server/content_management'; - -import { - getReferencesForPanelId, - prefixReferencesFromPanel, -} from '../dashboard_container/persistable_state/dashboard_container_references'; - -export const isDashboardSection = ( - widget: DashboardAttributes['panels'][number] -): widget is DashboardSection => { - return 'panels' in widget; -}; - -export const convertPanelsArrayToPanelSectionMaps = ( - panels?: DashboardAttributes['panels'] -): { panels: DashboardPanelMap; sections: DashboardSectionMap } => { - const panelsMap: DashboardPanelMap = {}; - const sectionsMap: DashboardSectionMap = {}; - - /** - * panels and sections are mixed in the DashboardAttributes 'panels' key, so we need - * to separate them out into separate maps for the dashboard client side code - */ - panels?.forEach((widget, i) => { - if (isDashboardSection(widget)) { - const sectionId = widget.gridData.i ?? String(i); - const { panels: sectionPanels, ...restOfSection } = widget; - sectionsMap[sectionId] = { - ...restOfSection, - gridData: { - ...widget.gridData, - i: sectionId, - }, - id: sectionId, - }; - (sectionPanels as DashboardPanel[]).forEach((panel, j) => { - const panelId = panel.panelIndex ?? String(j); - const transformed = transformPanel(panel); - panelsMap[panelId] = { - ...transformed, - gridData: { ...transformed.gridData, sectionId, i: panelId }, - }; - }); - } else { - // if not a section, then this widget is a panel - panelsMap[widget.panelIndex ?? String(i)] = transformPanel(widget); - } - }); - - return { panels: panelsMap, sections: sectionsMap }; -}; - -const transformPanel = (panel: DashboardPanel): DashboardPanelMap[string] => { - return { - type: panel.type, - gridData: panel.gridData, - panelRefName: panel.panelRefName, - explicitInput: { - ...(panel.id !== undefined && { savedObjectId: panel.id }), - ...(panel.title !== undefined && { title: panel.title }), - ...panel.panelConfig, - }, - version: panel.version, - }; -}; - -export const convertPanelSectionMapsToPanelsArray = ( - panels: DashboardPanelMap, - sections: DashboardSectionMap, - removeLegacyVersion?: boolean -): DashboardAttributes['panels'] => { - const combined: DashboardAttributes['panels'] = []; - - const panelsInSections: { [sectionId: string]: DashboardSection } = {}; - Object.entries(sections).forEach(([sectionId, sectionState]) => { - panelsInSections[sectionId] = { ...omit(sectionState, 'id'), panels: [] }; - }); - Object.entries(panels).forEach(([panelId, panelState]) => { - const savedObjectId = (panelState.explicitInput as { savedObjectId?: string }).savedObjectId; - const title = (panelState.explicitInput as { title?: string }).title; - const { sectionId, ...gridData } = panelState.gridData; // drop section ID - const convertedPanelState = { - /** - * Version information used to be stored in the panel until 8.11 when it was moved to live inside the - * explicit Embeddable Input. If removeLegacyVersion is not passed, we'd like to keep this information for - * the time being. - */ - ...(!removeLegacyVersion ? { version: panelState.version } : {}), - - type: panelState.type, - gridData, - panelIndex: panelId, - panelConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']), - ...(title !== undefined && { title }), - ...(savedObjectId !== undefined && { id: savedObjectId }), - ...(panelState.panelRefName !== undefined && { panelRefName: panelState.panelRefName }), - }; - - if (sectionId) { - panelsInSections[sectionId].panels.push(convertedPanelState); - } else { - combined.push(convertedPanelState); - } - }); - - return [...combined, ...Object.values(panelsInSections)]; -}; - -/** - * When saving a dashboard as a copy, we should generate new IDs for all panels so that they are - * properly refreshed when navigating between Dashboards - */ -export const generateNewPanelIds = (panels: DashboardPanelMap, references?: Reference[]) => { - const newPanelsMap: DashboardPanelMap = {}; - const newReferences: Reference[] = []; - for (const [oldId, panel] of Object.entries(panels)) { - const newId = v4(); - newPanelsMap[newId] = { - ...panel, - gridData: { ...panel.gridData, i: newId }, - explicitInput: panel.explicitInput ?? {}, - }; - newReferences.push( - ...prefixReferencesFromPanel(newId, getReferencesForPanelId(oldId, references ?? [])) - ); - } - return { panels: newPanelsMap, references: newReferences }; -}; diff --git a/src/platform/plugins/shared/dashboard/common/reference_utils.ts b/src/platform/plugins/shared/dashboard/common/reference_utils.ts new file mode 100644 index 000000000000..d4c8174d5a70 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/common/reference_utils.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { Reference } from '@kbn/content-management-utils'; + +const controlGroupReferencePrefix = 'controlGroup_'; + +export const getReferencesForPanelId = (id: string, references: Reference[]): Reference[] => { + const prefix = `${id}:`; + const filteredReferences = references + .filter((reference) => reference.name.indexOf(prefix) === 0) + .map((reference) => ({ ...reference, name: reference.name.replace(prefix, '') })); + return filteredReferences; +}; + +export const getReferencesForControls = (references: Reference[]): Reference[] => { + return references.filter((reference) => reference.name.startsWith(controlGroupReferencePrefix)); +}; + +export const prefixReferencesFromPanel = (id: string, references: Reference[]): Reference[] => { + const prefix = `${id}:`; + return references + .filter((reference) => reference.type !== 'tag') // panel references should never contain tags. If they do, they must be removed + .map((reference) => ({ + ...reference, + name: `${prefix}${reference.name}`, + })); +}; diff --git a/src/platform/plugins/shared/dashboard/common/types.ts b/src/platform/plugins/shared/dashboard/common/types.ts index 35380e90a4e2..6c120b8b706d 100644 --- a/src/platform/plugins/shared/dashboard/common/types.ts +++ b/src/platform/plugins/shared/dashboard/common/types.ts @@ -12,15 +12,9 @@ import type { SerializableRecord, Writable } from '@kbn/utility-types'; import type { Filter, Query, TimeRange } from '@kbn/es-query'; import type { ViewMode } from '@kbn/presentation-publishing'; import type { RefreshInterval } from '@kbn/data-plugin/public'; -import type { ControlGroupSerializedState } from '@kbn/controls-plugin/common'; -import type { DashboardPanelMap, DashboardSectionMap } from './dashboard_container/types'; -import type { - DashboardAttributes, - DashboardOptions, - DashboardPanel, - DashboardSection, -} from '../server/content_management'; +import { ControlGroupSerializedState } from '@kbn/controls-plugin/common'; +import type { DashboardAttributes, DashboardOptions } from '../server/content_management'; export interface DashboardCapabilities { showWriteControls: boolean; @@ -29,16 +23,6 @@ export interface DashboardCapabilities { [key: string]: boolean; } -/** - * A partially parsed version of the Dashboard Attributes used for inject and extract logic for both the Dashboard Container and the Dashboard Saved Object. - */ -export interface ParsedDashboardAttributesWithType { - id: string; - panels: DashboardPanelMap; - sections: DashboardSectionMap; - type: 'dashboard'; -} - export interface DashboardAttributesAndReferences { attributes: DashboardAttributes; references: Reference[]; @@ -57,8 +41,7 @@ export interface DashboardState extends DashboardSettings { timeRange?: TimeRange; refreshInterval?: RefreshInterval; viewMode: ViewMode; - panels: DashboardPanelMap; - sections: DashboardSectionMap; + panels: DashboardAttributes['panels']; /** * Temporary. Currently Dashboards are in charge of providing references to all of their children. @@ -74,11 +57,9 @@ export interface DashboardState extends DashboardSettings { } export type DashboardLocatorParams = Partial< - Omit & { + DashboardState & { controlGroupInput?: DashboardState['controlGroupInput'] & SerializableRecord; - panels: Array; - references?: DashboardState['references'] & SerializableRecord; /** diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts index a296c75dd814..0cfda49ed6ca 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts @@ -15,8 +15,7 @@ export const DEFAULT_DASHBOARD_STATE: DashboardState = { query: { query: '', language: 'kuery' }, description: '', filters: [], - panels: {}, - sections: {}, + panels: [], title: '', tags: [], diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/generate_new_panel_ids.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/generate_new_panel_ids.test.ts new file mode 100644 index 000000000000..88fd2b26def5 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/generate_new_panel_ids.test.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { generateNewPanelIds } from './generate_new_panel_ids'; + +jest.mock('uuid', () => { + let count = 0; + return { + v4: () => `${100 + count++}`, + }; +}); + +describe('generateNewPanelIds', () => { + test('should generate new ids for panels', () => { + const { newPanels, newPanelReferences } = generateNewPanelIds( + [ + { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' }, + panelConfig: { title: 'panel One' }, + panelIndex: '1', + type: 'testPanelType', + }, + ], + [ + { + type: 'refType', + id: 'ref1', + name: '1:panel_1', + }, + ] + ); + + expect(newPanels).toEqual([ + { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '100' }, + panelConfig: { title: 'panel One' }, + panelIndex: '100', + type: 'testPanelType', + }, + ]); + + expect(newPanelReferences).toEqual([ + { + type: 'refType', + id: 'ref1', + name: '100:panel_1', + }, + ]); + }); + + test('should generate new ids for panels with sections', () => { + const { newPanels, newPanelReferences } = generateNewPanelIds( + [ + { + title: 'Section One', + collapsed: true, + gridData: { + y: 6, + i: 'section1', + }, + panels: [ + { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' }, + panelConfig: { title: 'panel One' }, + panelIndex: '1', + type: 'testPanelType', + }, + ], + }, + ], + [ + { + type: 'refType', + id: 'ref1', + name: '1:panel_1', + }, + ] + ); + + expect(newPanels).toEqual([ + { + title: 'Section One', + collapsed: true, + gridData: { + y: 6, + i: '101', + }, + panels: [ + { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '102' }, + panelConfig: { title: 'panel One' }, + panelIndex: '102', + type: 'testPanelType', + }, + ], + }, + ]); + + expect(newPanelReferences).toEqual([ + { + type: 'refType', + id: 'ref1', + name: '102:panel_1', + }, + ]); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/generate_new_panel_ids.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/generate_new_panel_ids.ts new file mode 100644 index 000000000000..a3b922dee0b7 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/generate_new_panel_ids.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { v4 } from 'uuid'; +import type { Reference } from '@kbn/content-management-utils'; +import type { DashboardPanel } from '../../server'; +import { + DashboardState, + getReferencesForPanelId, + isDashboardSection, + prefixReferencesFromPanel, +} from '../../common'; + +export function generateNewPanelIds(panels: DashboardState['panels'], references?: Reference[]) { + const newPanels: DashboardState['panels'] = []; + const newPanelReferences: Reference[] = []; + + function generateNewPanelId(panel: DashboardPanel) { + const newPanelId = v4(); + const oldPanelId = panel.panelIndex ?? panel.gridData.i; + const panelReferences = + oldPanelId && references ? getReferencesForPanelId(oldPanelId, references) : []; + + newPanelReferences.push(...prefixReferencesFromPanel(newPanelId, panelReferences)); + + return { + ...panel, + panelIndex: newPanelId, + gridData: { ...panel.gridData, i: newPanelId }, + }; + } + + for (const panel of panels) { + if (isDashboardSection(panel)) { + const section = panel; + const newSectionId = v4(); + newPanels.push({ + ...section, + gridData: { ...section.gridData, i: newSectionId }, + panels: section.panels.map((panelInSection) => { + return generateNewPanelId(panelInSection as DashboardPanel); + }), + }); + } else { + newPanels.push(generateNewPanelId(panel)); + } + } + return { newPanels, newPanelReferences }; +} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts index 5e490569e491..032c16b571f3 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts @@ -12,10 +12,7 @@ import { EmbeddablePackageState } from '@kbn/embeddable-plugin/public'; import { BehaviorSubject, debounceTime, merge } from 'rxjs'; import { v4 } from 'uuid'; import { DASHBOARD_APP_ID } from '../../common/constants'; -import { - getReferencesForControls, - getReferencesForPanelId, -} from '../../common/dashboard_container/persistable_state/dashboard_container_references'; +import { getReferencesForControls, getReferencesForPanelId } from '../../common'; import type { DashboardState } from '../../common/types'; import { getDashboardContentManagementService } from '../services/dashboard_content_management_service'; import { LoadDashboardReturn } from '../services/dashboard_content_management_service/types'; @@ -81,7 +78,6 @@ export function getDashboardApi({ const layoutManager = initializeLayoutManager( incomingEmbeddable, initialState.panels, - initialState.sections, trackPanel, getReferences ); @@ -116,18 +112,13 @@ export function getDashboardApi({ }); function getState() { - const { - panels, - sections, - references: panelReferences, - } = layoutManager.internalApi.serializeLayout(); + const { panels, references: panelReferences } = layoutManager.internalApi.serializeLayout(); const { state: unifiedSearchState, references: searchSourceReferences } = unifiedSearchManager.internalApi.getState(); const dashboardState: DashboardState = { ...settingsManager.api.getSettings(), ...unifiedSearchState, panels, - sections, viewMode: viewModeManager.api.viewMode$.value, }; @@ -138,7 +129,7 @@ export function getDashboardApi({ return { dashboardState, controlGroupReferences, - panelReferences, + panelReferences: panelReferences ?? [], searchSourceReferences, }; } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts index d3a8786c67c9..d05e01a18b7a 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { DashboardPanelState } from '../../common'; +import type { DashboardPanel } from '../../server'; import { dataService, savedObjectsTaggingService } from '../services/kibana_services'; import { getSampleDashboardState } from '../mocks'; @@ -88,7 +88,7 @@ describe('getSerializedState', () => { it('should generate new IDs for panels and references when generateNewIds is true', () => { const dashboardState = { ...getSampleDashboardState(), - panels: { oldPanelId: { type: 'visualization' } as unknown as DashboardPanelState }, + panels: [{ panelIndex: 'oldPanelId', type: 'visualization' } as DashboardPanel], }; const result = getSerializedState({ controlGroupReferences: [], @@ -110,7 +110,6 @@ describe('getSerializedState', () => { "gridData": Object { "i": "54321", }, - "panelConfig": Object {}, "panelIndex": "54321", "type": "visualization", }, @@ -158,69 +157,4 @@ describe('getSerializedState', () => { expect(result.references).toEqual(panelReferences); }); - - it('should serialize sections', () => { - const dashboardState = { - ...getSampleDashboardState(), - panels: { - oldPanelId: { - type: 'visualization', - gridData: { sectionId: 'section1' }, - } as unknown as DashboardPanelState, - }, - sections: { - section1: { - id: 'section1', - title: 'Section One', - collapsed: false, - gridData: { y: 1, i: 'section1' }, - }, - section2: { - id: 'section2', - title: 'Section Two', - collapsed: true, - gridData: { y: 2, i: 'section2' }, - }, - }, - }; - const result = getSerializedState({ - controlGroupReferences: [], - generateNewIds: true, - dashboardState, - panelReferences: [], - searchSourceReferences: [], - }); - - expect(result.attributes.panels).toMatchInlineSnapshot(` - Array [ - Object { - "collapsed": false, - "gridData": Object { - "i": "section1", - "y": 1, - }, - "panels": Array [ - Object { - "gridData": Object { - "i": "54321", - }, - "panelConfig": Object {}, - "panelIndex": "54321", - "type": "visualization", - }, - ], - "title": "Section One", - }, - Object { - "collapsed": true, - "gridData": Object { - "i": "section2", - "y": 2, - }, - "panels": Array [], - "title": "Section Two", - }, - ] - `); - }); }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts index 29286088651d..4d2aa7713553 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts @@ -12,10 +12,6 @@ import { pick } from 'lodash'; import moment, { Moment } from 'moment'; import type { Reference } from '@kbn/content-management-utils'; -import { - convertPanelSectionMapsToPanelsArray, - generateNewPanelIds, -} from '../../common/lib/dashboard_panel_converters'; import type { DashboardAttributes } from '../../server'; import type { DashboardState } from '../../common'; @@ -26,6 +22,7 @@ import { } from '../services/dashboard_content_management_service/lib/dashboard_versioning'; import { dataService, savedObjectsTaggingService } from '../services/kibana_services'; import { DashboardApi } from './types'; +import { generateNewPanelIds } from './generate_new_panel_ids'; const LATEST_DASHBOARD_CONTAINER_VERSION = convertNumberToDashboardVersion(LATEST_VERSION); @@ -64,7 +61,6 @@ export const getSerializedState = ({ filters, timeRestore, description, - sections, // Dashboard options useMargins, @@ -78,10 +74,7 @@ export const getSerializedState = ({ let { panels } = dashboardState; let prefixedPanelReferences = panelReferences; if (generateNewIds) { - const { panels: newPanels, references: newPanelReferences } = generateNewPanelIds( - panels, - panelReferences - ); + const { newPanels, newPanelReferences } = generateNewPanelIds(panels, panelReferences); panels = newPanels; prefixedPanelReferences = newPanelReferences; // @@ -98,7 +91,6 @@ export const getSerializedState = ({ syncTooltips, hidePanelTitles, }; - const savedPanels = convertPanelSectionMapsToPanelsArray(panels, sections, true); /** * Parse global time filter settings @@ -123,7 +115,7 @@ export const getSerializedState = ({ refreshInterval, timeRestore, options, - panels: savedPanels, + panels, timeFrom, title, timeTo, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/are_layouts_equal.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/are_layouts_equal.ts similarity index 100% rename from src/platform/plugins/shared/dashboard/public/dashboard_api/are_layouts_equal.ts rename to src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/are_layouts_equal.ts diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/deserialize_layout.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/deserialize_layout.test.ts new file mode 100644 index 000000000000..345a3f75b0f6 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/deserialize_layout.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { deserializeLayout } from './deserialize_layout'; + +describe('deserializeLayout', () => { + test('should deserialize panels', () => { + const { layout, childState } = deserializeLayout( + [ + { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' }, + panelConfig: { title: 'panel One' }, + panelIndex: '1', + type: 'testPanelType', + }, + { + title: 'Section One', + collapsed: true, + gridData: { + y: 6, + i: 'section1', + }, + panels: [ + { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '3' }, + panelConfig: { title: 'panel Three' }, + panelIndex: '3', + type: 'testPanelType', + }, + ], + }, + ], + () => [] + ); + expect(layout.panels).toMatchInlineSnapshot(` + Object { + "1": Object { + "gridData": Object { + "h": 6, + "i": "1", + "w": 6, + "x": 0, + "y": 0, + }, + "type": "testPanelType", + }, + "3": Object { + "gridData": Object { + "h": 6, + "i": "3", + "sectionId": "section1", + "w": 6, + "x": 0, + "y": 0, + }, + "type": "testPanelType", + }, + } + `); + expect(layout.sections).toMatchInlineSnapshot(` + Object { + "section1": Object { + "collapsed": true, + "gridData": Object { + "i": "section1", + "y": 6, + }, + "title": "Section One", + }, + } + `); + expect(childState).toMatchInlineSnapshot(` + Object { + "1": Object { + "rawState": Object { + "title": "panel One", + }, + "references": Array [], + }, + "3": Object { + "rawState": Object { + "title": "panel Three", + }, + "references": Array [], + }, + } + `); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/deserialize_layout.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/deserialize_layout.ts new file mode 100644 index 000000000000..ef9f961b0cd3 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/deserialize_layout.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { v4 } from 'uuid'; +import type { Reference } from '@kbn/content-management-utils'; +import { type DashboardState, isDashboardSection } from '../../../common'; +import type { DashboardPanel } from '../../../server'; +import type { DashboardChildState, DashboardLayout } from './types'; + +export function deserializeLayout( + panels: DashboardState['panels'], + getReferences: (id: string) => Reference[] +) { + const layout: DashboardLayout = { + panels: {}, + sections: {}, + }; + const childState: DashboardChildState = {}; + + function pushPanel(panel: DashboardPanel, sectionId?: string) { + const panelId = panel.panelIndex ?? v4(); + layout.panels[panelId] = { + type: panel.type, + gridData: { + ...panel.gridData, + ...(sectionId && { sectionId }), + i: panelId, + }, + }; + childState[panelId] = { + rawState: { + ...panel.panelConfig, + }, + references: getReferences(panelId), + }; + } + + panels.forEach((widget) => { + if (isDashboardSection(widget)) { + const sectionId = widget.gridData.i ?? v4(); + const { panels: sectionPanels, ...restOfSection } = widget; + layout.sections[sectionId] = { + collapsed: false, + ...restOfSection, + gridData: { + ...widget.gridData, + i: sectionId, + }, + }; + (sectionPanels as DashboardPanel[]).forEach((panel) => { + pushPanel(panel, sectionId); + }); + } else { + // if not a section, then this widget is a panel + pushPanel(widget); + } + }); + return { layout, childState }; +} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/index.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/index.ts new file mode 100644 index 000000000000..6fdc95576e48 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/index.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 + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export type { DashboardLayout, DashboardLayoutPanel } from './types'; +export { areLayoutsEqual } from './are_layouts_equal'; +export { initializeLayoutManager } from './layout_manager'; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.test.ts similarity index 65% rename from src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager.test.ts rename to src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.test.ts index c777434b2b8e..587257008973 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.test.ts @@ -7,9 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { DashboardPanelMap } from '../../common'; import { initializeLayoutManager } from './layout_manager'; -import { initializeTrackPanel } from './track_panel'; +import { initializeTrackPanel } from '../track_panel'; import type { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; import { HasLibraryTransforms, @@ -32,25 +31,28 @@ describe('layout manager', () => { jest.clearAllMocks(); }); - const panels: DashboardPanelMap = { - panelOne: { - gridData: { w: 1, h: 1, x: 0, y: 0, i: 'panelOne' }, + const PANEL_ONE_ID = 'panelOne'; + + const panels = [ + { + gridData: { w: 1, h: 1, x: 0, y: 0, i: PANEL_ONE_ID }, type: 'testPanelType', - explicitInput: { title: 'Panel One' }, + panelConfig: { title: 'Panel One' }, + panelIndex: PANEL_ONE_ID, }, - }; + ]; const childApi: DefaultEmbeddableApi = { - type: panels.panelOne.type, - uuid: panels.panelOne.gridData.i, + type: 'testPanelType', + uuid: PANEL_ONE_ID, phase$: {} as unknown as PublishingSubject, serializeState: jest.fn(), }; test('can register child APIs', () => { - const layoutManager = initializeLayoutManager(undefined, panels, {}, trackPanelMock, () => []); + const layoutManager = initializeLayoutManager(undefined, panels, trackPanelMock, () => []); layoutManager.internalApi.registerChildApi(childApi); - expect(layoutManager.api.children$.getValue()[panels.panelOne.gridData.i]).toBe(childApi); + expect(layoutManager.api.children$.getValue()[PANEL_ONE_ID]).toBe(childApi); }); test('should append incoming embeddable to existing panels', () => { @@ -70,7 +72,6 @@ describe('layout manager', () => { const layoutManager = initializeLayoutManager( incomingEmbeddable, panels, - {}, trackPanelMock, () => [] ); @@ -94,54 +95,8 @@ describe('layout manager', () => { }); }); - describe('serializeLayout', () => { - test('should serialize the latest state of all panels', () => { - const layoutManager = initializeLayoutManager( - undefined, - panels, - {}, - trackPanelMock, - () => [] - ); - - layoutManager.internalApi.registerChildApi(childApi); - layoutManager.internalApi.setChildState(panels.panelOne.gridData.i, { - rawState: { title: 'Updated Panel One' }, - }); - const serializedLayout = layoutManager.internalApi.serializeLayout(); - expect(serializedLayout.panels).toEqual({ - panelOne: { - gridData: { w: 1, h: 1, x: 0, y: 0, i: 'panelOne' }, - type: 'testPanelType', - explicitInput: { title: 'Updated Panel One' }, - }, - }); - }); - - test('should serialize the latest state of all panels when a child API is unavailable', () => { - const layoutManager = initializeLayoutManager( - undefined, - panels, - {}, - trackPanelMock, - () => [] - ); - expect(layoutManager.api.children$.getValue()[panels.panelOne.gridData.i]).toBe(undefined); - - // serializing should still work without an API present, returning the last known state of the panel - const serializedLayout = layoutManager.internalApi.serializeLayout(); - expect(serializedLayout.panels).toEqual({ - panelOne: { - gridData: { w: 1, h: 1, x: 0, y: 0, i: 'panelOne' }, - type: 'testPanelType', - explicitInput: { title: 'Panel One' }, - }, - }); - }); - }); - describe('duplicatePanel', () => { - const titleManager = initializeTitleManager(panels.panelOne.explicitInput); + const titleManager = initializeTitleManager(panels[0].panelConfig); const childApiToDuplicate = { ...childApi, ...titleManager.api, @@ -151,13 +106,7 @@ describe('layout manager', () => { }; test('should add duplicated panel to layout', async () => { - const layoutManager = initializeLayoutManager( - undefined, - panels, - {}, - trackPanelMock, - () => [] - ); + const layoutManager = initializeLayoutManager(undefined, panels, trackPanelMock, () => []); layoutManager.internalApi.registerChildApi(childApiToDuplicate); await layoutManager.api.duplicatePanel('panelOne'); @@ -182,13 +131,7 @@ describe('layout manager', () => { }); test('should clone by reference embeddable as by value', async () => { - const layoutManager = initializeLayoutManager( - undefined, - panels, - {}, - trackPanelMock, - () => [] - ); + const layoutManager = initializeLayoutManager(undefined, panels, trackPanelMock, () => []); layoutManager.internalApi.registerChildApi({ ...childApiToDuplicate, checkForDuplicateTitle: jest.fn(), @@ -213,13 +156,7 @@ describe('layout manager', () => { }); test('should give a correct title to the clone of a clone', async () => { - const layoutManager = initializeLayoutManager( - undefined, - panels, - {}, - trackPanelMock, - () => [] - ); + const layoutManager = initializeLayoutManager(undefined, panels, trackPanelMock, () => []); const titleManagerOfClone = initializeTitleManager({ title: 'Panel One (copy)' }); layoutManager.internalApi.registerChildApi({ ...childApiToDuplicate, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.ts similarity index 73% rename from src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager.ts rename to src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.ts index 3ddf9c7fedf5..624e193443be 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.ts @@ -8,7 +8,15 @@ */ import { filter, map as lodashMap, max } from 'lodash'; -import { BehaviorSubject, Observable, combineLatestWith, debounceTime, map, merge } from 'rxjs'; +import { + BehaviorSubject, + Observable, + combineLatestWith, + debounceTime, + map, + merge, + tap, +} from 'rxjs'; import { v4 } from 'uuid'; import { METRIC_TYPE } from '@kbn/analytics'; @@ -29,30 +37,29 @@ import { apiPublishesUnsavedChanges, getTitle, logStateDiff, - shouldLogStateDiff, } from '@kbn/presentation-publishing'; import { asyncForEach } from '@kbn/std'; -import type { DashboardSectionMap, DashboardState } from '../../common'; -import { DashboardPanelMap } from '../../common'; -import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../common/content_management'; -import { prefixReferencesFromPanel } from '../../common/dashboard_container/persistable_state/dashboard_container_references'; -import { dashboardClonePanelActionStrings } from '../dashboard_actions/_dashboard_actions_strings'; -import { getPanelAddedSuccessString } from '../dashboard_app/_dashboard_app_strings'; -import { getPanelPlacementSetting } from '../panel_placement/get_panel_placement_settings'; -import { placeClonePanel } from '../panel_placement/place_clone_panel_strategy'; -import { runPanelPlacementStrategy } from '../panel_placement/place_new_panel_strategies'; -import { PanelPlacementStrategy } from '../plugin_constants'; -import { coreServices, usageCollectionService } from '../services/kibana_services'; -import { DASHBOARD_UI_METRIC_ID } from '../utils/telemetry_constants'; +import type { DashboardPanel } from '../../../server'; +import type { DashboardState } from '../../../common'; +import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../common/content_management'; +import { dashboardClonePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings'; +import { getPanelAddedSuccessString } from '../../dashboard_app/_dashboard_app_strings'; +import { getPanelPlacementSetting } from '../../panel_placement/get_panel_placement_settings'; +import { placeClonePanel } from '../../panel_placement/place_clone_panel_strategy'; +import { runPanelPlacementStrategy } from '../../panel_placement/place_new_panel_strategies'; +import { PanelPlacementStrategy } from '../../plugin_constants'; +import { coreServices, usageCollectionService } from '../../services/kibana_services'; +import { DASHBOARD_UI_METRIC_ID } from '../../utils/telemetry_constants'; import { areLayoutsEqual } from './are_layouts_equal'; -import type { initializeTrackPanel } from './track_panel'; -import { DashboardChildState, DashboardChildren, DashboardLayout, DashboardPanel } from './types'; +import type { initializeTrackPanel } from '../track_panel'; +import { deserializeLayout } from './deserialize_layout'; +import { serializeLayout } from './serialize_layout'; +import type { DashboardChildren, DashboardLayout, DashboardLayoutPanel } from './types'; export function initializeLayoutManager( incomingEmbeddable: EmbeddablePackageState | undefined, - initialPanels: DashboardPanelMap, // SERIALIZED STATE ONLY TODO Remove the DashboardPanelMap layer. We could take the Saved Dashboard Panels array here directly. - initialSections: DashboardSectionMap, + initialPanels: DashboardState['panels'], trackPanel: ReturnType, getReferences: (id: string) => Reference[] ) { @@ -62,76 +69,16 @@ export function initializeLayoutManager( const children$ = new BehaviorSubject({}); const { layout: initialLayout, childState: initialChildState } = deserializeLayout( initialPanels, - initialSections + getReferences ); const layout$ = new BehaviorSubject(initialLayout); // layout is the source of truth for which panels are in the dashboard. let currentChildState = initialChildState; // childState is the source of truth for the state of each panel. - function deserializeLayout(panelMap: DashboardPanelMap, sectionMap: DashboardSectionMap) { - const layout: DashboardLayout = { - panels: {}, - sections: {}, - }; - const childState: DashboardChildState = {}; - Object.keys(sectionMap).forEach((sectionId) => { - layout.sections[sectionId] = { collapsed: false, ...sectionMap[sectionId] }; - }); - Object.keys(panelMap).forEach((panelId) => { - const { gridData, explicitInput, type } = panelMap[panelId]; - layout.panels[panelId] = { type, gridData } as DashboardPanel; - childState[panelId] = { - rawState: explicitInput, - references: getReferences(panelId), - }; - }); - return { layout, childState }; - } - - const serializeLayout = (): { - references: Reference[]; - panels: DashboardPanelMap; - sections: DashboardSectionMap; - } => { - const references: Reference[] = []; - const layout = layout$.value; - const panels: DashboardPanelMap = {}; - - for (const panelId of Object.keys(layout.panels)) { - references.push( - ...prefixReferencesFromPanel(panelId, currentChildState[panelId]?.references ?? []) - ); - panels[panelId] = { - ...layout.panels[panelId], - explicitInput: currentChildState[panelId]?.rawState ?? {}, - }; - - // TODO move savedObjectRef extraction into embeddable implemenations - const savedObjectId = (panels[panelId].explicitInput as { savedObjectId?: string }) - .savedObjectId; - if (savedObjectId) { - panels[panelId].panelRefName = `panel_${panelId}`; - - references.push({ - name: `${panelId}:panel_${panelId}`, - type: panels[panelId].type, - id: savedObjectId, - }); - } - } - return { panels, sections: { ...layout.sections }, references }; - }; - - const resetLayout = ({ - panels: lastSavedPanels, - sections: lastSavedSections, - }: DashboardState) => { - const { layout: lastSavedLayout, childState: lastSavedChildState } = deserializeLayout( - lastSavedPanels, - lastSavedSections - ); - - layout$.next(lastSavedLayout); - currentChildState = lastSavedChildState; + let lastSavedLayout = initialLayout; + let lastSavedChildState = initialChildState; + const resetLayout = () => { + layout$.next({ ...lastSavedLayout }); + currentChildState = { ...lastSavedChildState }; let childrenModified = false; const currentChildren = { ...children$.value }; for (const uuid of Object.keys(currentChildren)) { @@ -174,7 +121,7 @@ export function initializeLayoutManager( ...layout$.value, panels: { ...layout$.value.panels, - [uuid]: { gridData: { ...gridData, i: uuid }, type } as DashboardPanel, + [uuid]: { gridData: { ...gridData, i: uuid }, type }, }, }; } @@ -191,7 +138,7 @@ export function initializeLayoutManager( ...layout$.value, panels: { ...otherPanels, - [uuid]: { gridData: { ...newPanelPlacement, i: uuid }, type } as DashboardPanel, + [uuid]: { gridData: { ...newPanelPlacement, i: uuid }, type }, }, }; }; @@ -202,7 +149,7 @@ export function initializeLayoutManager( if (incomingEmbeddable) { const { serializedState, size, type } = incomingEmbeddable; const uuid = incomingEmbeddable.embeddableId ?? v4(); - const existingPanel: DashboardPanel | undefined = layout$.value.panels[uuid]; + const existingPanel: DashboardLayoutPanel | undefined = layout$.value.panels[uuid]; const sameType = existingPanel?.type === type; const gridData = existingPanel ? existingPanel.gridData : placeIncomingPanel(uuid, size); @@ -218,7 +165,7 @@ export function initializeLayoutManager( ...layout$.value, panels: { ...layout$.value.panels, - [uuid]: { gridData, type } as DashboardPanel, + [uuid]: { gridData, type }, }, }); trackPanel.setScrollToPanelId(uuid); @@ -370,36 +317,29 @@ export function initializeLayoutManager( return { internalApi: { - getSerializedStateForPanel: (uuid: string) => currentChildState[uuid], + getSerializedStateForPanel: (panelId: string) => currentChildState[panelId], + getLastSavedStateForPanel: (panelId: string) => lastSavedChildState[panelId], layout$, reset: resetLayout, - serializeLayout, + serializeLayout: () => serializeLayout(layout$.value, currentChildState), startComparing$: ( lastSavedState$: BehaviorSubject - ): Observable<{ panels?: DashboardPanelMap; sections?: DashboardSectionMap }> => { + ): Observable<{ panels?: DashboardState['panels'] }> => { return layout$.pipe( debounceTime(100), combineLatestWith( lastSavedState$.pipe( - map((lastSaved) => ({ panels: lastSaved.panels, sections: lastSaved.sections })) + map((lastSaved) => deserializeLayout(lastSaved.panels, getReferences)), + tap(({ layout, childState }) => { + lastSavedChildState = childState; + lastSavedLayout = layout; + }) ) ), - map(([, { panels: lastSavedPanels, sections: lastSavedSections }]) => { - const { panels, sections } = serializeLayout(); - if ( - !areLayoutsEqual( - { panels: lastSavedPanels, sections: lastSavedSections }, - { panels, sections } - ) - ) { - if (shouldLogStateDiff()) { - logStateDiff( - 'dashboard layout', - deserializeLayout(lastSavedPanels, lastSavedSections).layout, - deserializeLayout(panels, sections).layout - ); - } - return { panels, sections }; + map(([currentLayout]) => { + if (!areLayoutsEqual(lastSavedLayout, currentLayout)) { + logStateDiff('dashboard layout', lastSavedLayout, currentLayout); + return { panels: serializeLayout(currentLayout, currentChildState).panels }; } return {}; }) @@ -448,7 +388,6 @@ export function initializeLayoutManager( const sections = { ...currentLayout.sections }; const newId = v4(); sections[newId] = { - id: newId, gridData: { i: newId, y: maxY }, title: i18n.translate('dashboard.defaultSectionTitle', { defaultMessage: 'New collapsible section', diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/serialize_layout.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/serialize_layout.test.ts new file mode 100644 index 000000000000..49cfc066b987 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/serialize_layout.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { serializeLayout } from './serialize_layout'; + +describe('serializeLayout', () => { + test('should serialize panels', () => { + const layout = { + panels: { + '1': { + gridData: { + h: 6, + i: '1', + w: 6, + x: 0, + y: 0, + }, + type: 'testPanelType', + }, + '3': { + gridData: { + h: 6, + i: '3', + sectionId: 'section1', + w: 6, + x: 0, + y: 0, + }, + type: 'testPanelType', + }, + }, + sections: { + section1: { + collapsed: true, + gridData: { + i: 'section1', + y: 6, + }, + title: 'Section One', + }, + }, + }; + const childState = { + '1': { + rawState: { + title: 'panel One', + }, + references: [ + { + name: 'myRef', + id: 'ref1', + type: 'testRefType', + }, + ], + }, + '3': { + rawState: { + title: 'panel Three', + }, + references: [], + }, + }; + + const { panels, references } = serializeLayout(layout, childState); + expect(panels).toMatchInlineSnapshot(` + Array [ + Object { + "gridData": Object { + "h": 6, + "i": "1", + "w": 6, + "x": 0, + "y": 0, + }, + "panelConfig": Object { + "title": "panel One", + }, + "panelIndex": "1", + "type": "testPanelType", + }, + Object { + "collapsed": true, + "gridData": Object { + "i": "section1", + "y": 6, + }, + "panels": Array [ + Object { + "gridData": Object { + "h": 6, + "i": "3", + "w": 6, + "x": 0, + "y": 0, + }, + "panelConfig": Object { + "title": "panel Three", + }, + "panelIndex": "3", + "type": "testPanelType", + }, + ], + "title": "Section One", + }, + ] + `); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "ref1", + "name": "1:myRef", + "type": "testRefType", + }, + ] + `); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/serialize_layout.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/serialize_layout.ts new file mode 100644 index 000000000000..46d7a6fff53e --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/serialize_layout.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { type DashboardState, prefixReferencesFromPanel } from '../../../common'; +import type { DashboardChildState, DashboardLayout } from './types'; +import type { DashboardSection } from '../../../server'; + +export function serializeLayout( + layout: DashboardLayout, + childState: DashboardChildState +): Pick { + const sections: { [sectionId: string]: DashboardSection } = {}; + Object.entries(layout.sections).forEach(([sectionId, sectionState]) => { + sections[sectionId] = { ...sectionState, panels: [] }; + }); + + const references: DashboardState['references'] = []; + const panels: DashboardState['panels'] = []; + Object.entries(layout.panels).forEach(([panelId, { gridData, type }]) => { + const panelConfig = childState[panelId]?.rawState ?? {}; + references.push(...prefixReferencesFromPanel(panelId, childState[panelId]?.references ?? [])); + // TODO move savedObjectRef extraction into embeddable implemenations + const savedObjectId = (panelConfig as { savedObjectId?: string }).savedObjectId; + let panelRefName: string | undefined; + if (savedObjectId) { + panelRefName = `panel_${panelId}`; + + references.push({ + name: `${panelId}:panel_${panelId}`, + type, + id: savedObjectId, + }); + } + + const { sectionId, ...restOfGridData } = gridData; // drop section ID + const panelState = { + type, + gridData: restOfGridData, + panelIndex: panelId, + panelConfig, + ...(panelRefName !== undefined && { panelRefName }), + }; + + if (sectionId) { + sections[sectionId].panels.push(panelState); + } else { + panels.push(panelState); + } + }); + + return { + panels: [...panels, ...Object.values(sections)], + references, + }; +} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/types.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/types.ts new file mode 100644 index 000000000000..6fef16ca457b --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { SerializedPanelState } from '@kbn/presentation-publishing'; +import type { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; +import type { DashboardPanel, DashboardSection } from '../../../server'; + +export interface DashboardChildren { + [uuid: string]: DefaultEmbeddableApi; +} + +export interface DashboardLayoutPanel { + gridData: DashboardPanel['gridData'] & { sectionId?: string }; + type: DashboardPanel['type']; +} + +export interface DashboardLayout { + panels: { + [uuid: string]: DashboardLayoutPanel; + }; + sections: { [id: string]: Pick }; +} + +export interface DashboardChildState { + [uuid: string]: SerializedPanelState; +} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts index 78e579cb13b5..246bcc0792d5 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts @@ -45,38 +45,18 @@ import { PublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/p import { PublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session'; import { LocatorPublic } from '@kbn/share-plugin/common'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; -import { - DashboardLocatorParams, - DashboardPanelMap, - DashboardPanelState, - DashboardSectionMap, - DashboardSettings, - DashboardState, -} from '../../common'; +import { DashboardLocatorParams, DashboardSettings, DashboardState } from '../../common'; import type { DashboardAttributes, GridData } from '../../server/content_management'; import { LoadDashboardReturn, SaveDashboardReturn, } from '../services/dashboard_content_management_service/types'; +import { DashboardLayout } from './layout_manager/types'; export const DASHBOARD_API_TYPE = 'dashboard'; export const ReservedLayoutItemTypes: readonly string[] = ['section'] as const; -export type DashboardPanel = Pick & HasType; -export interface DashboardLayout { - panels: { [uuid: string]: DashboardPanel }; // partial of DashboardPanelState - sections: DashboardSectionMap; -} - -export interface DashboardChildState { - [uuid: string]: SerializedPanelState; -} - -export interface DashboardChildren { - [uuid: string]: DefaultEmbeddableApi; -} - export interface DashboardCreationOptions { getInitialInput?: () => Partial; @@ -177,10 +157,6 @@ export interface DashboardInternalApi { layout$: BehaviorSubject; registerChildApi: (api: DefaultEmbeddableApi) => void; setControlGroupApi: (controlGroupApi: ControlGroupApi) => void; - serializeLayout: () => { - references: Reference[]; - panels: DashboardPanelMap; - sections: DashboardSectionMap; - }; + serializeLayout: () => Pick; isSectionCollapsed: (sectionId?: string) => boolean; } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts index cb8705170639..3534f9218adf 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts @@ -137,7 +137,7 @@ export function initializeUnsavedChangesManager({ // dashboardStateToBackup.references will be used instead of savedObjectResult.references // To avoid missing references, make sure references contains all references // even if panels or control group does not have unsaved changes - dashboardStateToBackup.references = [...references, ...controlGroupReferences]; + dashboardStateToBackup.references = [...(references ?? []), ...controlGroupReferences]; if (hasPanelChanges) dashboardStateToBackup.panels = panels; if (hasControlGroupChanges) dashboardStateToBackup.controlGroupInput = controlGroupInput; } @@ -158,18 +158,14 @@ export function initializeUnsavedChangesManager({ : undefined; } - if (!lastSavedDashboardState.panels[childId]) return; - return { - rawState: lastSavedDashboardState.panels[childId].explicitInput, - references: getReferences(childId), - }; + return layoutManager.internalApi.getLastSavedStateForPanel(childId); }; return { api: { asyncResetToLastSavedState: async () => { const savedState = lastSavedState$.value; - layoutManager.internalApi.reset(savedState); + layoutManager.internalApi.reset(); unifiedSearchManager.internalApi.reset(savedState); settingsManager.internalApi.reset(savedState); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx index 80d5a88c66dd..c76f61bce875 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx @@ -8,7 +8,6 @@ */ import { Capabilities } from '@kbn/core/public'; -import { convertPanelSectionMapsToPanelsArray } from '../../../../common/lib/dashboard_panel_converters'; import { DashboardLocatorParams } from '../../../../common/types'; import { getDashboardBackupService } from '../../../services/dashboard_backup_service'; import { shareService } from '../../../services/kibana_services'; @@ -122,13 +121,9 @@ describe('ShowShareModal', () => { locatorParams: { params: DashboardLocatorParams }; } ).locatorParams.params; - const rawDashboardState = { - ...unsavedDashboardState, - panels: convertPanelSectionMapsToPanelsArray(unsavedDashboardState.panels, {}), - }; unsavedStateKeys.forEach((key) => { expect(shareLocatorParams[key]).toStrictEqual( - (rawDashboardState as unknown as Partial)[key] + (unsavedDashboardState as unknown as Partial)[key] ); }); }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx index ef24ba300fb9..ef1044af6824 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx @@ -21,7 +21,6 @@ import { getStateFromKbnUrl, setStateToKbnUrl, unhashUrl } from '@kbn/kibana-uti import { LocatorPublic } from '@kbn/share-plugin/common'; import { DashboardLocatorParams } from '../../../../common'; -import { convertPanelSectionMapsToPanelsArray } from '../../../../common/lib/dashboard_panel_converters'; import { getDashboardBackupService } from '../../../services/dashboard_backup_service'; import { dataService, shareService } from '../../../services/kibana_services'; import { getDashboardCapabilities } from '../../../utils/get_dashboard_capabilities'; @@ -113,13 +112,10 @@ export function ShowShareModal({ ); }; - const { - panels: allUnsavedPanelsMap, - sections: allUnsavedSectionsMap, - ...unsavedDashboardState - } = getDashboardBackupService().getState(savedObjectId) ?? {}; + const unsavedDashboardState = + getDashboardBackupService().getState(savedObjectId) ?? ({} as DashboardLocatorParams); - const hasPanelChanges = allUnsavedPanelsMap !== undefined; + const hasPanelChanges = unsavedDashboardState.panels !== undefined; const unsavedDashboardStateForLocator: DashboardLocatorParams = { ...unsavedDashboardState, @@ -127,12 +123,6 @@ export function ShowShareModal({ unsavedDashboardState.controlGroupInput as DashboardLocatorParams['controlGroupInput'], references: unsavedDashboardState.references as DashboardLocatorParams['references'], }; - if (allUnsavedPanelsMap || allUnsavedSectionsMap) { - unsavedDashboardStateForLocator.panels = convertPanelSectionMapsToPanelsArray( - allUnsavedPanelsMap ?? {}, - allUnsavedSectionsMap ?? {} - ); - } const locatorParams: DashboardLocatorParams = { dashboardId: savedObjectId, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_dashboard_state.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_dashboard_state.test.ts index 971787269761..90ac57c4d9c8 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_dashboard_state.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_dashboard_state.test.ts @@ -11,8 +11,6 @@ import { omit } from 'lodash'; import { DEFAULT_DASHBOARD_STATE } from '../../../dashboard_api/default_dashboard_state'; import { extractDashboardState } from './extract_dashboard_state'; -const DASHBOARD_STATE = omit(DEFAULT_DASHBOARD_STATE, ['panels', 'sections']); - describe('extractDashboardState', () => { test('should extract all DashboardState fields', () => { const optionalState = { @@ -28,11 +26,11 @@ describe('extractDashboardState', () => { }; expect( extractDashboardState({ - ...DASHBOARD_STATE, + ...DEFAULT_DASHBOARD_STATE, ...optionalState, }) ).toEqual({ - ...DASHBOARD_STATE, + ...omit(DEFAULT_DASHBOARD_STATE, 'panels'), ...optionalState, }); }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_panels_state.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_panels_state.test.ts index 1ea516e40626..80b3180f96ec 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_panels_state.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_panels_state.test.ts @@ -11,46 +11,8 @@ import { coreServices } from '../../../services/kibana_services'; import { extractPanelsState } from './extract_panels_state'; describe('extractPanelsState', () => { - describe('>= 8.18 panels state', () => { - test('should convert embeddableConfig to panelConfig', () => { - const dashboardState = extractPanelsState({ - panels: [ - { - panelConfig: { - timeRange: { - from: 'now-7d/d', - to: 'now', - }, - }, - gridData: {}, - id: 'de71f4f0-1902-11e9-919b-ffe5949a18d2', - panelIndex: 'c505cc42-fbde-451d-8720-302dc78d7e0d', - title: 'Custom title', - type: 'map', - }, - ], - }); - expect(dashboardState.panels).toEqual({ - ['c505cc42-fbde-451d-8720-302dc78d7e0d']: { - explicitInput: { - savedObjectId: 'de71f4f0-1902-11e9-919b-ffe5949a18d2', - timeRange: { - from: 'now-7d/d', - to: 'now', - }, - title: 'Custom title', - }, - gridData: {}, - type: 'map', - panelRefName: undefined, - version: undefined, - }, - }); - }); - }); - - describe('< 8.17 panels state', () => { - test('should convert embeddableConfig to panelConfig', () => { + describe('< 8.19 panels state', () => { + test('should move id and title to panelConfig', () => { const dashboardState = extractPanelsState({ panels: [ { @@ -68,9 +30,9 @@ describe('extractPanelsState', () => { }, ], }); - expect(dashboardState.panels).toEqual({ - ['c505cc42-fbde-451d-8720-302dc78d7e0d']: { - explicitInput: { + expect(dashboardState.panels).toEqual([ + { + panelConfig: { savedObjectId: 'de71f4f0-1902-11e9-919b-ffe5949a18d2', timeRange: { from: 'now-7d/d', @@ -78,12 +40,44 @@ describe('extractPanelsState', () => { }, title: 'Custom title', }, + panelIndex: 'c505cc42-fbde-451d-8720-302dc78d7e0d', gridData: {}, type: 'map', - panelRefName: undefined, - version: undefined, }, + ]); + }); + }); + + describe('< 8.17 panels state', () => { + test('should convert embeddableConfig to panelConfig', () => { + const dashboardState = extractPanelsState({ + panels: [ + { + embeddableConfig: { + timeRange: { + from: 'now-7d/d', + to: 'now', + }, + }, + gridData: {}, + panelIndex: 'c505cc42-fbde-451d-8720-302dc78d7e0d', + type: 'map', + }, + ], }); + expect(dashboardState.panels).toEqual([ + { + panelConfig: { + timeRange: { + from: 'now-7d/d', + to: 'now', + }, + }, + panelIndex: 'c505cc42-fbde-451d-8720-302dc78d7e0d', + gridData: {}, + type: 'map', + }, + ]); }); }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_panels_state.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_panels_state.ts index 7017a625aac3..d0ef80eb4d35 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_panels_state.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_panels_state.ts @@ -8,13 +8,10 @@ */ import semverSatisfies from 'semver/functions/satisfies'; -import { convertPanelsArrayToPanelSectionMaps } from '../../../../common/lib/dashboard_panel_converters'; import { DashboardState } from '../../../../common'; import { coreServices } from '../../../services/kibana_services'; import { getPanelTooOldErrorString } from '../../_dashboard_app_strings'; -type PanelState = Pick; - /** * We no longer support loading panels from a version older than 7.3 in the URL. * @returns whether or not there is a panel in the URL state saved with a version before 7.3 @@ -37,7 +34,9 @@ const isPanelVersionTooOld = (panels: unknown[]) => { return false; }; -export function extractPanelsState(state: { [key: string]: unknown }): Partial { +export function extractPanelsState(state: { [key: string]: unknown }): { + panels?: DashboardState['panels']; +} { const panels = Array.isArray(state.panels) ? state.panels : []; if (panels.length === 0) { @@ -49,17 +48,29 @@ export function extractPanelsState(state: { [key: string]: unknown }): Partial

{ - if (typeof panel === 'object' && panel?.embeddableConfig) { - const { embeddableConfig, ...rest } = panel; - return { - ...rest, - panelConfig: embeddableConfig, - }; + const standardizedPanels = panels.map((legacyPanel) => { + const panel = typeof legacyPanel === 'object' ? { ...legacyPanel } : {}; + + // < 8.17 panels state stored panelConfig as embeddableConfig + if (panel?.embeddableConfig) { + panel.panelConfig = panel.embeddableConfig; + delete panel.embeddableConfig; } + + // <8.19 'id' (saved object id) stored as siblings to panelConfig + if (panel.id && panel.panelConfig && typeof panel.panelConfig === 'object') { + panel.panelConfig.savedObjectId = panel.id; + delete panel.id; + } + + // <8.19 'title' stored as siblings to panelConfig + if (panel.title && panel.panelConfig && typeof panel.panelConfig === 'object') { + panel.panelConfig.title = panel.title; + delete panel.title; + } + return panel; }); - return convertPanelsArrayToPanelSectionMaps(standardizedPanels); + return { panels: standardizedPanels }; } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/search_sessions_integration.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/search_sessions_integration.ts index a55ed7dfa01c..63aa0ebfd439 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/search_sessions_integration.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/search_sessions_integration.ts @@ -19,7 +19,6 @@ import { import { History } from 'history'; import { map } from 'rxjs'; import { SEARCH_SESSION_ID } from '../../../common/constants'; -import { convertPanelSectionMapsToPanelsArray } from '../../../common/lib/dashboard_panel_converters'; import { DashboardLocatorParams } from '../../../common/types'; import { DashboardApi, DashboardInternalApi } from '../../dashboard_api/types'; import { dataService } from '../../services/kibana_services'; @@ -81,7 +80,12 @@ function getLocatorParams({ shouldRestoreSearchSession: boolean; }): DashboardLocatorParams { const savedObjectId = dashboardApi.savedObjectId$.value; - const { panels, sections, references } = dashboardInternalApi.serializeLayout(); + const panels = savedObjectId + ? (dashboardInternalApi.serializeLayout() as Pick< + DashboardLocatorParams, + 'panels' | 'references' + >) + : undefined; return { viewMode: dashboardApi.viewMode$.value ?? 'view', useHash: false, @@ -101,14 +105,6 @@ function getLocatorParams({ value: 0, } : undefined, - ...(savedObjectId - ? {} - : { - panels: convertPanelSectionMapsToPanelsArray( - panels, - sections - ) as DashboardLocatorParams['panels'], - references: references as DashboardLocatorParams['references'], - }), + ...panels, }; } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.test.tsx index c3eee737f3e7..654b41697c17 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.test.tsx @@ -14,16 +14,25 @@ import { useBatchedPublishingSubjects as mockUseBatchedPublishingSubjects } from import { RenderResult, act, getByLabelText, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { DashboardPanelMap, DashboardSectionMap } from '../../../common'; +import { DashboardState } from '../../../common'; import { DashboardContext, useDashboardApi as mockUseDashboardApi, } from '../../dashboard_api/use_dashboard_api'; import { DashboardInternalContext } from '../../dashboard_api/use_dashboard_internal_api'; -import { buildMockDashboardApi, getMockDashboardPanels } from '../../mocks'; +import { + buildMockDashboardApi, + getMockLayoutWithSections, + getMockPanels, + getMockPanelsWithSections, +} from '../../mocks'; import { DashboardGrid } from './dashboard_grid'; import type { Props as DashboardGridItemProps } from './dashboard_grid_item'; +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('54321'), +})); + jest.mock('./dashboard_grid_item', () => { return { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -68,17 +77,9 @@ const verifyElementHasClass = ( expect(itemToCheck!.classList.contains(className)).toBe(true); }; -const createAndMountDashboardGrid = async (overrides?: { - panels?: DashboardPanelMap; - sections?: DashboardSectionMap; -}) => { - const panels = overrides?.panels ?? getMockDashboardPanels().panels; - const sections = overrides?.sections; +const createAndMountDashboardGrid = async (overrides?: Partial) => { const { api, internalApi } = buildMockDashboardApi({ - overrides: { - panels, - ...(sections && { sections }), - }, + overrides, }); const component = render( @@ -91,6 +92,7 @@ const createAndMountDashboardGrid = async (overrides?: { ); // panels in collapsed sections should not render + const { panels, sections } = internalApi.layout$.value; const panelRenderCount = sections ? Object.values(panels).filter((value) => { const sectionId = value.gridData.sectionId; @@ -117,7 +119,9 @@ describe('DashboardGrid', () => { }); test('removes panel when removed from container', async () => { - const { dashboardApi, component } = await createAndMountDashboardGrid(); + const { dashboardApi, component } = await createAndMountDashboardGrid({ + panels: getMockPanels(), + }); // remove panel await act(async () => { @@ -129,7 +133,9 @@ describe('DashboardGrid', () => { }); test('renders expanded panel', async () => { - const { dashboardApi, component } = await createAndMountDashboardGrid(); + const { dashboardApi, component } = await createAndMountDashboardGrid({ + panels: getMockPanels(), + }); // maximize panel await act(async () => { @@ -154,7 +160,9 @@ describe('DashboardGrid', () => { }); test('renders focused panel', async () => { - const { dashboardApi, component } = await createAndMountDashboardGrid(); + const { dashboardApi, component } = await createAndMountDashboardGrid({ + panels: getMockPanels(), + }); const overlayMock = { onClose: new Promise((resolve) => { resolve(); @@ -185,10 +193,8 @@ describe('DashboardGrid', () => { describe('sections', () => { test('renders sections', async () => { - const { panels, sections } = getMockDashboardPanels(true); await createAndMountDashboardGrid({ - panels, - sections, + panels: getMockPanelsWithSections(), }); const header1 = screen.getByTestId('kbnGridSectionHeader-section1'); @@ -200,10 +206,8 @@ describe('DashboardGrid', () => { }); test('can add new section', async () => { - const { panels, sections } = getMockDashboardPanels(true); const { dashboardApi, internalApi } = await createAndMountDashboardGrid({ - panels, - sections, + panels: getMockPanelsWithSections(), }); dashboardApi.addNewSection(); await waitFor(() => { @@ -211,23 +215,24 @@ describe('DashboardGrid', () => { expect(headers.length).toEqual(3); }); - const newHeader = Object.values(internalApi.layout$.getValue().sections).filter( - ({ gridData: { y } }) => y === 8 - )[0]; - - expect(newHeader.title).toEqual('New collapsible section'); - expect(screen.getByText(newHeader.title)).toBeInTheDocument(); - expect(newHeader.collapsed).toBe(false); - expect(screen.getByTestId(`kbnGridSectionHeader-${newHeader.id}`).classList).not.toContain( + const newSection = internalApi.layout$.getValue().sections['54321']; + expect(newSection).toEqual({ + gridData: { + i: '54321', + y: 8, + }, + title: 'New collapsible section', + collapsed: false, + }); + expect(screen.getByText(newSection.title)).toBeInTheDocument(); + expect(screen.getByTestId(`kbnGridSectionHeader-54321`).classList).not.toContain( 'kbnGridSectionHeader--collapsed' ); }); test('dashboard state updates on collapse', async () => { - const { panels, sections } = getMockDashboardPanels(true); - const { internalApi } = await createAndMountDashboardGrid({ - panels, - sections, + const { internalApi } = await await createAndMountDashboardGrid({ + panels: getMockPanelsWithSections(), }); const headerButton = screen.getByTestId(`kbnGridSectionTitle-section2`); @@ -240,20 +245,16 @@ describe('DashboardGrid', () => { }); test('dashboard state updates on section deletion', async () => { - const { panels, sections } = getMockDashboardPanels(true, { - sections: { - emptySection: { - id: 'emptySection', + const { internalApi } = await createAndMountDashboardGrid({ + panels: [ + ...getMockPanelsWithSections(), + { title: 'Empty section', collapsed: false, gridData: { i: 'emptySection', y: 8 }, + panels: [], }, - }, - }); - - const { internalApi } = await createAndMountDashboardGrid({ - panels, - sections, + ], }); // can delete empty section @@ -285,30 +286,26 @@ describe('DashboardGrid', () => { expect(Object.keys(internalApi.layout$.getValue().panels)).not.toContain('3'); // this is the panel in section1 }); }); + }); - test('layout responds to dashboard state update', async () => { - const withoutSections = getMockDashboardPanels(); - const withSections = getMockDashboardPanels(true); + test('layout responds to dashboard state update', async () => { + const { internalApi } = await createAndMountDashboardGrid({ + panels: getMockPanels(), + }); - const { internalApi } = await createAndMountDashboardGrid({ - panels: withoutSections.panels, - sections: {}, - }); + let sectionContainers = screen.getAllByTestId(`kbnGridSectionWrapper-`, { + exact: false, + }); + expect(sectionContainers.length).toBe(1); // only the first top section is rendered - let sectionContainers = screen.getAllByTestId(`kbnGridSectionWrapper-`, { + internalApi.layout$.next(getMockLayoutWithSections()); + + await waitFor(() => { + sectionContainers = screen.getAllByTestId(`kbnGridSectionWrapper-`, { exact: false, }); - expect(sectionContainers.length).toBe(1); // only the first top section is rendered - - internalApi.layout$.next(withSections); - - await waitFor(() => { - sectionContainers = screen.getAllByTestId(`kbnGridSectionWrapper-`, { - exact: false, - }); - expect(sectionContainers.length).toBe(2); // section wrappers are not rendered for collapsed sections - expect(screen.getAllByTestId('dashboardGridItem').length).toBe(3); // one panel is in a collapsed section - }); + expect(sectionContainers.length).toBe(2); // section wrappers are not rendered for collapsed sections + expect(screen.getAllByTestId('dashboardGridItem').length).toBe(3); // one panel is in a collapsed section }); }); }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.tsx index c8474433c167..37787c548861 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.tsx @@ -17,8 +17,7 @@ import { default as React, useCallback, useEffect, useMemo, useRef, useState } f import { useMemoCss } from '@kbn/css-utils/public/use_memo_css'; import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../common/content_management/constants'; import { GridData } from '../../../common/content_management/v2/types'; -import { areLayoutsEqual } from '../../dashboard_api/are_layouts_equal'; -import { DashboardLayout } from '../../dashboard_api/types'; +import { areLayoutsEqual, type DashboardLayout } from '../../dashboard_api/layout_manager'; import { useDashboardApi } from '../../dashboard_api/use_dashboard_api'; import { useDashboardInternalApi } from '../../dashboard_api/use_dashboard_internal_api'; import { @@ -111,7 +110,6 @@ export const DashboardGrid = ({ updatedLayout.sections[widget.id] = { collapsed: widget.isCollapsed, title: widget.title, - id: widget.id, gridData: { i: widget.id, y: widget.row, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.test.tsx index d3d6fe83a84f..ecf90c102759 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.test.tsx @@ -35,18 +35,20 @@ jest.mock('@kbn/embeddable-plugin/public', () => { const TEST_EMBEDDABLE = 'TEST_EMBEDDABLE'; const createAndMountDashboardGridItem = (props: DashboardGridItemProps) => { - const panels = { - '1': { + const panels = [ + { gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' }, type: TEST_EMBEDDABLE, - explicitInput: { id: '1' }, + panelConfig: {}, + panelIndex: '1', }, - '2': { + { gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' }, type: TEST_EMBEDDABLE, - explicitInput: { id: '2' }, + panelConfig: {}, + panelIndex: '2', }, - }; + ]; const { api, internalApi } = buildMockDashboardApi({ overrides: { panels } }); const component = render( diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.tsx index 28d34ab4978c..a151982fb033 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.tsx @@ -14,7 +14,6 @@ import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import classNames from 'classnames'; import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useMemoCss } from '@kbn/css-utils/public/use_memo_css'; -import { DashboardPanelState } from '../../../common'; import { useDashboardApi } from '../../dashboard_api/use_dashboard_api'; import { useDashboardInternalApi } from '../../dashboard_api/use_dashboard_internal_api'; import { presentationUtilService } from '../../services/kibana_services'; @@ -29,7 +28,7 @@ export interface Props extends DivProps { dashboardContainerRef?: React.MutableRefObject; id: string; index?: number; - type: DashboardPanelState['type']; + type: string; key: string; isRenderable?: boolean; setDragHandles?: (refs: Array) => void; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.test.tsx index 7d917b765961..239007827409 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.test.tsx @@ -14,11 +14,11 @@ import { render, waitFor } from '@testing-library/react'; import { DashboardContext } from '../../dashboard_api/use_dashboard_api'; import { DashboardInternalContext } from '../../dashboard_api/use_dashboard_internal_api'; -import { buildMockDashboardApi, getMockDashboardPanels } from '../../mocks'; +import { buildMockDashboardApi, getMockPanels } from '../../mocks'; import { DashboardViewport } from './dashboard_viewport'; const createAndMountDashboardViewport = async () => { - const panels = getMockDashboardPanels().panels; + const panels = getMockPanels(); const { api, internalApi } = buildMockDashboardApi({ overrides: { panels, diff --git a/src/platform/plugins/shared/dashboard/public/mocks.tsx b/src/platform/plugins/shared/dashboard/public/mocks.tsx index fd22f7c79a76..c30a0f544699 100644 --- a/src/platform/plugins/shared/dashboard/public/mocks.tsx +++ b/src/platform/plugins/shared/dashboard/public/mocks.tsx @@ -12,7 +12,7 @@ import { BehaviorSubject } from 'rxjs'; import { DashboardStart } from './plugin'; import { DashboardState } from '../common/types'; import { getDashboardApi } from './dashboard_api/get_dashboard_api'; -import { DashboardPanelMap, DashboardSectionMap } from '../common'; +import { deserializeLayout } from './dashboard_api/layout_manager/deserialize_layout'; export type Start = jest.Mocked; @@ -126,68 +126,70 @@ export function getSampleDashboardState(overrides?: Partial): Da }, timeRestore: false, viewMode: 'view', - panels: {}, - sections: {}, + panels: [], ...overrides, }; } -export function getMockDashboardPanels( - withSections: boolean = false, - overrides?: { - panels?: DashboardPanelMap; - sections?: DashboardSectionMap; - } -): { panels: DashboardPanelMap; sections: DashboardSectionMap } { - const panels = { - '1': { +export function getMockPanels() { + return [ + { gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' }, - type: 'lens', - explicitInput: { id: '1' }, + panelConfig: { title: 'panel One' }, + panelIndex: '1', + type: 'testPanelType', }, - '2': { + { gridData: { x: 6, y: 0, w: 6, h: 6, i: '2' }, - type: 'lens', - explicitInput: { id: '2' }, + panelConfig: { title: 'panel Two' }, + panelIndex: '2', + type: 'testPanelType', }, - ...overrides?.panels, - }; - if (!withSections) return { panels, sections: {} }; - - return { - panels: { - ...panels, - '3': { - gridData: { x: 0, y: 0, w: 6, h: 6, i: '3', sectionId: 'section1' }, - type: 'lens', - explicitInput: { id: '3' }, - }, - '4': { - gridData: { x: 0, y: 0, w: 6, h: 6, i: '4', sectionId: 'section2' }, - type: 'lens', - explicitInput: { id: '4' }, - }, - }, - sections: { - section1: { - id: 'section1', - title: 'Section One', - collapsed: true, - gridData: { - y: 6, - i: 'section1', - }, - }, - section2: { - id: 'section2', - title: 'Section Two', - collapsed: false, - gridData: { - y: 7, - i: 'section2', - }, - }, - ...overrides?.sections, - }, - } as any; + ]; +} + +export function getMockPanelsWithSections() { + return [ + ...getMockPanels(), + { + title: 'Section One', + collapsed: true, + gridData: { + y: 6, + i: 'section1', + }, + panels: [ + { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '3' }, + panelConfig: { title: 'panel Three' }, + panelIndex: '3', + type: 'testPanelType', + }, + ], + }, + { + title: 'Section Two', + collapsed: false, + gridData: { + y: 7, + i: 'section2', + }, + panels: [ + { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '4' }, + panelConfig: { title: 'panel Four' }, + panelIndex: '4', + type: 'testPanelType', + }, + ], + }, + ]; +} + +export function getMockLayout() { + return deserializeLayout(getMockPanels(), () => []).layout; +} + +export function getMockLayoutWithSections() { + return deserializeLayout(getMockPanelsWithSections(), () => []).layout; } diff --git a/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.test.ts b/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.test.ts index 3a2c8fc8ee10..ca71b2dbf45c 100644 --- a/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.test.ts +++ b/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getMockDashboardPanels } from '../mocks'; +import { getMockLayout, getMockLayoutWithSections } from '../mocks'; import { placeClonePanel } from './place_clone_panel_strategy'; describe('Clone panel placement strategies', () => { @@ -16,7 +16,7 @@ describe('Clone panel placement strategies', () => { '1': { gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' }, type: 'lens', - explicitInput: { id: '1' }, + panelConfig: {}, }, }; const { newPanelPlacement, otherPanels } = placeClonePanel({ @@ -35,7 +35,7 @@ describe('Clone panel placement strategies', () => { }); it('panel collision at desired clone location', () => { - const { panels } = getMockDashboardPanels(); + const panels = getMockLayout().panels; const { newPanelPlacement, otherPanels } = placeClonePanel({ width: 6, height: 6, @@ -52,8 +52,7 @@ describe('Clone panel placement strategies', () => { }); it('ignores panels in other sections', () => { - const { panels } = getMockDashboardPanels(true); - + const panels = getMockLayoutWithSections().panels; const { newPanelPlacement, otherPanels } = placeClonePanel({ width: 6, height: 6, diff --git a/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.ts b/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.ts index 5bd33508d610..ffc02cbb9114 100644 --- a/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.ts +++ b/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.ts @@ -13,7 +13,7 @@ import { cloneDeep, forOwn } from 'lodash'; import { DASHBOARD_GRID_COLUMN_COUNT } from '../../common/content_management'; import type { GridData } from '../../server/content_management'; import { PanelPlacementProps, PanelPlacementReturn } from './types'; -import { DashboardPanel } from '../dashboard_api/types'; +import { DashboardLayoutPanel } from '../dashboard_api/layout_manager'; interface IplacementDirection { grid: Omit; @@ -53,7 +53,7 @@ export function placeClonePanel({ } const beside = panelToPlaceBeside.gridData; const otherPanelGridData: GridData[] = []; - forOwn(currentPanels, (panel: DashboardPanel) => { + forOwn(currentPanels, (panel: DashboardLayoutPanel) => { if (panel.gridData.sectionId === sectionId) { // only check against panels that are in the same section as the cloned panel otherPanelGridData.push(panel.gridData); diff --git a/src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.test.ts b/src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.test.ts index eff226705f9d..b6dccecbca88 100644 --- a/src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.test.ts +++ b/src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getMockDashboardPanels } from '../mocks'; +import { getMockLayout, getMockLayoutWithSections } from '../mocks'; import { PanelPlacementStrategy } from '../plugin_constants'; import { runPanelPlacementStrategy } from './place_new_panel_strategies'; @@ -28,7 +28,7 @@ describe('new panel placement strategies', () => { }); it('push other panels down', () => { - const { panels } = getMockDashboardPanels(); + const panels = getMockLayout().panels; const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy( PanelPlacementStrategy.placeAtTop, { width: 6, height: 6, currentPanels: panels } @@ -57,7 +57,7 @@ describe('new panel placement strategies', () => { }); it('ignores panels in other sections', () => { - const { panels } = getMockDashboardPanels(true); + const panels = getMockLayoutWithSections().panels; const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy( PanelPlacementStrategy.placeAtTop, { width: 6, height: 6, currentPanels: panels, sectionId: 'section1' } @@ -105,15 +105,13 @@ describe('new panel placement strategies', () => { }); it('top left most space is available', () => { - const { panels } = getMockDashboardPanels(false, { - panels: { - '1': { - gridData: { x: 6, y: 0, w: 6, h: 6, i: '1' }, - type: 'lens', - explicitInput: { id: '1' }, - }, + const panels = { + ...getMockLayout().panels, + '1': { + gridData: { x: 6, y: 0, w: 6, h: 6, i: '1' }, + type: 'lens', }, - }); + }; const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy( PanelPlacementStrategy.findTopLeftMostOpenSpace, @@ -129,15 +127,13 @@ describe('new panel placement strategies', () => { }); it('panel must be pushed down', () => { - const { panels } = getMockDashboardPanels(true, { - panels: { - '5': { - gridData: { x: 6, y: 0, w: 42, h: 6, i: '5' }, - type: 'lens', - explicitInput: { id: '1' }, - }, + const panels = { + ...getMockLayoutWithSections().panels, + '5': { + gridData: { x: 6, y: 0, w: 42, h: 6, i: '5' }, + type: 'lens', }, - }); + }; const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy( PanelPlacementStrategy.findTopLeftMostOpenSpace, { width: 6, height: 6, currentPanels: panels } @@ -152,30 +148,25 @@ describe('new panel placement strategies', () => { }); it('ignores panels in other sections', () => { - const { panels } = getMockDashboardPanels(true, { - panels: { - '1': { - gridData: { x: 0, y: 0, w: 6, h: 100, i: '1' }, - type: 'lens', - explicitInput: { id: '1' }, - }, - '2': { - gridData: { x: 6, y: 6, w: 42, h: 100, i: '2' }, - type: 'lens', - explicitInput: { id: '2' }, - }, - '6': { - gridData: { x: 0, y: 6, w: 6, h: 6, i: '6', sectionId: 'section1' }, - type: 'lens', - explicitInput: { id: '1' }, - }, - '7': { - gridData: { x: 6, y: 0, w: 42, h: 12, i: '7', sectionId: 'section1' }, - type: 'lens', - explicitInput: { id: '1' }, - }, + const panels = { + ...getMockLayoutWithSections().panels, + '1': { + gridData: { x: 0, y: 0, w: 6, h: 100, i: '1' }, + type: 'lens', }, - }); + '2': { + gridData: { x: 6, y: 6, w: 42, h: 100, i: '2' }, + type: 'lens', + }, + '6': { + gridData: { x: 0, y: 6, w: 6, h: 6, i: '6', sectionId: 'section1' }, + type: 'lens', + }, + '7': { + gridData: { x: 6, y: 0, w: 42, h: 12, i: '7', sectionId: 'section1' }, + type: 'lens', + }, + }; const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy( PanelPlacementStrategy.findTopLeftMostOpenSpace, { width: 6, height: 6, currentPanels: panels, sectionId: 'section1' } diff --git a/src/platform/plugins/shared/dashboard/public/panel_placement/types.ts b/src/platform/plugins/shared/dashboard/public/panel_placement/types.ts index 92b60ae7d197..aeba558c7e5a 100644 --- a/src/platform/plugins/shared/dashboard/public/panel_placement/types.ts +++ b/src/platform/plugins/shared/dashboard/public/panel_placement/types.ts @@ -11,7 +11,7 @@ import { MaybePromise } from '@kbn/utility-types'; import { SerializedPanelState } from '@kbn/presentation-publishing'; import type { GridData } from '../../server/content_management'; import { PanelPlacementStrategy } from '../plugin_constants'; -import { DashboardLayout } from '../dashboard_api/types'; +import { DashboardLayout } from '../dashboard_api/layout_manager'; export interface PanelPlacementSettings { strategy?: PanelPlacementStrategy; diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts index 9907aef4646d..0c6944c94f25 100644 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts +++ b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts @@ -13,7 +13,6 @@ import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public'; import { has } from 'lodash'; import { getDashboardContentManagementCache } from '..'; -import { convertPanelsArrayToPanelSectionMaps } from '../../../../common/lib/dashboard_panel_converters'; import type { DashboardGetIn, DashboardGetOut } from '../../../../server/content_management'; import { DEFAULT_DASHBOARD_STATE } from '../../../dashboard_api/default_dashboard_state'; import { cleanFiltersForSerialize } from '../../../utils/clean_filters_for_serialize'; @@ -156,10 +155,6 @@ export const loadDashboardState = async ({ } : undefined; - const { panels: panelMap, sections: sectionsMap } = convertPanelsArrayToPanelSectionMaps( - panels ?? [] - ); - return { managed, references, @@ -172,10 +167,9 @@ export const loadDashboardState = async ({ description, timeRange, filters, - panels: panelMap, + panels, query, title, - sections: sectionsMap, viewMode: 'view', // dashboards loaded from saved object default to view mode. If it was edited recently, the view mode from session storage will override this. tags: diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.test.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.test.ts index cd35294f8fb1..2fc9533c045a 100644 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.test.ts +++ b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.test.ts @@ -10,7 +10,7 @@ import { getSampleDashboardState } from '../../../mocks'; import { contentManagementService, coreServices, dataService } from '../../kibana_services'; import { saveDashboardState } from './save_dashboard_state'; -import { DashboardPanelMap } from '../../../../common/dashboard_container/types'; +import { DashboardPanel } from '../../../../server'; contentManagementService.client.create = jest.fn().mockImplementation(({ options }) => { if (options.id === undefined) { @@ -86,7 +86,7 @@ describe('Save dashboard state', () => { dashboardState: { ...getSampleDashboardState(), title: 'BooThree', - panels: { aVerySpecialVeryUniqueId: { type: 'boop' } } as unknown as DashboardPanelMap, + panels: [{ type: 'boop', panelIndex: 'idOne' } as DashboardPanel], }, lastSavedId: 'Boogatoonie', saveOptions: { saveAsCopy: true }, @@ -112,7 +112,7 @@ describe('Save dashboard state', () => { dashboardState: { ...getSampleDashboardState(), title: 'BooFour', - panels: { idOne: { type: 'boop' } } as unknown as DashboardPanelMap, + panels: [{ type: 'boop', panelIndex: 'idOne' } as DashboardPanel], }, panelReferences: [{ name: 'idOne:panel_idOne', type: 'boop', id: 'idOne' }], lastSavedId: 'Boogatoonie', @@ -140,7 +140,7 @@ describe('Save dashboard state', () => { dashboardState: { ...getSampleDashboardState(), title: 'BooThree', - panels: { idOne: { type: 'boop' } } as unknown as DashboardPanelMap, + panels: [{ type: 'boop', panelIndex: 'idOne' } as DashboardPanel], }, lastSavedId: 'Boogatoonie', saveOptions: { saveAsCopy: true }, diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.ts index c7ef2e661c14..7438d5ff3ee5 100644 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.ts +++ b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.ts @@ -33,7 +33,7 @@ export const saveDashboardState = async ({ const { attributes, references } = getSerializedState({ controlGroupReferences, - generateNewIds: saveOptions.saveAsCopy, + generateNewIds: saveOptions.saveAsCopy, // When saving a dashboard as a copy, we should generate new IDs for all panels dashboardState, panelReferences, searchSourceReferences, diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts index 425256357e7d..59a3a4263416 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts @@ -266,9 +266,6 @@ export const panelSchema = schema.object({ unknowns: 'allow', } ), - id: schema.maybe( - schema.string({ meta: { description: 'The saved object id for by reference panels' } }) - ), type: schema.string({ meta: { description: 'The embeddable type' } }), panelRefName: schema.maybe(schema.string()), gridData: panelGridDataSchema, @@ -277,7 +274,6 @@ export const panelSchema = schema.object({ meta: { description: 'The unique ID of the panel.' }, }) ), - title: schema.maybe(schema.string({ meta: { description: 'The title of the panel' } })), version: schema.maybe( schema.string({ meta: { diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.test.ts index f20935cbbe10..21fcfa669d19 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.test.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.test.ts @@ -92,12 +92,14 @@ describe('dashboardAttributesOut', () => { }, panels: [ { - panelConfig: { enhancements: {} }, + panelConfig: { + enhancements: {}, + savedObjectId: '1', + title: 'title1', + }, gridData: { x: 0, y: 0, w: 10, h: 10, i: '1' }, - id: '1', panelIndex: '1', panelRefName: 'ref1', - title: 'title1', type: 'type1', version: '2', }, @@ -179,6 +181,8 @@ describe('dashboardAttributesOut', () => { { panelConfig: { enhancements: {}, + savedObjectId: '1', + title: 'title1', }, gridData: { x: 0, @@ -187,10 +191,8 @@ describe('dashboardAttributesOut', () => { h: 10, i: '1', }, - id: '1', panelIndex: '1', panelRefName: 'ref1', - title: 'title1', type: 'type1', version: '2', }, @@ -243,8 +245,10 @@ describe('itemAttrsToSavedObject', () => { panels: [ { gridData: { x: 0, y: 0, w: 10, h: 10, i: '1' }, - id: '1', - panelConfig: { enhancements: {} }, + panelConfig: { + enhancements: {}, + savedObjectId: '1', + }, panelIndex: '1', panelRefName: 'ref1', title: 'title1', @@ -276,7 +280,7 @@ describe('itemAttrsToSavedObject', () => { "searchSourceJSON": "{\\"query\\":{\\"query\\":\\"test\\",\\"language\\":\\"KQL\\"}}", }, "optionsJSON": "{\\"hidePanelTitles\\":true,\\"useMargins\\":false,\\"syncColors\\":false,\\"syncTooltips\\":false,\\"syncCursor\\":false}", - "panelsJSON": "[{\\"id\\":\\"1\\",\\"panelRefName\\":\\"ref1\\",\\"title\\":\\"title1\\",\\"type\\":\\"type1\\",\\"version\\":\\"2\\",\\"embeddableConfig\\":{\\"enhancements\\":{}},\\"panelIndex\\":\\"1\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":10,\\"h\\":10,\\"i\\":\\"1\\"}}]", + "panelsJSON": "[{\\"panelRefName\\":\\"ref1\\",\\"title\\":\\"title1\\",\\"type\\":\\"type1\\",\\"version\\":\\"2\\",\\"embeddableConfig\\":{\\"enhancements\\":{},\\"savedObjectId\\":\\"1\\"},\\"panelIndex\\":\\"1\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":10,\\"h\\":10,\\"i\\":\\"1\\"}}]", "refreshInterval": Object { "pause": true, "value": 1000, @@ -384,12 +388,14 @@ describe('savedObjectToItem', () => { timeRestore: true, panels: [ { - panelConfig: { enhancements: {} }, + panelConfig: { + enhancements: {}, + savedObjectId: '1', + title: 'title1', + }, gridData: { x: 0, y: 0, w: 10, h: 10, i: '1' }, - id: '1', panelIndex: '1', panelRefName: 'ref1', - title: 'title1', type: 'type1', version: '2', }, diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts index 042f06d75556..326c93c02df6 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts @@ -9,7 +9,7 @@ import { v4 as uuidv4 } from 'uuid'; -import { isDashboardSection } from '../../../../../common/lib/dashboard_panel_converters'; +import { isDashboardSection } from '../../../../../common'; import { DashboardSavedObjectAttributes, SavedDashboardPanel, diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts index 9902f040f74b..0fb389e9dfbb 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts @@ -10,7 +10,7 @@ import { SavedObjectReference } from '@kbn/core/server'; import { SavedDashboardPanel, SavedDashboardSection } from '../../../../dashboard_saved_object'; import { DashboardAttributes, DashboardPanel, DashboardSection } from '../../types'; -import { getReferencesForPanelId } from '../../../../../common/dashboard_container/persistable_state/dashboard_container_references'; +import { getReferencesForPanelId } from '../../../../../common'; export function transformPanelsOut( panelsJSON: string = '{}', @@ -60,13 +60,19 @@ function transformPanelProperties( ? references.find((reference) => reference.name === panelRefName) : undefined; + const storedSavedObjectId = id ?? embeddableConfig.savedObjectId; + const savedObjectId = matchingReference ? matchingReference.id : storedSavedObjectId; + return { gridData: rest, - id: matchingReference ? matchingReference.id : id, - panelConfig: embeddableConfig, + panelConfig: { + ...embeddableConfig, + // <8.19 savedObjectId and title stored as siblings to embeddableConfig + ...(savedObjectId !== undefined && { savedObjectId }), + ...(title !== undefined && { title }), + }, panelIndex, panelRefName, - title, type: matchingReference ? matchingReference.type : type, version, }; diff --git a/src/platform/plugins/shared/dashboard/server/dashboard_container/dashboard_container_embeddable_factory.ts b/src/platform/plugins/shared/dashboard/server/dashboard_container/dashboard_container_embeddable_factory.ts index f5eaa9fc4287..b6ae120e9ba6 100644 --- a/src/platform/plugins/shared/dashboard/server/dashboard_container/dashboard_container_embeddable_factory.ts +++ b/src/platform/plugins/shared/dashboard/server/dashboard_container/dashboard_container_embeddable_factory.ts @@ -12,7 +12,7 @@ import { EmbeddableRegistryDefinition } from '@kbn/embeddable-plugin/server'; import { createExtract, createInject, -} from '../../common/dashboard_container/persistable_state/dashboard_container_references'; +} from '../dashboard_saved_object/migrations/migrate_extract_panel_references/dashboard_container_references'; export const dashboardPersistableStateServiceFactory = ( persistableStateService: EmbeddablePersistableStateService diff --git a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/migrations/dashboard_panel_converters.ts b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/migrations/dashboard_panel_converters.ts index 6e2e9cecc1d4..24a2ff157f2d 100644 --- a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/migrations/dashboard_panel_converters.ts +++ b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/migrations/dashboard_panel_converters.ts @@ -7,15 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { v4 } from 'uuid'; import { omit } from 'lodash'; -import type { Reference } from '@kbn/content-management-utils'; import { SavedDashboardPanel } from '../schema/v2'; -import { - getReferencesForPanelId, - prefixReferencesFromPanel, -} from './migrate_extract_panel_references/dashboard_container_references'; import { DashboardPanelMap810, DashboardPanelState810 } from './types'; export function convertSavedDashboardPanelToPanelState( @@ -86,24 +80,3 @@ export const convertPanelMapToSavedPanels = ( convertPanelStateToSavedDashboardPanel(panel, removeLegacyVersion) ); }; - -/** - * When saving a dashboard as a copy, we should generate new IDs for all panels so that they are - * properly refreshed when navigating between Dashboards - */ -export const generateNewPanelIds = (panels: DashboardPanelState810, references?: Reference[]) => { - const newPanelsMap: DashboardPanelMap810 = {}; - const newReferences: Reference[] = []; - for (const [oldId, panel] of Object.entries(panels)) { - const newId = v4(); - newPanelsMap[newId] = { - ...panel, - gridData: { ...panel.gridData, i: newId }, - explicitInput: { ...panel.explicitInput, id: newId }, - }; - newReferences.push( - ...prefixReferencesFromPanel(newId, getReferencesForPanelId(oldId, references ?? [])) - ); - } - return { panels: newPanelsMap, references: newReferences }; -}; diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts index ba10ebbf5669..50eba1ede01b 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts @@ -6,7 +6,7 @@ */ import type { DataView } from '@kbn/data-views-plugin/common'; -import type { DashboardPanelMap } from '@kbn/dashboard-plugin/common'; +import type { DashboardState } from '@kbn/dashboard-plugin/common'; import { existingDashboardFileNames, loadDashboardFile } from './dashboards/dashboard_catalog'; import { getDashboardFileName } from './dashboards/get_dashboard_file_name'; interface DashboardFileProps { @@ -47,7 +47,7 @@ const getAdhocDataView = (dataView: DataView) => { export async function convertSavedDashboardToPanels( props: MetricsDashboardProps, dataView: DataView -): Promise { +): Promise { const dashboardFilename = getDashboardFileNameFromProps(props); const dashboardJSON = !!dashboardFilename ? await loadDashboardFile(dashboardFilename) : false; @@ -63,10 +63,11 @@ export async function convertSavedDashboardToPanels( const datasourceStates = attributes?.state?.datasourceStates ?? {}; const layers = datasourceStates.formBased?.layers ?? datasourceStates.textBased?.layers ?? []; - acc[gridData.i] = { + acc.push({ type: panel.type, gridData, - explicitInput: { + panelIndex, + panelConfig: { id: panelIndex, ...embeddableConfig, title, @@ -84,10 +85,10 @@ export async function convertSavedDashboardToPanels( }, }, }, - }; + }); return acc; - }, {}) as DashboardPanelMap; + }, []); return panels; } diff --git a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts index 5a5c9dc2ab33..074b2fc3dfab 100644 --- a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts +++ b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts @@ -163,7 +163,7 @@ export class RelatedDashboardsClient { panelIndex: p.panelIndex || uuidv4(), type: p.type, panelConfig: p.panelConfig, - title: p.title, + title: (p.panelConfig as { title?: string }).title, }, matchedBy: { index: [index] }, })), @@ -213,7 +213,7 @@ export class RelatedDashboardsClient { panelIndex: p.panel.panelIndex || uuidv4(), type: p.panel.type, panelConfig: p.panel.panelConfig, - title: p.panel.title, + title: (p.panel.panelConfig as { title?: string }).title, }, matchedBy: { fields: Array.from(p.matchingFields) }, })), diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/incident_management/suggested_dashboards.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/incident_management/suggested_dashboards.ts index a6c2d386b2a1..01f3b805cfd3 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/incident_management/suggested_dashboards.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/incident_management/suggested_dashboards.ts @@ -460,6 +460,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { panelIndex: '8db7a201-95c8-4211-89b5-1288e18c8f2e', type: 'lens', panelConfig: { + title: '', enhancements: { dynamicActions: { events: [] } }, syncColors: false, syncCursor: true,