mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
06ecb5aee9
commit
6827db46d5
18 changed files with 445 additions and 83 deletions
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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;
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,6 @@
|
|||
"kbn_references": [
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/core-mount-utils-browser",
|
||||
"@kbn/core-saved-objects-api-server",
|
||||
"@kbn/content-management-utils",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
? {
|
||||
|
|
|
@ -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.
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue