[Embeddable rebuild] Allow Dashboard to provide references (#176455)

Adds the ability for the Dashboard to provide references for its React Embeddable children to inject, and adds the ability for the React Embeddable children to provide extracted references back to the Dashboard.
This commit is contained in:
Devon Thomson 2024-02-12 14:55:16 -05:00 committed by GitHub
parent 06ecb5aee9
commit 6827db46d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 445 additions and 83 deletions

View file

@ -8,12 +8,15 @@
"server": true,
"browser": true,
"requiredPlugins": [
"dataViews",
"embeddable",
"uiActions",
"dashboard",
"data",
"charts",
"fieldFormats"
],
"extraPublicDirs": [
"public/hello_world"
]
"requiredBundles": ["presentationUtil"],
"extraPublicDirs": ["public/hello_world"]
}
}

View file

@ -6,9 +6,13 @@
* Side Public License, v 1.
*/
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { Plugin, CoreSetup, CoreStart } from '@kbn/core/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import {
HelloWorldEmbeddableFactory,
HELLO_WORLD_EMBEDDABLE,
@ -33,6 +37,8 @@ import {
} from './filter_debugger';
import { registerMarkdownEditorEmbeddable } from './react_embeddables/eui_markdown/eui_markdown_react_embeddable';
import { registerCreateEuiMarkdownAction } from './react_embeddables/eui_markdown/create_eui_markdown_action';
import { registerFieldListFactory } from './react_embeddables/field_list/field_list_react_embeddable';
import { registerCreateFieldListAction } from './react_embeddables/field_list/create_field_list_action';
export interface EmbeddableExamplesSetupDependencies {
embeddable: EmbeddableSetup;
@ -40,7 +46,12 @@ export interface EmbeddableExamplesSetupDependencies {
}
export interface EmbeddableExamplesStartDependencies {
dataViews: DataViewsPublicPluginStart;
embeddable: EmbeddableStart;
uiActions: UiActionsStart;
data: DataPublicPluginStart;
charts: ChartsPluginStart;
fieldFormats: FieldFormatsStart;
}
interface ExampleEmbeddableFactories {
@ -70,9 +81,6 @@ export class EmbeddableExamplesPlugin
core: CoreSetup<EmbeddableExamplesStartDependencies>,
deps: EmbeddableExamplesSetupDependencies
) {
registerMarkdownEditorEmbeddable();
registerCreateEuiMarkdownAction(deps.uiActions);
this.exampleEmbeddableFactories.getHelloWorldEmbeddableFactory =
deps.embeddable.registerEmbeddableFactory(
HELLO_WORLD_EMBEDDABLE,
@ -104,6 +112,12 @@ export class EmbeddableExamplesPlugin
core: CoreStart,
deps: EmbeddableExamplesStartDependencies
): EmbeddableExamplesStart {
registerFieldListFactory(core, deps);
registerCreateFieldListAction(deps.uiActions);
registerMarkdownEditorEmbeddable();
registerCreateEuiMarkdownAction(deps.uiActions);
return {
createSampleData: async () => {},
factories: this.exampleEmbeddableFactories as ExampleEmbeddableFactories,

View file

@ -0,0 +1,11 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export const FIELD_LIST_ID = 'field_list';
export const ADD_FIELD_LIST_ACTION_ID = 'create_field_list';
export const FIELD_LIST_DATA_VIEW_REF_NAME = 'field_list_data_view_id';

View file

@ -0,0 +1,35 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { UiActionsPublicStart } from '@kbn/ui-actions-plugin/public/plugin';
import { ADD_FIELD_LIST_ACTION_ID, FIELD_LIST_ID } from './constants';
export const registerCreateFieldListAction = (uiActions: UiActionsPublicStart) => {
uiActions.registerAction<EmbeddableApiContext>({
id: ADD_FIELD_LIST_ACTION_ID,
getIconType: () => 'indexOpen',
isCompatible: async ({ embeddable }) => {
return apiIsPresentationContainer(embeddable);
},
execute: async ({ embeddable }) => {
if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError();
embeddable.addNewPanel({
panelType: FIELD_LIST_ID,
});
},
getDisplayName: () =>
i18n.translate('embeddableExamples.unifiedFieldList.displayName', {
defaultMessage: 'Field list',
}),
});
uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_FIELD_LIST_ACTION_ID);
};

View file

@ -0,0 +1,221 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { css } from '@emotion/react';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { Reference } from '@kbn/content-management-utils';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import {
DataViewsPublicPluginStart,
DATA_VIEW_SAVED_OBJECT_TYPE,
type DataView,
} from '@kbn/data-views-plugin/public';
import {
initializeReactEmbeddableTitles,
initializeReactEmbeddableUuid,
ReactEmbeddableFactory,
RegisterReactEmbeddable,
registerReactEmbeddableFactory,
useReactEmbeddableApiHandle,
useReactEmbeddableUnsavedChanges,
} from '@kbn/embeddable-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { i18n } from '@kbn/i18n';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { LazyDataViewPicker, withSuspense } from '@kbn/presentation-util-plugin/public';
import { euiThemeVars } from '@kbn/ui-theme';
import {
UnifiedFieldListSidebarContainer,
type UnifiedFieldListSidebarContainerProps,
} from '@kbn/unified-field-list';
import { cloneDeep } from 'lodash';
import React, { useEffect, useState } from 'react';
import { BehaviorSubject } from 'rxjs';
import { FIELD_LIST_DATA_VIEW_REF_NAME, FIELD_LIST_ID } from './constants';
import { FieldListApi, FieldListSerializedStateState } from './types';
const DataViewPicker = withSuspense(LazyDataViewPicker, null);
const getCreationOptions: UnifiedFieldListSidebarContainerProps['getCreationOptions'] = () => {
return {
originatingApp: '',
localStorageKeyPrefix: 'examples',
timeRangeUpdatesType: 'timefilter',
compressed: true,
showSidebarToggleButton: false,
disablePopularFields: true,
};
};
export const registerFieldListFactory = (
core: CoreStart,
{
dataViews,
data,
charts,
fieldFormats,
}: {
dataViews: DataViewsPublicPluginStart;
data: DataPublicPluginStart;
charts: ChartsPluginStart;
fieldFormats: FieldFormatsStart;
}
) => {
const fieldListEmbeddableFactory: ReactEmbeddableFactory<
FieldListSerializedStateState,
FieldListApi
> = {
deserializeState: (state) => {
const serializedState = cloneDeep(state.rawState) as FieldListSerializedStateState;
// inject the reference
const dataViewIdRef = state.references?.find(
(ref) => ref.name === FIELD_LIST_DATA_VIEW_REF_NAME
);
if (dataViewIdRef && serializedState) {
serializedState.dataViewId = dataViewIdRef?.id;
}
return serializedState;
},
getComponent: async (initialState, maybeId) => {
const uuid = initializeReactEmbeddableUuid(maybeId);
const { titlesApi, titleComparators, serializeTitles } =
initializeReactEmbeddableTitles(initialState);
const allDataViews = await dataViews.getIdsWithTitle();
const selectedDataViewId$ = new BehaviorSubject<string | undefined>(
initialState.dataViewId ?? (await dataViews.getDefaultDataView())?.id
);
const selectedFieldNames$ = new BehaviorSubject<string[] | undefined>(
initialState.selectedFieldNames
);
return RegisterReactEmbeddable((apiRef) => {
const { unsavedChanges, resetUnsavedChanges } = useReactEmbeddableUnsavedChanges(
uuid,
fieldListEmbeddableFactory,
{
dataViewId: [selectedDataViewId$, (value) => selectedDataViewId$.next(value)],
selectedFieldNames: [
selectedFieldNames$,
(value) => selectedFieldNames$.next(value),
(a, b) => {
return (a?.slice().sort().join(',') ?? '') === (b?.slice().sort().join(',') ?? '');
},
],
...titleComparators,
}
);
useReactEmbeddableApiHandle(
{
...titlesApi,
unsavedChanges,
resetUnsavedChanges,
serializeState: async () => {
const dataViewId = selectedDataViewId$.getValue();
const references: Reference[] = dataViewId
? [
{
type: DATA_VIEW_SAVED_OBJECT_TYPE,
name: FIELD_LIST_DATA_VIEW_REF_NAME,
id: dataViewId,
},
]
: [];
return {
rawState: {
...serializeTitles(),
// here we skip serializing the dataViewId, because the reference contains that information.
selectedFieldNames: selectedFieldNames$.getValue(),
},
references,
};
},
},
apiRef,
uuid
);
const [selectedDataViewId, selectedFieldNames] = useBatchedPublishingSubjects(
selectedDataViewId$,
selectedFieldNames$
);
const [selectedDataView, setSelectedDataView] = useState<DataView | undefined>(undefined);
useEffect(() => {
if (!selectedDataViewId) return;
let mounted = true;
(async () => {
const dataView = await dataViews.get(selectedDataViewId);
if (!mounted) return;
setSelectedDataView(dataView);
})();
return () => {
mounted = false;
};
}, [selectedDataViewId]);
return (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem
grow={false}
css={css`
padding: ${euiThemeVars.euiSizeS};
`}
>
<DataViewPicker
dataViews={allDataViews}
selectedDataViewId={selectedDataViewId}
onChangeDataViewId={(nextSelection) => {
selectedDataViewId$.next(nextSelection);
}}
trigger={{
label:
selectedDataView?.getName() ??
i18n.translate('embeddableExamples.unifiedFieldList.selectDataViewMessage', {
defaultMessage: 'Please select a data view',
}),
}}
/>
</EuiFlexItem>
<EuiFlexItem>
{selectedDataView ? (
<UnifiedFieldListSidebarContainer
fullWidth={true}
variant="list-always"
dataView={selectedDataView}
allFields={selectedDataView.fields}
getCreationOptions={getCreationOptions}
workspaceSelectedFieldNames={selectedFieldNames}
services={{ dataViews, data, fieldFormats, charts, core }}
onAddFieldToWorkspace={(field) =>
selectedFieldNames$.next([
...(selectedFieldNames$.getValue() ?? []),
field.name,
])
}
onRemoveFieldFromWorkspace={(field) => {
selectedFieldNames$.next(
(selectedFieldNames$.getValue() ?? []).filter((name) => name !== field.name)
);
}}
/>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
);
});
},
};
registerReactEmbeddableFactory(FIELD_LIST_ID, fieldListEmbeddableFactory);
};

View file

@ -0,0 +1,19 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import {
DefaultEmbeddableApi,
SerializedReactEmbeddableTitles,
} from '@kbn/embeddable-plugin/public';
export type FieldListSerializedStateState = SerializedReactEmbeddableTitles & {
dataViewId?: string;
selectedFieldNames?: string[];
};
export type FieldListApi = DefaultEmbeddableApi;

View file

@ -21,6 +21,14 @@
"@kbn/ui-theme",
"@kbn/i18n",
"@kbn/es-query",
"@kbn/presentation-containers"
"@kbn/presentation-containers",
"@kbn/data-views-plugin",
"@kbn/data-plugin",
"@kbn/charts-plugin",
"@kbn/field-formats-plugin",
"@kbn/content-management-utils",
"@kbn/core-lifecycle-browser",
"@kbn/presentation-util-plugin",
"@kbn/unified-field-list"
]
}

View file

@ -6,14 +6,14 @@
* Side Public License, v 1.
*/
import type { SavedObjectReference } from '@kbn/core-saved-objects-api-server';
import { Reference } from '@kbn/content-management-utils';
/**
* A package containing the serialized Embeddable state, with references extracted. When saving Embeddables using any
* strategy, this is the format that should be used.
*/
export interface SerializedPanelState<RawStateType extends object = object> {
references?: SavedObjectReference[];
references?: Reference[];
rawState: RawStateType;
version?: string;
}

View file

@ -9,6 +9,6 @@
"kbn_references": [
"@kbn/presentation-publishing",
"@kbn/core-mount-utils-browser",
"@kbn/core-saved-objects-api-server",
"@kbn/content-management-utils",
]
}

View file

@ -6,18 +6,32 @@
* Side Public License, v 1.
*/
import {
EmbeddableInput,
EmbeddableStateWithType,
EmbeddablePersistableStateService,
} from '@kbn/embeddable-plugin/common';
import { Reference } from '@kbn/content-management-utils';
import { CONTROL_GROUP_TYPE, PersistableControlGroupInput } from '@kbn/controls-plugin/common';
import { DashboardPanelState } from '../types';
import {
EmbeddableInput,
EmbeddablePersistableStateService,
EmbeddableStateWithType,
} from '@kbn/embeddable-plugin/common';
import { ParsedDashboardAttributesWithType } from '../../types';
const getPanelStatePrefix = (state: DashboardPanelState) => `${state.explicitInput.id}:`;
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 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_';
const controlGroupId = 'dashboard_control_group';
@ -35,16 +49,15 @@ export const createInject = (
for (const [key, panel] of Object.entries(workingState.panels)) {
workingState.panels[key] = { ...panel };
// Find the references for this panel
const prefix = getPanelStatePrefix(panel);
const filteredReferences = references
.filter((reference) => reference.name.indexOf(prefix) === 0)
.map((reference) => ({ ...reference, name: reference.name.replace(prefix, '') }));
const filteredReferences = getReferencesForPanelId(key, references);
const panelReferences = filteredReferences.length === 0 ? references : filteredReferences;
// Inject dashboard references back in
/**
* 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
@ -116,15 +129,18 @@ export const createExtract = (
workingState.panels = { ...workingState.panels };
// Run every panel through the state service to get the nested references
for (const [key, panel] of Object.entries(workingState.panels)) {
const prefix = getPanelStatePrefix(panel);
// If the panel is a saved object, then we will make the reference for that saved object and change the explicit input
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.
*/
if (panel.explicitInput.savedObjectId) {
panel.panelRefName = `panel_${key}`;
panel.panelRefName = `panel_${id}`;
references.push({
name: `${prefix}panel_${key}`,
name: `${id}:panel_${id}`,
type: panel.type,
id: panel.explicitInput.savedObjectId as string,
});
@ -137,18 +153,10 @@ export const createExtract = (
type: panel.type,
});
// We're going to prefix the names of the references so that we don't end up with dupes (from visualizations for instance)
const prefixedReferences = panelReferences
.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}`,
}));
references.push(...prefixedReferences);
references.push(...prefixReferencesFromPanel(id, panelReferences));
const { type, ...restOfState } = panelState;
workingState.panels[key].explicitInput = restOfState as EmbeddableInput;
workingState.panels[id].explicitInput = restOfState as EmbeddableInput;
}
}

View file

@ -6,20 +6,19 @@
* Side Public License, v 1.
*/
import React, { useState, useRef, useEffect, useLayoutEffect, useMemo } from 'react';
import { EuiLoadingChart } from '@elastic/eui';
import classNames from 'classnames';
import { PhaseEvent } from '@kbn/presentation-publishing';
import { css } from '@emotion/react';
import {
ReactEmbeddableRenderer,
EmbeddablePanel,
reactEmbeddableRegistryHasKey,
ReactEmbeddableRenderer,
ViewMode,
} from '@kbn/embeddable-plugin/public';
import { css } from '@emotion/react';
import { PhaseEvent } from '@kbn/presentation-publishing';
import classNames from 'classnames';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { DashboardPanelState } from '../../../../common';
import { getReferencesForPanelId } from '../../../../common/dashboard_container/persistable_state/dashboard_container_references';
import { pluginServices } from '../../../services/plugin_services';
import { useDashboardContainer } from '../../embeddable/dashboard_container';
@ -101,17 +100,19 @@ export const Item = React.forwardRef<HTMLDivElement, Props>(
: css``;
const renderedEmbeddable = useMemo(() => {
const references = getReferencesForPanelId(id, container.savedObjectReferences);
// render React embeddable
if (reactEmbeddableRegistryHasKey(type)) {
return (
<ReactEmbeddableRenderer
uuid={id}
key={`${type}_${id}`}
type={type}
// TODO Embeddable refactor. References here
state={{ rawState: panel.explicitInput, version: panel.version, references: [] }}
state={{ rawState: panel.explicitInput, version: panel.version, references }}
/>
);
}
// render legacy embeddable
return (
<EmbeddablePanel
key={type}
@ -125,8 +126,6 @@ export const Item = React.forwardRef<HTMLDivElement, Props>(
);
}, [container, id, index, onPanelStatusChange, type, panel]);
// render legacy embeddable
return (
<div
css={focusStyles}

View file

@ -6,17 +6,21 @@
* Side Public License, v 1.
*/
import { Reference } from '@kbn/content-management-utils';
import type { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { reactEmbeddableRegistryHasKey } from '@kbn/embeddable-plugin/public';
import {
EmbeddableInput,
isReferenceOrValueEmbeddable,
reactEmbeddableRegistryHasKey,
} from '@kbn/embeddable-plugin/public';
import { SerializedPanelState } from '@kbn/presentation-containers';
import { showSaveModal } from '@kbn/saved-objects-plugin/public';
import { cloneDeep } from 'lodash';
import React from 'react';
import { batch } from 'react-redux';
import { EmbeddableInput, isReferenceOrValueEmbeddable } from '@kbn/embeddable-plugin/public';
import { DashboardContainerInput, DashboardPanelMap } from '../../../../common';
import { prefixReferencesFromPanel } from '../../../../common/dashboard_container/persistable_state/dashboard_container_references';
import { DASHBOARD_CONTENT_ID, SAVED_OBJECT_POST_TIME } from '../../../dashboard_constants';
import {
SaveDashboardReturn,
@ -30,7 +34,8 @@ import { DashboardSaveModal } from './overlays/save_modal';
const serializeAllPanelState = async (
dashboard: DashboardContainer
): Promise<DashboardContainerInput['panels']> => {
): Promise<{ panels: DashboardContainerInput['panels']; references: Reference[] }> => {
const references: Reference[] = [];
const reactEmbeddableSavePromises: Array<
Promise<{ serializedState: SerializedPanelState; uuid: string }>
> = [];
@ -51,8 +56,9 @@ const serializeAllPanelState = async (
const saveResults = await Promise.all(reactEmbeddableSavePromises);
for (const { serializedState, uuid } of saveResults) {
panels[uuid].explicitInput = { ...serializedState.rawState, id: uuid };
references.push(...prefixReferencesFromPanel(uuid, serializedState.references ?? []));
}
return panels;
return { panels, references };
};
export function runSaveAs(this: DashboardContainer) {
@ -112,7 +118,7 @@ export function runSaveAs(this: DashboardContainer) {
// do not save if title is duplicate and is unconfirmed
return {};
}
const nextPanels = await serializeAllPanelState(this);
const { panels: nextPanels, references } = await serializeAllPanelState(this);
const dashboardStateToSave: DashboardContainerInput = {
...currentState,
panels: nextPanels,
@ -127,6 +133,7 @@ export function runSaveAs(this: DashboardContainer) {
const beforeAddTime = window.performance.now();
const saveResult = await saveDashboardState({
panelReferences: references,
currentState: stateToSave,
saveOptions,
lastSavedId,
@ -145,12 +152,13 @@ export function runSaveAs(this: DashboardContainer) {
batch(() => {
this.dispatch.setStateFromSaveModal(stateFromSaveModal);
this.dispatch.setLastSavedInput(dashboardStateToSave);
this.lastSavedState.next();
if (this.controlGroup && persistableControlGroupInput) {
this.controlGroup.dispatch.setLastSavedInput(persistableControlGroupInput);
}
});
}
this.savedObjectReferences = saveResult.references ?? [];
this.lastSavedState.next();
resolve(saveResult);
return saveResult;
};
@ -186,7 +194,7 @@ export async function runQuickSave(this: DashboardContainer) {
if (managed) return;
const nextPanels = await serializeAllPanelState(this);
const { panels: nextPanels, references } = await serializeAllPanelState(this);
const dashboardStateToSave: DashboardContainerInput = { ...currentState, panels: nextPanels };
let stateToSave: SavedDashboardInput = dashboardStateToSave;
let persistableControlGroupInput: PersistableControlGroupInput | undefined;
@ -196,11 +204,13 @@ export async function runQuickSave(this: DashboardContainer) {
}
const saveResult = await saveDashboardState({
lastSavedId,
panelReferences: references,
currentState: stateToSave,
saveOptions: {},
lastSavedId,
});
this.savedObjectReferences = saveResult.references ?? [];
this.dispatch.setLastSavedInput(dashboardStateToSave);
this.lastSavedState.next();
if (this.controlGroup && persistableControlGroupInput) {
@ -279,6 +289,7 @@ export async function runClone(this: DashboardContainer) {
title: newTitle,
},
});
this.savedObjectReferences = saveResult.references ?? [];
resolve(saveResult);
return saveResult.id
? {

View file

@ -239,6 +239,13 @@ export const initializeDashboard = async ({
description: initialDashboardInput.title,
};
// --------------------------------------------------------------------------------------
// Track references
// --------------------------------------------------------------------------------------
untilDashboardReady().then((dashboard) => {
dashboard.savedObjectReferences = loadDashboardReturn?.references;
});
// --------------------------------------------------------------------------------------
// Set up unified search integration.
// --------------------------------------------------------------------------------------

View file

@ -6,14 +6,8 @@
* Side Public License, v 1.
*/
import { v4 } from 'uuid';
import { omit } from 'lodash';
import React, { createContext, useContext } from 'react';
import ReactDOM from 'react-dom';
import { batch } from 'react-redux';
import { BehaviorSubject, Subject, Subscription } from 'rxjs';
import { map, distinctUntilChanged } from 'rxjs/operators';
import deepEqual from 'fast-deep-equal';
import { METRIC_TYPE } from '@kbn/analytics';
import { Reference } from '@kbn/content-management-utils';
import type { ControlGroupContainer } from '@kbn/controls-plugin/public';
import type { KibanaExecutionContext, OverlayRef } from '@kbn/core/public';
import { RefreshInterval } from '@kbn/data-plugin/public';
@ -21,9 +15,9 @@ import type { DataView } from '@kbn/data-views-plugin/public';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import {
Container,
DefaultEmbeddableApi,
EmbeddableFactoryNotFoundError,
isExplicitInputWithAttributes,
DefaultEmbeddableApi,
PanelNotFoundError,
ReactEmbeddableParentContext,
reactEmbeddableRegistryHasKey,
@ -33,17 +27,25 @@ import {
type EmbeddableOutput,
type IEmbeddable,
} from '@kbn/embeddable-plugin/public';
import { METRIC_TYPE } from '@kbn/analytics';
import { I18nProvider } from '@kbn/i18n-react';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import { I18nProvider } from '@kbn/i18n-react';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { PanelPackage } from '@kbn/presentation-containers';
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-full-screen';
import deepEqual from 'fast-deep-equal';
import { omit } from 'lodash';
import React, { createContext, useContext } from 'react';
import ReactDOM from 'react-dom';
import { batch } from 'react-redux';
import { BehaviorSubject, Subject, Subscription } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { v4 } from 'uuid';
import { DashboardLocatorParams, DASHBOARD_CONTAINER_TYPE } from '../..';
import { DashboardContainerInput, DashboardPanelState } from '../../../common';
import { getReferencesForPanelId } from '../../../common/dashboard_container/persistable_state/dashboard_container_references';
import { dashboardReplacePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings';
import {
DASHBOARD_APP_ID,
DASHBOARD_LOADED_EVENT,
@ -55,6 +57,7 @@ import { DashboardAnalyticsService } from '../../services/analytics/types';
import { DashboardCapabilitiesService } from '../../services/dashboard_capabilities/types';
import { pluginServices } from '../../services/plugin_services';
import { placePanel } from '../component/panel_placement';
import { panelPlacementStrategies } from '../component/panel_placement/place_new_panel_strategies';
import { DashboardViewport } from '../component/viewport/dashboard_viewport';
import { DashboardExternallyAccessibleApi } from '../external_api/dashboard_api';
import { dashboardContainerReducers } from '../state/dashboard_container_reducers';
@ -80,8 +83,6 @@ import {
dashboardTypeDisplayLowercase,
dashboardTypeDisplayName,
} from './dashboard_container_factory';
import { dashboardReplacePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings';
import { panelPlacementStrategies } from '../component/panel_placement/place_new_panel_strategies';
export interface InheritedChildInput {
filters: Filter[];
@ -160,6 +161,7 @@ export class DashboardContainer
// new embeddable framework
public reactEmbeddableChildren: BehaviorSubject<{ [key: string]: DefaultEmbeddableApi }> =
new BehaviorSubject<{ [key: string]: DefaultEmbeddableApi }>({});
public savedObjectReferences: Reference[] = [];
constructor(
initialInput: DashboardContainerInput,
@ -736,8 +738,8 @@ export class DashboardContainer
} = this.getState();
const panel: DashboardPanelState | undefined = panels[childId];
// TODO Embeddable refactor. References here
return { rawState: panel?.explicitInput, version: panel?.version, references: [] };
const references = getReferencesForPanelId(childId, this.savedObjectReferences);
return { rawState: panel?.explicitInput, version: panel?.version, references };
};
public removePanel(id: string) {

View file

@ -56,7 +56,7 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen
contentManagement,
savedObjectsTagging,
}),
saveDashboardState: ({ currentState, saveOptions, lastSavedId }) =>
saveDashboardState: ({ currentState, saveOptions, lastSavedId, panelReferences }) =>
saveDashboardState({
data,
embeddable,
@ -64,6 +64,7 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen
lastSavedId,
currentState,
notifications,
panelReferences,
dashboardBackup,
contentManagement,
initializerContext,

View file

@ -57,7 +57,12 @@ export const loadDashboardState = async ({
* This is a newly created dashboard, so there is no saved object state to load.
*/
if (!savedObjectId) {
return { dashboardInput: newDashboardState, dashboardFound: true, newDashboardCreated: true };
return {
dashboardInput: newDashboardState,
dashboardFound: true,
newDashboardCreated: true,
references: [],
};
}
/**
@ -97,6 +102,7 @@ export const loadDashboardState = async ({
dashboardInput: newDashboardState,
dashboardFound: false,
dashboardId: savedObjectId,
references: [],
};
}
@ -192,6 +198,7 @@ export const loadDashboardState = async ({
return {
managed,
references,
resolveMeta,
dashboardInput,
anyMigrationRun,

View file

@ -73,6 +73,7 @@ export const saveDashboardState = async ({
lastSavedId,
saveOptions,
currentState,
panelReferences,
dashboardBackup,
contentManagement,
savedObjectsTagging,
@ -180,6 +181,8 @@ export const saveDashboardState = async ({
? savedObjectsTagging.updateTagsReferences(dashboardReferences, tags)
: dashboardReferences;
const allReferences = [...references, ...(panelReferences ?? [])];
/**
* Save the saved object using the content management
*/
@ -191,7 +194,11 @@ export const saveDashboardState = async ({
>({
contentTypeId: DASHBOARD_CONTENT_ID,
data: attributes,
options: { id: idToSaveTo, references, overwrite: true },
options: {
id: idToSaveTo,
references: allReferences,
overwrite: true,
},
});
const newId = result.item.id;
@ -207,12 +214,12 @@ export const saveDashboardState = async ({
*/
if (newId !== lastSavedId) {
dashboardBackup.clearState(lastSavedId);
return { redirectRequired: true, id: newId };
return { redirectRequired: true, id: newId, references: allReferences };
} else {
dashboardContentManagementCache.deleteDashboard(newId); // something changed in an existing dashboard, so delete it from the cache so that it can be re-fetched
}
}
return { id: newId };
return { id: newId, references: allReferences };
} catch (error) {
toasts.addDanger({
title: dashboardSaveToastStrings.getFailureString(currentState.title, error.message),

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { Reference } from '@kbn/content-management-utils';
import { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
import { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public';
@ -74,6 +75,12 @@ export interface LoadDashboardReturn {
resolveMeta?: DashboardResolveMeta;
dashboardInput: SavedDashboardInput;
anyMigrationRun?: boolean;
/**
* Raw references returned directly from the Dashboard saved object. These
* should be provided to the React Embeddable children on deserialize.
*/
references: Reference[];
}
/**
@ -84,12 +91,14 @@ export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { saveAsCopy?: boolea
export interface SaveDashboardProps {
currentState: SavedDashboardInput;
saveOptions: SavedDashboardSaveOpts;
panelReferences?: Reference[];
lastSavedId?: string;
}
export interface SaveDashboardReturn {
id?: string;
error?: string;
references?: Reference[];
redirectRequired?: boolean;
}