mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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 <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
62fc123ba9
commit
3d6954e252
58 changed files with 1007 additions and 1750 deletions
|
@ -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'],
|
||||
}),
|
||||
};
|
||||
}}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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<GridData, 'i' | 'y'>;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type DashboardAttributes = Omit<DashboardAttributesV1, 'controlGroupInput'> & {
|
||||
controlGroupInput?: ControlGroupAttributes;
|
||||
sections?: DashboardSectionState[];
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
};
|
||||
};
|
|
@ -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<GridData, 'i' | 'y'>;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface DashboardPanelMap {
|
||||
[key: string]: DashboardPanelState;
|
||||
}
|
||||
|
||||
export interface DashboardPanelState<PanelState = object> {
|
||||
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[];
|
||||
}
|
|
@ -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\\""`);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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 };
|
||||
};
|
|
@ -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}`,
|
||||
}));
|
||||
};
|
|
@ -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, 'panels' | 'sections'> & {
|
||||
DashboardState & {
|
||||
controlGroupInput?: DashboardState['controlGroupInput'] & SerializableRecord;
|
||||
|
||||
panels: Array<DashboardPanel | DashboardSection>;
|
||||
|
||||
references?: DashboardState['references'] & SerializableRecord;
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,8 +15,7 @@ export const DEFAULT_DASHBOARD_STATE: DashboardState = {
|
|||
query: { query: '', language: 'kuery' },
|
||||
description: '',
|
||||
filters: [],
|
||||
panels: {},
|
||||
sections: {},
|
||||
panels: [],
|
||||
title: '',
|
||||
tags: [],
|
||||
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 [],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
}
|
|
@ -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';
|
|
@ -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<PhaseEvent | undefined>,
|
||||
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,
|
|
@ -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<typeof initializeTrackPanel>,
|
||||
getReferences: (id: string) => Reference[]
|
||||
) {
|
||||
|
@ -62,76 +69,16 @@ export function initializeLayoutManager(
|
|||
const children$ = new BehaviorSubject<DashboardChildren>({});
|
||||
const { layout: initialLayout, childState: initialChildState } = deserializeLayout(
|
||||
initialPanels,
|
||||
initialSections
|
||||
getReferences
|
||||
);
|
||||
const layout$ = new BehaviorSubject<DashboardLayout>(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<DashboardState>
|
||||
): 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',
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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<DashboardState, 'panels' | 'references'> {
|
||||
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,
|
||||
};
|
||||
}
|
|
@ -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<DashboardSection, 'collapsed' | 'gridData' | 'title'> };
|
||||
}
|
||||
|
||||
export interface DashboardChildState {
|
||||
[uuid: string]: SerializedPanelState<object>;
|
||||
}
|
|
@ -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<DashboardPanelState, 'gridData'> & HasType;
|
||||
export interface DashboardLayout {
|
||||
panels: { [uuid: string]: DashboardPanel }; // partial of DashboardPanelState
|
||||
sections: DashboardSectionMap;
|
||||
}
|
||||
|
||||
export interface DashboardChildState {
|
||||
[uuid: string]: SerializedPanelState<object>;
|
||||
}
|
||||
|
||||
export interface DashboardChildren {
|
||||
[uuid: string]: DefaultEmbeddableApi;
|
||||
}
|
||||
|
||||
export interface DashboardCreationOptions {
|
||||
getInitialInput?: () => Partial<DashboardState>;
|
||||
|
||||
|
@ -177,10 +157,6 @@ export interface DashboardInternalApi {
|
|||
layout$: BehaviorSubject<DashboardLayout>;
|
||||
registerChildApi: (api: DefaultEmbeddableApi) => void;
|
||||
setControlGroupApi: (controlGroupApi: ControlGroupApi) => void;
|
||||
serializeLayout: () => {
|
||||
references: Reference[];
|
||||
panels: DashboardPanelMap;
|
||||
sections: DashboardSectionMap;
|
||||
};
|
||||
serializeLayout: () => Pick<DashboardState, 'panels' | 'references'>;
|
||||
isSectionCollapsed: (sectionId?: string) => boolean;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<DashboardLocatorParams>)[key]
|
||||
(unsavedDashboardState as unknown as Partial<DashboardLocatorParams>)[key]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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<DashboardState, 'panels' | 'sections'>;
|
||||
|
||||
/**
|
||||
* 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<PanelState> {
|
||||
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<P
|
|||
return {};
|
||||
}
|
||||
|
||||
// < 8.17 panels state stored panelConfig as embeddableConfig
|
||||
const standardizedPanels = panels.map((panel) => {
|
||||
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 };
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<DashboardState>) => {
|
||||
const { api, internalApi } = buildMockDashboardApi({
|
||||
overrides: {
|
||||
panels,
|
||||
...(sections && { sections }),
|
||||
},
|
||||
overrides,
|
||||
});
|
||||
const component = render(
|
||||
<EuiThemeProvider>
|
||||
|
@ -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<void>((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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<HTMLElement | null>;
|
||||
id: string;
|
||||
index?: number;
|
||||
type: DashboardPanelState['type'];
|
||||
type: string;
|
||||
key: string;
|
||||
isRenderable?: boolean;
|
||||
setDragHandles?: (refs: Array<HTMLElement | null>) => void;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<DashboardStart>;
|
||||
|
||||
|
@ -126,68 +126,70 @@ export function getSampleDashboardState(overrides?: Partial<DashboardState>): 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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<GridData, 'i'>;
|
||||
|
@ -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);
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<PanelState extends object>(
|
||||
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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<DashboardPanelMap | undefined> {
|
||||
): Promise<DashboardState['panels'] | undefined> {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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) },
|
||||
})),
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue