kibana/examples/grid_example/public/use_mock_dashboard_api.tsx
Devon Thomson 3e882d8cd9
[Embeddables] Serialized State Only (#215947)
Closes https://github.com/elastic/kibana/issues/205531
Closes #219877.
Closes https://github.com/elastic/kibana/issues/213153
Closes https://github.com/elastic/kibana/issues/150920
Closes https://github.com/elastic/kibana/issues/203130
 
### Overview
The embeddable framework has two types of state: `SerializedState` and
`RuntimeState`.

`SerializedState` is the form of the state when saved into a Dashboard
saved object. I.e. the References are extracted, and state saved
externally (by reference) is removed. In contrast `RuntimeState` is an
exact snapshot of the state used by the embeddable to render.

<b>Exposing SerializedState and RuntimeState was a mistake</b> that
caused numerous regressions and architectural complexities.

This PR simplifies the embeddable framework by only exposing
`SerializedState`. `RuntimeState` stays localized to the embeddable
implementation and is never leaked to the embeddable framework.

### Whats changed
* `ReactEmbeddableFactory<SerializedState, RuntimeState, Api>` =>
`EmbeddableFactory<SerializedState, Api>`
* `deserializeState` removed from embeddable factory. Instead,
`SerializedState` is passed directly into `buildEmbeddable`.
* `buildEmbeddable` parameter `buildApi` replaced with `finalizeApi`.
`buildApi({ api, comparators })` => `finalizeApi(api)`.
* The embeddable framework previously used its knowledge of
`RuntimeState` to setup and monitor unsaved changes. Now, unsaved
changes setup is pushed down to the embeddable implementation since the
embeddable framework no longer has knowledge of embeddable RuntimeState.

### Reviewer instructions
<b>Please prioritize reviews.</b> This is a large effort from our team
and is blocking many other initiatives. Getting this merged is a top
priority.

This is a large change that would best be reviewed by manually testing
the changes
* adding/editing your embeddable types
* Ensuring dashboard shows unsaved changes as expected
* Ensuring dashboard resets unsaved changes as expected
* Ensuring dashboard does not show unsaved changes after save and reset
* Returning to a dashboard with unsaved changes renders embeddables with
those unsaved changes

---------

Co-authored-by: Hannah Mudge <Heenawter@users.noreply.github.com>
Co-authored-by: Nathan Reese <reese.nathan@elastic.co>
Co-authored-by: Nick Peihl <nick.peihl@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Catherine Liu <catherine.liu@elastic.co>
Co-authored-by: Ola Pawlus <98127445+olapawlus@users.noreply.github.com>
2025-05-06 15:08:34 -06:00

126 lines
4.4 KiB
TypeScript

/*
* 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 { cloneDeep } from 'lodash';
import { useMemo } from 'react';
import { BehaviorSubject } from 'rxjs';
import { v4 } from 'uuid';
import { TimeRange } from '@kbn/es-query';
import { PanelPackage } from '@kbn/presentation-containers';
import { ViewMode } from '@kbn/presentation-publishing';
import {
MockDashboardApi,
MockSerializedDashboardState,
MockedDashboardPanelMap,
MockedDashboardRowMap,
} from './types';
const DASHBOARD_GRID_COLUMN_COUNT = 48;
const DEFAULT_PANEL_HEIGHT = 15;
const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;
export const useMockDashboardApi = ({
savedState,
}: {
savedState: MockSerializedDashboardState;
}): MockDashboardApi => {
const mockDashboardApi = useMemo(() => {
const panels$ = new BehaviorSubject<MockedDashboardPanelMap>(savedState.panels);
const expandedPanelId$ = new BehaviorSubject<string | undefined>(undefined);
const viewMode$ = new BehaviorSubject<ViewMode>('edit');
return {
getSerializedStateForChild: (id: string) => {
return {
rawState: panels$.getValue()[id].explicitInput,
references: [],
};
},
children$: new BehaviorSubject({}),
timeRange$: new BehaviorSubject<TimeRange>({
from: 'now-24h',
to: 'now',
}),
filters$: new BehaviorSubject([]),
query$: new BehaviorSubject(''),
viewMode$,
setViewMode: (viewMode: ViewMode) => viewMode$.next(viewMode),
panels$,
getPanelCount: () => {
return Object.keys(panels$.getValue()).length;
},
rows$: new BehaviorSubject<MockedDashboardRowMap>(savedState.rows),
expandedPanelId$,
expandPanel: (id: string) => {
if (expandedPanelId$.getValue()) {
expandedPanelId$.next(undefined);
} else {
expandedPanelId$.next(id);
}
},
removePanel: (id: string) => {
const panels = { ...mockDashboardApi.panels$.getValue() };
delete panels[id]; // the grid layout component will handle compacting, if necessary
mockDashboardApi.panels$.next(panels);
},
replacePanel: async (id: string, newPanel: PanelPackage): Promise<string> => {
const currentPanels = mockDashboardApi.panels$.getValue();
const otherPanels = { ...currentPanels };
const oldPanel = currentPanels[id];
delete otherPanels[id];
const newId = v4();
otherPanels[newId] = {
...oldPanel,
explicitInput: { ...(newPanel.serializedState?.rawState ?? {}), id: newId },
};
mockDashboardApi.panels$.next(otherPanels);
return newId;
},
addNewPanel: async (panelPackage: PanelPackage): Promise<undefined> => {
// we are only implementing "place at top" here, for demo purposes
const currentPanels = mockDashboardApi.panels$.getValue();
const otherPanels = { ...currentPanels };
for (const [id, panel] of Object.entries(currentPanels)) {
const currentPanel = cloneDeep(panel);
currentPanel.gridData.y = currentPanel.gridData.y + DEFAULT_PANEL_HEIGHT;
otherPanels[id] = currentPanel;
}
const newId = v4();
mockDashboardApi.panels$.next({
...otherPanels,
[newId]: {
type: panelPackage.panelType,
gridData: {
row: 'first',
x: 0,
y: 0,
w: DEFAULT_PANEL_WIDTH,
h: DEFAULT_PANEL_HEIGHT,
i: newId,
},
explicitInput: {
...(panelPackage.serializedState?.rawState ?? {}),
id: newId,
},
},
});
},
canRemovePanels: () => true,
getChildApi: () => {
throw new Error('getChildApi implemenation not provided');
},
};
// only run onMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return mockDashboardApi;
};