From 3d6954e25284014527209cb8ba50db255cbfe41a Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 24 Jun 2025 08:48:08 -0600 Subject: [PATCH] [Dashboards as code] remove client transform of panels array to map (#224314) Closes https://github.com/elastic/kibana/issues/224294 ### External team reviewers @elastic/kibana-presentation team is working on "Dashboards as code" project where we provide a human readable CRUD API for dashboards. Part of this work is aligning dashboard client code with the shape of dashboard server api. As such, we are changing the shape of `panels` from a Map to an Array - to directly consume what is being returned from the dashboard server api. ### PR Overview The goal of this PR is to update dashboard client-side state `panels` type to match the type from dashboard server api. The dashboard server api returns panels as an Array, while the dashboard client-side logic is expecting panels to be a Map keyed by panel id. This type change required the following changes: * Refactored dashboard client code to receive panels as an array and return panels as an array. Biggest work is in layout_manager `deserializeState` and `serializeState` methods. * Remove `convertPanelsArrayToPanelSectionMaps` from `loadDashboardState`. `convertPanelsArrayToPanelSectionMaps` performed 2 tasks 1) Convert panels array to map. This is no longer needed as now dashboard client code accepts panels in its native shape from the dashboard server api. 2) Move `id` and `title` fields into embeddable state. This is no longer needed as now dashboard server api does this transform before sending the dashboard to the client. * Remove `convertPanelSectionMapsToPanelsArray` from `getSerializedState`. `convertPanelSectionMapsToPanelsArray` performed 2 tasks. 1) Convert panels map into panels array. This is no longer needed as now panels is provided to `getSerializedState` in the shape required for the dashboard server api. 2) Lift `id` and `title` fields from into top level panel state. This is no longer needed as all embeddable state should remain under `panelConfig`. * Remove a bunch of code in `dashboard/common` as now the client and server are do not need to depend on shared logic as the client is much simpler and no longer needs to transform the server response. Much of this shared logic was copied into server saved object migrations in https://github.com/elastic/kibana/pull/223980 but can now be removed from common since its no longer used in the client. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../public/static_by_value_example.tsx | 6 +- .../static_by_value_example_panels.json | 20 +- .../common/content_management/v2/types.ts | 8 +- .../dashboard_container_references.test.ts | 162 ------- .../dashboard_container_references.ts | 147 ------ .../common/dashboard_container/types.ts | 45 -- .../dashboard_saved_object_references.test.ts | 454 ------------------ .../dashboard_saved_object_references.ts | 93 ---- .../plugins/shared/dashboard/common/index.ts | 14 +- .../dashboard/common/is_dashboard_section.ts | 16 + .../common/lib/dashboard_panel_converters.ts | 146 ------ .../dashboard/common/reference_utils.ts | 34 ++ .../plugins/shared/dashboard/common/types.ts | 27 +- .../dashboard_api/default_dashboard_state.ts | 3 +- .../generate_new_panel_ids.test.ts | 113 +++++ .../dashboard_api/generate_new_panel_ids.ts | 55 +++ .../public/dashboard_api/get_dashboard_api.ts | 15 +- .../get_serialized_state.test.ts | 70 +-- .../dashboard_api/get_serialized_state.ts | 14 +- .../{ => layout_manager}/are_layouts_equal.ts | 0 .../layout_manager/deserialize_layout.test.ts | 95 ++++ .../layout_manager/deserialize_layout.ts | 65 +++ .../dashboard_api/layout_manager/index.ts | 12 + .../layout_manager.test.ts | 97 +--- .../{ => layout_manager}/layout_manager.ts | 157 ++---- .../layout_manager/serialize_layout.test.ts | 123 +++++ .../layout_manager/serialize_layout.ts | 61 +++ .../dashboard_api/layout_manager/types.ts | 32 ++ .../dashboard/public/dashboard_api/types.ts | 30 +- .../dashboard_api/unsaved_changes_manager.ts | 10 +- .../top_nav/share/show_share_modal.test.tsx | 7 +- .../top_nav/share/show_share_modal.tsx | 16 +- .../url/bwc/extract_dashboard_state.test.ts | 6 +- .../url/bwc/extract_panels_state.test.ts | 84 ++-- .../url/bwc/extract_panels_state.ts | 37 +- .../url/search_sessions_integration.ts | 18 +- .../grid/dashboard_grid.test.tsx | 121 +++-- .../grid/dashboard_grid.tsx | 4 +- .../grid/dashboard_grid_item.test.tsx | 14 +- .../grid/dashboard_grid_item.tsx | 3 +- .../viewport/dashboard_viewport.test.tsx | 4 +- .../plugins/shared/dashboard/public/mocks.tsx | 116 ++--- .../place_clone_panel_strategy.test.ts | 9 +- .../place_clone_panel_strategy.ts | 4 +- .../place_new_panel_strategies.test.ts | 75 ++- .../dashboard/public/panel_placement/types.ts | 2 +- .../lib/load_dashboard_state.ts | 8 +- .../lib/save_dashboard_state.test.ts | 8 +- .../lib/save_dashboard_state.ts | 2 +- .../content_management/v3/cm_services.ts | 4 - .../v3/transform_utils.test.ts | 28 +- .../v3/transforms/in/panels_in_transforms.ts | 2 +- .../transforms/out/panels_out_transforms.ts | 14 +- .../dashboard_container_embeddable_factory.ts | 2 +- .../migrations/dashboard_panel_converters.ts | 27 -- .../app/metrics/static_dashboard/helper.ts | 13 +- .../services/related_dashboards_client.ts | 4 +- .../suggested_dashboards.ts | 1 + 58 files changed, 1007 insertions(+), 1750 deletions(-) delete mode 100644 src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.test.ts delete mode 100644 src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.ts delete mode 100644 src/platform/plugins/shared/dashboard/common/dashboard_container/types.ts delete mode 100644 src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.test.ts delete mode 100644 src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts create mode 100644 src/platform/plugins/shared/dashboard/common/is_dashboard_section.ts delete mode 100644 src/platform/plugins/shared/dashboard/common/lib/dashboard_panel_converters.ts create mode 100644 src/platform/plugins/shared/dashboard/common/reference_utils.ts create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_api/generate_new_panel_ids.test.ts create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_api/generate_new_panel_ids.ts rename src/platform/plugins/shared/dashboard/public/dashboard_api/{ => layout_manager}/are_layouts_equal.ts (100%) create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/deserialize_layout.test.ts create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/deserialize_layout.ts create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/index.ts rename src/platform/plugins/shared/dashboard/public/dashboard_api/{ => layout_manager}/layout_manager.test.ts (65%) rename src/platform/plugins/shared/dashboard/public/dashboard_api/{ => layout_manager}/layout_manager.ts (73%) create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/serialize_layout.test.ts create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/serialize_layout.ts create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/types.ts 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,