Skip to content

Commit

Permalink
[Embeddable rebuild] Allow Dashboard to provide references (#176455)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ThomThomson authored Feb 12, 2024
1 parent 06ecb5a commit 6827db4
Show file tree
Hide file tree
Showing 18 changed files with 443 additions and 81 deletions.
9 changes: 6 additions & 3 deletions examples/embeddable_examples/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
20 changes: 17 additions & 3 deletions examples/embeddable_examples/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,14 +37,21 @@ 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;
uiActions: UiActionsStart;
}

export interface EmbeddableExamplesStartDependencies {
dataViews: DataViewsPublicPluginStart;
embeddable: EmbeddableStart;
uiActions: UiActionsStart;
data: DataPublicPluginStart;
charts: ChartsPluginStart;
fieldFormats: FieldFormatsStart;
}

interface ExampleEmbeddableFactories {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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);
};
Original file line number Diff line number Diff line change
@@ -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);
};
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 9 additions & 1 deletion examples/embeddable_examples/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
Loading

0 comments on commit 6827db4

Please sign in to comment.