Skip to content

Commit

Permalink
[Embeddables Rebuild] Data table example (#181768)
Browse files Browse the repository at this point in the history
Adds a new data table example that shows how embeddables can interact with their siblings
  • Loading branch information
ThomThomson authored Apr 26, 2024
1 parent 13a968a commit 98d2ab2
Show file tree
Hide file tree
Showing 13 changed files with 400 additions and 19 deletions.
5 changes: 3 additions & 2 deletions examples/embeddable_examples/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
"data",
"charts",
"fieldFormats",
"developerExamples"
"developerExamples",
"dataViewFieldEditor"
],
"requiredBundles": ["presentationUtil"]
"requiredBundles": ["presentationUtil", "kibanaUtils", "kibanaReact"]
}
}
29 changes: 20 additions & 9 deletions examples/embeddable_examples/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,28 @@
* Side Public License, v 1.
*/

import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
import {
EmbeddableSetup,
EmbeddableStart,
registerReactEmbeddableFactory,
} 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 { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';

import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { setupApp } from './app/setup_app';
import { DATA_TABLE_ID } from './react_embeddables/data_table/constants';
import { registerCreateDataTableAction } from './react_embeddables/data_table/create_data_table_action';
import { EUI_MARKDOWN_ID } from './react_embeddables/eui_markdown/constants';
import { registerCreateEuiMarkdownAction } from './react_embeddables/eui_markdown/create_eui_markdown_action';
import { FIELD_LIST_ID } from './react_embeddables/field_list/constants';
import { registerCreateFieldListAction } from './react_embeddables/field_list/create_field_list_action';
import { registerAddSearchPanelAction } from './react_embeddables/search/register_add_search_panel_action';
import { registerSearchEmbeddable } from './react_embeddables/search/register_search_embeddable';
import { EUI_MARKDOWN_ID } from './react_embeddables/eui_markdown/constants';
import { FIELD_LIST_ID } from './react_embeddables/field_list/constants';
import { setupApp } from './app/setup_app';

export interface SetupDeps {
developerExamples: DeveloperExamplesSetup;
Expand All @@ -35,6 +37,7 @@ export interface SetupDeps {

export interface StartDeps {
dataViews: DataViewsPublicPluginStart;
dataViewFieldEditor: DataViewFieldEditorStart;
embeddable: EmbeddableStart;
uiActions: UiActionsStart;
data: DataPublicPluginStart;
Expand Down Expand Up @@ -66,6 +69,14 @@ export class EmbeddableExamplesPlugin implements Plugin<void, void, SetupDeps, S

registerAddSearchPanelAction(deps.uiActions);
registerSearchEmbeddable(deps);

registerCreateDataTableAction(deps.uiActions);
registerReactEmbeddableFactory(DATA_TABLE_ID, async () => {
const { getDataTableFactory } = await import(
'./react_embeddables/data_table/data_table_react_embeddable'
);
return getDataTableFactory(core, deps);
});
}

public stop() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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 DATA_TABLE_ID = 'data_table';
export const ADD_DATA_TABLE_ACTION_ID = 'create_data_table';
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { addPanelGrouping } from '../add_panel_grouping';
import { ADD_DATA_TABLE_ACTION_ID, DATA_TABLE_ID } from './constants';

// -----------------------------------------------------------------------------
// Create and register an action which allows this embeddable to be created from
// the dashboard toolbar context menu.
// -----------------------------------------------------------------------------
export const registerCreateDataTableAction = (uiActions: UiActionsStart) => {
uiActions.registerAction<EmbeddableApiContext>({
id: ADD_DATA_TABLE_ACTION_ID,
grouping: [addPanelGrouping],
getIconType: () => 'tableDensityNormal',
isCompatible: async ({ embeddable }) => {
return apiIsPresentationContainer(embeddable);
},
execute: async ({ embeddable }) => {
if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError();
embeddable.addNewPanel(
{
panelType: DATA_TABLE_ID,
},
true
);
},
getDisplayName: () =>
i18n.translate('embeddableExamples.dataTable.ariaLabel', {
defaultMessage: 'Data table',
}),
});
uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_DATA_TABLE_ACTION_ID);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* 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 { DataView } from '@kbn/data-views-plugin/public';
import { buildDataTableRecord } from '@kbn/discover-utils';
import { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types';
import { Filter } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { listenForCompatibleApi } from '@kbn/presentation-containers';
import { apiPublishesDataViews, fetch$ } from '@kbn/presentation-publishing';
import { BehaviorSubject, combineLatest, lastValueFrom, map, Subscription, switchMap } from 'rxjs';
import { StartDeps } from '../../plugin';
import { apiPublishesSelectedFields } from '../field_list/publishes_selected_fields';
import { DataTableApi } from './types';

export const initializeDataTableQueries = async (
services: StartDeps,
api: DataTableApi,
queryLoading$: BehaviorSubject<boolean | undefined>
) => {
// initialize services
const defaultDataViewPromise = services.data.dataViews.getDefault();
const searchSourcePromise = services.data.search.searchSource.create();
const [defaultDataView, searchSource] = await Promise.all([
defaultDataViewPromise,
searchSourcePromise,
]);
if (!defaultDataView) {
throw new Error(
i18n.translate('embeddableExamples.dataTable.noDataViewError', {
defaultMessage: 'At least one data view is required to use the data table example..',
})
);
}

// set up search source
let abortController: AbortController | undefined;
const fields: Record<string, string> = { field: '*', include_unmapped: 'true' };
searchSource.setField('fields', [fields]);
searchSource.setField('size', 50);

// initialize state for API.
const fields$ = new BehaviorSubject<string[]>([]);
const dataView$ = new BehaviorSubject<DataView>(defaultDataView);
const rows$ = new BehaviorSubject<DataTableRecord[] | undefined>([]);

const dataSubscription = new Subscription();

// set up listeners - these will listen for the closest compatible api (parent or sibling)
const stopListeningForDataViews = listenForCompatibleApi(
api.parentApi,
apiPublishesDataViews,
(dataViewProvider) => {
if (!dataViewProvider) {
dataView$.next(defaultDataView);
return;
}
const dataViewSubscription = dataViewProvider.dataViews.subscribe((dataViews) => {
dataView$.next(dataViews?.[0] ?? defaultDataView);
});
return () => dataViewSubscription.unsubscribe();
}
);
const stopListeningForFields = listenForCompatibleApi(
api.parentApi,
apiPublishesSelectedFields,
(fieldsProvider) => {
if (!fieldsProvider) {
fields$.next([]);
return;
}
const fieldsSubscription = fieldsProvider.selectedFields.subscribe((nextFields) => {
fields$.next(nextFields ?? []);
});
return () => fieldsSubscription.unsubscribe();
}
);

// run query whenever the embeddable's fetch state or the data view changes
dataSubscription.add(
combineLatest([fetch$(api), dataView$])
.pipe(
map(([{ filters, timeRange, query, timeslice, searchSessionId }, dataView]) => ({
filters,
timeRange,
query,
dataView,
timeslice,
searchSessionId,
})),
switchMap(async ({ filters, query, timeRange, dataView, timeslice, searchSessionId }) => {
if (!dataView) return;
queryLoading$.next(true);
const appliedTimeRange = timeslice
? {
from: new Date(timeslice[0]).toISOString(),
to: new Date(timeslice[1]).toISOString(),
mode: 'absolute' as 'absolute',
}
: timeRange;
const timeRangeFilter = services.data.query.timefilter.timefilter.createFilter(
dataView,
appliedTimeRange
) as Filter;
searchSource.setField('filter', [...(filters ?? []), timeRangeFilter]);
searchSource.setField('query', query);
searchSource.setField('index', dataView);

abortController?.abort();
abortController = new AbortController();
const { rawResponse: resp } = await lastValueFrom(
searchSource.fetch$({
abortSignal: abortController.signal,
sessionId: searchSessionId,
disableWarningToasts: true,
})
);
queryLoading$.next(false);
return resp.hits.hits.map((hit) => buildDataTableRecord(hit as EsHitRecord, dataView));
})
)
.subscribe((rows) => rows$.next(rows))
);

return {
rows$,
fields$,
dataView$,
stop: () => {
stopListeningForDataViews();
stopListeningForFields();
dataSubscription.unsubscribe();
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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 { EuiScreenReaderOnly } from '@elastic/eui';
import { css } from '@emotion/react';
import { CellActionsProvider } from '@kbn/cell-actions';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import {
initializeTimeRange,
initializeTitles,
useBatchedPublishingSubjects,
} from '@kbn/presentation-publishing';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { DataLoadingState, UnifiedDataTable, UnifiedDataTableProps } from '@kbn/unified-data-table';
import React, { useEffect } from 'react';
import { BehaviorSubject } from 'rxjs';
import { StartDeps } from '../../plugin';
import { DATA_TABLE_ID } from './constants';
import { initializeDataTableQueries } from './data_table_queries';
import { DataTableApi, DataTableSerializedState } from './types';

export const getDataTableFactory = (
core: CoreStart,
services: StartDeps
): ReactEmbeddableFactory<DataTableSerializedState, DataTableApi> => ({
type: DATA_TABLE_ID,
deserializeState: (state) => {
return state.rawState as DataTableSerializedState;
},
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
const storage = new Storage(localStorage);
const timeRange = initializeTimeRange(state);
const queryLoading$ = new BehaviorSubject<boolean | undefined>(true);
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
const allServices: UnifiedDataTableProps['services'] = {
...services,
storage,
theme: core.theme,
uiSettings: core.uiSettings,
toastNotifications: core.notifications.toasts,
};

const api = buildApi(
{
...timeRange.api,
...titlesApi,
dataLoading: queryLoading$,
serializeState: () => {
return {
rawState: { ...serializeTitles(), ...timeRange.serialize() },
};
},
},
{ ...titleComparators, ...timeRange.comparators }
);

const queryService = await initializeDataTableQueries(services, api, queryLoading$);

// Create the React Embeddable component
return {
api,
Component: () => {
// unwrap publishing subjects into reactive state
const [fields, rows, loading, dataView] = useBatchedPublishingSubjects(
queryService.fields$,
queryService.rows$,
queryLoading$,
queryService.dataView$
);

// stop query service on unmount
useEffect(() => {
return () => {
queryService.stop();
};
}, []);

return (
<>
<EuiScreenReaderOnly>
<span id="dataTableReactEmbeddableAria">
{i18n.translate('embeddableExamples.dataTable.ariaLabel', {
defaultMessage: 'Data table',
})}
</span>
</EuiScreenReaderOnly>
<div
css={css`
width: 100%;
`}
>
<KibanaRenderContextProvider theme={core.theme} i18n={core.i18n}>
<KibanaContextProvider services={allServices}>
<CellActionsProvider
getTriggerCompatibleActions={services.uiActions.getTriggerCompatibleActions}
>
<UnifiedDataTable
sort={[]}
rows={rows}
showTimeCol={true}
onFilter={() => {}}
dataView={dataView}
sampleSizeState={100}
columns={fields ?? []}
useNewFieldsApi={true}
services={allServices}
onSetColumns={() => {}}
ariaLabelledBy="dataTableReactEmbeddableAria"
loadingState={loading ? DataLoadingState.loading : DataLoadingState.loaded}
/>
</CellActionsProvider>
</KibanaContextProvider>
</KibanaRenderContextProvider>
</div>
</>
);
},
};
},
});
Loading

0 comments on commit 98d2ab2

Please sign in to comment.