[dashboard] remove 'id' from DashboardPanelState explicitInput (#210927)

PR decouples dashboard plugin from EmbeddableInput type. EmbeddableInput
type is targeted from removal.

The largest change is removing `id` from `explicitInput`. Instead `id`
is available as the key in panels map.

---------

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:
Nathan Reese 2025-02-19 08:24:56 -07:00 committed by GitHub
parent 95f4cbba80
commit 09eb3503cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 81 additions and 114 deletions

View file

@ -10,7 +10,6 @@
import { createExtract, createInject } from './dashboard_container_references';
import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks';
import { ParsedDashboardAttributesWithType } from '../../types';
import { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common';
const persistableStateService = createEmbeddablePersistableStateServiceMock();
@ -83,7 +82,7 @@ const dashboardWithExtractedByValuePanel: ParsedDashboardAttributesWithType = {
explicitInput: {
id: 'panel_1',
extracted_reference: 'ref',
} as Partial<SavedObjectEmbeddableInput> & { id: string; extracted_reference: string },
},
},
},
};
@ -104,7 +103,7 @@ const unextractedDashboardByValueState: ParsedDashboardAttributesWithType = {
explicitInput: {
id: 'panel_1',
value: 'id',
} as Partial<SavedObjectEmbeddableInput> & { id: string; value: string },
},
},
},
};

View file

@ -9,7 +9,6 @@
import type { Reference } from '@kbn/content-management-utils';
import {
EmbeddableInput,
EmbeddablePersistableStateService,
EmbeddableStateWithType,
} from '@kbn/embeddable-plugin/common';
@ -89,7 +88,7 @@ export const createInject = (
panelReferences
);
workingState.panels[key].explicitInput = injectedState as EmbeddableInput;
workingState.panels[key].explicitInput = injectedState;
}
}
@ -118,16 +117,17 @@ export const createExtract = (
* TODO move this logic into the persistable state service extract method for each panel type
* that could be by value or by reference.
*/
if (panel.explicitInput.savedObjectId) {
const savedObjectId = (panel.explicitInput as { savedObjectId?: string }).savedObjectId;
if (savedObjectId) {
panel.panelRefName = `panel_${id}`;
references.push({
name: `${id}:panel_${id}`,
type: panel.type,
id: panel.explicitInput.savedObjectId as string,
id: savedObjectId,
});
delete panel.explicitInput.savedObjectId;
delete (panel.explicitInput as { savedObjectId?: string }).savedObjectId;
}
const { state: panelState, references: panelReferences } = persistableStateService.extract({
@ -138,7 +138,7 @@ export const createExtract = (
references.push(...prefixReferencesFromPanel(id, panelReferences));
const { type, ...restOfState } = panelState;
workingState.panels[id].explicitInput = restOfState as EmbeddableInput;
workingState.panels[id].explicitInput = restOfState;
}
}

View file

@ -7,11 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import {
PanelState,
EmbeddableInput,
SavedObjectEmbeddableInput,
} from '@kbn/embeddable-plugin/common';
import { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common';
import type { Reference } from '@kbn/content-management-utils';
import type { GridData } from '../../server/content_management';
@ -20,9 +16,9 @@ export interface DashboardPanelMap {
[key: string]: DashboardPanelState;
}
export interface DashboardPanelState<
TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
> extends PanelState<TEmbeddableInput> {
export interface DashboardPanelState<PanelState = object> {
type: string;
explicitInput: PanelState;
readonly gridData: GridData;
panelRefName?: string;

View file

@ -61,11 +61,11 @@ export function extractReferences(
const panels = parsedAttributes.panels;
const panelMissingType = Object.values(panels).find((panel) => panel.type === undefined);
const panelMissingType = Object.entries(panels).find(
([panelId, panel]) => panel.type === undefined
);
if (panelMissingType) {
throw new Error(
`"type" attribute is missing from panel "${panelMissingType.explicitInput.id}"`
);
throw new Error(`"type" attribute is missing from panel "${panelMissingType[0]}"`);
}
const extract = createExtract(deps.embeddablePersistableStateService);

View file

@ -10,7 +10,6 @@
import { v4 } from 'uuid';
import { omit } from 'lodash';
import type { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common';
import type { Reference } from '@kbn/content-management-utils';
import type { DashboardPanelMap } from '..';
import type { DashboardPanel } from '../../server/content_management';
@ -44,9 +43,9 @@ export const convertPanelMapToPanelsArray = (
panels: DashboardPanelMap,
removeLegacyVersion?: boolean
) => {
return Object.values(panels).map((panelState) => {
const savedObjectId = (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId;
const panelIndex = panelState.explicitInput.id;
return Object.entries(panels).map(([panelId, panelState]) => {
const savedObjectId = (panelState.explicitInput as { savedObjectId?: string }).savedObjectId;
const title = (panelState.explicitInput as { title?: string }).title;
return {
/**
* Version information used to be stored in the panel until 8.11 when it was moved to live inside the
@ -57,11 +56,9 @@ export const convertPanelMapToPanelsArray = (
type: panelState.type,
gridData: panelState.gridData,
panelIndex,
panelIndex: panelId,
panelConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']),
...(panelState.explicitInput.title !== undefined && {
title: panelState.explicitInput.title,
}),
...(title !== undefined && { title }),
...(savedObjectId !== undefined && { id: savedObjectId }),
...(panelState.panelRefName !== undefined && { panelRefName: panelState.panelRefName }),
};

View file

@ -8,7 +8,6 @@
*/
import type { Reference } from '@kbn/content-management-utils';
import type { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
import type { DashboardPanelMap } from './dashboard_container/types';
import type { DashboardAttributes } from '../server/content_management';
@ -22,10 +21,11 @@ export interface DashboardCapabilities {
/**
* 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 type ParsedDashboardAttributesWithType = EmbeddableStateWithType & {
export interface ParsedDashboardAttributesWithType {
id: string;
panels: DashboardPanelMap;
type: 'dashboard';
};
}
export interface DashboardAttributesAndReferences {
attributes: DashboardAttributes;

View file

@ -76,20 +76,20 @@ export async function loadDashboardApi({
const overrideState = creationOptions?.getInitialInput?.();
if (overrideState?.panels) {
const overridePanels: DashboardPanelMap = {};
for (const panel of Object.values(overrideState?.panels)) {
overridePanels[panel.explicitInput.id] = {
for (const [panelId, panel] of Object.entries(overrideState?.panels)) {
overridePanels[panelId] = {
...panel,
/**
* here we need to keep the state of the panel that was already in the Dashboard if one exists.
* This is because this state will become the "last saved state" for this panel.
*/
...(combinedSessionState.panels[panel.explicitInput.id] ?? []),
...(combinedSessionState.panels[panelId] ?? []),
};
/**
* We also need to add the state of this react embeddable into the runtime state to be restored.
*/
initialPanelsRuntimeState[panel.explicitInput.id] = panel.explicitInput;
initialPanelsRuntimeState[panelId] = panel.explicitInput;
}
overrideState.panels = overridePanels;
}

View file

@ -68,33 +68,28 @@ export function initializePanelsManager(
// Place the incoming embeddable if there is one
// --------------------------------------------------------------------------------------
if (incomingEmbeddable) {
let incomingEmbeddablePanelState: DashboardPanelState;
if (
incomingEmbeddable.embeddableId &&
Boolean(panels$.value[incomingEmbeddable.embeddableId])
) {
const incomingPanelId = incomingEmbeddable.embeddableId ?? v4();
let incomingPanelState: DashboardPanelState;
if (incomingEmbeddable.embeddableId && Boolean(panels$.value[incomingPanelId])) {
// this embeddable already exists, just update the explicit input.
incomingEmbeddablePanelState = panels$.value[incomingEmbeddable.embeddableId];
const sameType = incomingEmbeddablePanelState.type === incomingEmbeddable.type;
incomingPanelState = panels$.value[incomingPanelId];
const sameType = incomingPanelState.type === incomingEmbeddable.type;
incomingEmbeddablePanelState.type = incomingEmbeddable.type;
setRuntimeStateForChild(incomingEmbeddable.embeddableId, {
incomingPanelState.type = incomingEmbeddable.type;
setRuntimeStateForChild(incomingPanelId, {
// if the incoming panel is the same type as what was there before we can safely spread the old panel's explicit input
...(sameType ? incomingEmbeddablePanelState.explicitInput : {}),
...(sameType ? incomingPanelState.explicitInput : {}),
...incomingEmbeddable.input,
id: incomingEmbeddable.embeddableId,
// maintain hide panel titles setting.
hidePanelTitles: incomingEmbeddablePanelState.explicitInput.hidePanelTitles,
hidePanelTitles: (incomingPanelState.explicitInput as { hidePanelTitles?: boolean })
.hidePanelTitles,
});
incomingEmbeddablePanelState.explicitInput = {
id: incomingEmbeddablePanelState.explicitInput.id,
};
incomingPanelState.explicitInput = {};
} else {
// otherwise this incoming embeddable is brand new.
const embeddableId = incomingEmbeddable.embeddableId ?? v4();
setRuntimeStateForChild(embeddableId, incomingEmbeddable.input);
setRuntimeStateForChild(incomingPanelId, incomingEmbeddable.input);
const { newPanelPlacement } = runPanelPlacementStrategy(
PanelPlacementStrategy.findTopLeftMostOpenSpace,
{
@ -103,22 +98,22 @@ export function initializePanelsManager(
currentPanels: panels$.value,
}
);
incomingEmbeddablePanelState = {
explicitInput: { id: embeddableId },
incomingPanelState = {
explicitInput: {},
type: incomingEmbeddable.type,
gridData: {
...newPanelPlacement,
i: embeddableId,
i: incomingPanelId,
},
};
}
setPanels({
...panels$.value,
[incomingEmbeddablePanelState.explicitInput.id]: incomingEmbeddablePanelState,
[incomingPanelId]: incomingPanelState,
});
trackPanel.setScrollToPanelId(incomingEmbeddablePanelState.explicitInput.id);
trackPanel.setHighlightPanelId(incomingEmbeddablePanelState.explicitInput.id);
trackPanel.setScrollToPanelId(incomingPanelId);
trackPanel.setHighlightPanelId(incomingPanelId);
}
async function untilEmbeddableLoaded<ApiType>(id: string): Promise<ApiType | undefined> {
@ -207,7 +202,6 @@ export function initializePanelsManager(
},
explicitInput: {
...serializedState?.rawState,
id: newId,
},
};
if (initialState) setRuntimeStateForChild(newId, initialState);
@ -215,7 +209,7 @@ export function initializePanelsManager(
setPanels({ ...otherPanels, [newId]: newPanel });
if (displaySuccessMessage) {
coreServices.notifications.toasts.addSuccess({
title: getPanelAddedSuccessString(newPanel.explicitInput.title),
title: getPanelAddedSuccessString((newPanel.explicitInput as { title?: string }).title),
'data-test-subj': 'addEmbeddableToDashboardSuccess',
});
trackPanel.setScrollToPanelId(newId);
@ -258,7 +252,7 @@ export function initializePanelsManager(
width: panelToClone.gridData.w,
height: panelToClone.gridData.h,
currentPanels: panels$.value,
placeBesideId: panelToClone.explicitInput.id,
placeBesideId: idToDuplicate,
});
const newPanel = {
@ -284,9 +278,8 @@ export function initializePanelsManager(
return Object.keys(panels$.value).length;
},
getSerializedStateForChild: (childId: string) => {
const rawState = panels$.value[childId]?.explicitInput ?? { id: childId };
const { id, ...serializedState } = rawState;
return Object.keys(serializedState).length === 0
const rawState = panels$.value[childId]?.explicitInput ?? {};
return Object.keys(rawState).length === 0
? undefined
: {
rawState,

View file

@ -26,7 +26,7 @@ type DivProps = Pick<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style'
export interface Props extends DivProps {
appFixedViewport?: HTMLElement;
dashboardContainerRef?: React.MutableRefObject<HTMLElement | null>;
id: DashboardPanelState['explicitInput']['id'];
id: string;
index?: number;
type: DashboardPanelState['type'];
key: string;

View file

@ -15,7 +15,6 @@ import {
import { Serializable, SerializableRecord } from '@kbn/utility-types';
import { SavedObjectMigrationFn } from '@kbn/core/server';
import { MigrateFunction } from '@kbn/kibana-utils-plugin/common';
import { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common';
import {
convertPanelStateToSavedDashboardPanel,
@ -23,10 +22,10 @@ import {
} from './utils';
import type { SavedDashboardPanel } from '..';
type ValueOrReferenceInput = SavedObjectEmbeddableInput & {
interface ValueOrReferenceInput {
attributes?: Serializable;
savedVis?: Serializable;
};
}
// Runs the embeddable migrations on each panel
export const migrateByValueDashboardPanels =
@ -76,9 +75,9 @@ export const migrateByValueDashboardPanels =
});
// Convert the embeddable state back into the panel shape
newPanels.push({
...convertPanelStateToSavedDashboardPanel({
...convertPanelStateToSavedDashboardPanel(panel.panelIndex, {
...originalPanelState,
explicitInput: { ...migratedInput, id: migratedInput.id as string },
explicitInput: { ...migratedInput },
}),
version,
});

View file

@ -38,7 +38,7 @@ export const migrateExplicitlyHiddenTitles: SavedObjectMigrationFn<any, any> = (
// Convert each panel into the dashboard panel state
const originalPanelState = convertSavedDashboardPanelToPanelState<EmbeddableInput>(panel);
newPanels.push(
convertPanelStateToSavedDashboardPanel({
convertPanelStateToSavedDashboardPanel(panel.panelIndex, {
...originalPanelState,
explicitInput: {
...originalPanelState.explicitInput,

View file

@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { EmbeddableInput } from '@kbn/embeddable-plugin/common/types';
import type { SavedDashboardPanel } from '../schema';
import type { DashboardPanelState } from '../../../common';
@ -44,7 +43,6 @@ test('convertSavedDashboardPanelToPanelState', () => {
},
explicitInput: {
something: 'hi!',
id: '123',
savedObjectId: 'savedObjectId',
},
type: 'search',
@ -87,11 +85,11 @@ test('convertPanelStateToSavedDashboardPanel', () => {
something: 'hi!',
id: '123',
savedObjectId: 'savedObjectId',
} as EmbeddableInput,
},
type: 'search',
};
expect(convertPanelStateToSavedDashboardPanel(dashboardPanel)).toEqual({
expect(convertPanelStateToSavedDashboardPanel('123', dashboardPanel)).toEqual({
type: 'search',
embeddableConfig: {
something: 'hi!',
@ -118,13 +116,12 @@ test('convertPanelStateToSavedDashboardPanel will not add an undefined id when n
i: '123',
},
explicitInput: {
id: '123',
something: 'hi!',
} as EmbeddableInput,
},
type: 'search',
};
const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel);
const converted = convertPanelStateToSavedDashboardPanel('123', dashboardPanel);
expect(Object.hasOwn(converted, 'id')).toBe(false);
});
@ -138,13 +135,12 @@ test('convertPanelStateToSavedDashboardPanel will not leave title as part of emb
i: '123',
},
explicitInput: {
id: '123',
title: 'title',
} as EmbeddableInput,
},
type: 'search',
};
const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel);
const converted = convertPanelStateToSavedDashboardPanel('123', dashboardPanel);
expect(Object.hasOwn(converted.embeddableConfig, 'title')).toBe(false);
expect(converted.title).toBe('title');
});
@ -159,13 +155,12 @@ test('convertPanelStateToSavedDashboardPanel retains legacy version info', () =>
i: '123',
},
explicitInput: {
id: '123',
title: 'title',
} as EmbeddableInput,
},
type: 'search',
version: '8.10.0',
};
const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel);
const converted = convertPanelStateToSavedDashboardPanel('123', dashboardPanel);
expect(converted.version).toBe('8.10.0');
});

View file

@ -8,41 +8,43 @@
*/
import { omit } from 'lodash';
import type { EmbeddableInput, SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common';
import type { SavedDashboardPanel } from '../schema';
import type { DashboardPanelState } from '../../../common';
export function convertSavedDashboardPanelToPanelState<
TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
>(savedDashboardPanel: SavedDashboardPanel): DashboardPanelState<TEmbeddableInput> {
export function convertSavedDashboardPanelToPanelState<PanelState = object>(
savedDashboardPanel: SavedDashboardPanel
): DashboardPanelState<PanelState> {
return {
type: savedDashboardPanel.type,
gridData: savedDashboardPanel.gridData,
panelRefName: savedDashboardPanel.panelRefName,
explicitInput: {
id: savedDashboardPanel.panelIndex,
...(savedDashboardPanel.id !== undefined && { savedObjectId: savedDashboardPanel.id }),
...(savedDashboardPanel.title !== undefined && { title: savedDashboardPanel.title }),
...savedDashboardPanel.embeddableConfig,
} as TEmbeddableInput,
} as PanelState,
version: savedDashboardPanel.version,
};
}
export function convertPanelStateToSavedDashboardPanel(
panelId: string,
panelState: DashboardPanelState
): SavedDashboardPanel {
const savedObjectId = (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId;
const panelIndex = panelState.explicitInput.id;
const savedObjectId = (panelState.explicitInput as { savedObjectId?: string }).savedObjectId;
const title = (panelState.explicitInput as { title?: string }).title;
return {
type: panelState.type,
gridData: {
...panelState.gridData,
i: panelIndex,
i: panelId,
},
panelIndex,
embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']),
...(panelState.explicitInput.title !== undefined && { title: panelState.explicitInput.title }),
panelIndex: panelId,
embeddableConfig: omit(
panelState.explicitInput as { id: string; savedObjectId?: string; title?: string },
['id', 'savedObjectId', 'title']
),
...(title !== undefined && { title }),
...(savedObjectId !== undefined && { id: savedObjectId }),
...(panelState.panelRefName !== undefined && { panelRefName: panelState.panelRefName }),
...(panelState.version !== undefined && { version: panelState.version }),

View file

@ -85,7 +85,6 @@ export const collectPanelsByType = (
collectorData.panels.by_type[type].details = embeddableService.telemetry(
{
...panel.embeddableConfig,
id: panel.id || '',
type: panel.type,
},
collectorData.panels.by_type[type].details
@ -101,7 +100,6 @@ export const controlsCollectorFactory =
{
...attributes.controlGroupInput,
type: CONTROL_GROUP_TYPE,
id: `DASHBOARD_${CONTROL_GROUP_TYPE}`,
},
collectorData.controls
) as ControlGroupTelemetry;

View file

@ -125,7 +125,6 @@ describe('Serialization utils', () => {
expect(serializedState).toEqual({
rawState: {
id: uuid,
type: 'search',
attributes: {
...toSavedSearchAttributes(savedSearch, searchSource.serialize().searchSourceJSON),

View file

@ -123,7 +123,6 @@ export const serializeState = ({
}
const { state, references } = extract({
id: uuid,
type: SEARCH_EMBEDDABLE_TYPE,
attributes: {
...savedSearchAttributes,

View file

@ -11,7 +11,6 @@ export type {
EmbeddableInput,
CommonEmbeddableStartContract,
EmbeddableStateWithType,
PanelState,
EmbeddablePersistableStateService,
EmbeddableRegistryDefinition,
} from './types';

View file

@ -74,19 +74,10 @@ export type EmbeddableInput = {
executionContext?: KibanaExecutionContext;
};
export interface PanelState<
E extends EmbeddableInput & { id: string } = { id: string; version?: string }
> {
// The type of embeddable in this panel. Will be used to find the factory in which to
// load the embeddable.
export type EmbeddableStateWithType = {
enhancements?: SerializableRecord;
type: string;
// Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input
// will be derived from the container's input. **State in here will override state derived from the container.**
explicitInput: Partial<E> & { id: string };
}
export type EmbeddableStateWithType = EmbeddableInput & { type: string };
};
export interface EmbeddableRegistryDefinition<
P extends EmbeddableStateWithType = EmbeddableStateWithType