Skip to content

Commit

Permalink
[Embeddables Rebuild] Clean up parenting structure (#179769)
Browse files Browse the repository at this point in the history
Significantly cleans up the PresentationContainer interface by adding a `children$` publishing subject.
  • Loading branch information
ThomThomson authored Apr 5, 2024
1 parent e5adbbd commit 3a1936f
Show file tree
Hide file tree
Showing 29 changed files with 294 additions and 300 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
*/

import { i18n } from '@kbn/i18n';
import { EmbeddableApiContext, apiCanAddNewPanel } from '@kbn/presentation-publishing';
import { apiCanAddNewPanel } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { ADD_EUI_MARKDOWN_ACTION_ID, EUI_MARKDOWN_ID } from './constants';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
*/

import { i18n } from '@kbn/i18n';
import { apiCanAddNewPanel, EmbeddableApiContext } from '@kbn/presentation-publishing';
import { apiCanAddNewPanel } 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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import {
type UnifiedFieldListSidebarContainerProps,
} from '@kbn/unified-field-list';
import { cloneDeep } from 'lodash';
import React, { useEffect, useState } from 'react';
import { BehaviorSubject, Subscription } from 'rxjs';
import React, { useEffect } from 'react';
import { BehaviorSubject, skip, Subscription, switchMap } from 'rxjs';
import { FIELD_LIST_DATA_VIEW_REF_NAME, FIELD_LIST_ID } from './constants';
import { FieldListApi, FieldListSerializedStateState } from './types';

Expand Down Expand Up @@ -81,20 +81,32 @@ export const getFieldListFactory = (
const subscriptions = new Subscription();
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(initialState);

const allDataViews = await dataViews.getIdsWithTitle();
const selectedDataViewId$ = new BehaviorSubject<string | undefined>(
initialState.dataViewId ?? (await dataViews.getDefaultDataView())?.id
);
// set up data views
const [allDataViews, defaultDataViewId] = await Promise.all([
dataViews.getIdsWithTitle(),
dataViews.getDefaultId(),
]);
if (!defaultDataViewId || allDataViews.length === 0) {
throw new Error(
i18n.translate('embeddableExamples.unifiedFieldList.noDefaultDataViewErrorMessage', {
defaultMessage: 'The field list must be used with at least one Data View present',
})
);
}
const initialDataViewId = initialState.dataViewId ?? defaultDataViewId;
const initialDataView = await dataViews.get(initialDataViewId);
const selectedDataViewId$ = new BehaviorSubject<string | undefined>(initialDataViewId);
const dataViews$ = new BehaviorSubject<DataView[] | undefined>([initialDataView]);

// transform data view ID into data views array.
const getDataViews = async (id?: string) => {
return id ? [await dataViews.get(id)] : undefined;
};
const dataViews$ = new BehaviorSubject<DataView[] | undefined>(
await getDataViews(initialState.dataViewId)
);
subscriptions.add(
selectedDataViewId$.subscribe(async (id) => dataViews$.next(await getDataViews(id)))
selectedDataViewId$
.pipe(
skip(1),
switchMap((dataViewId) => dataViews.get(dataViewId ?? defaultDataViewId))
)
.subscribe((nextSelectedDataView) => {
dataViews$.next([nextSelectedDataView]);
})
);

const selectedFieldNames$ = new BehaviorSubject<string[] | undefined>(
Expand All @@ -104,6 +116,7 @@ export const getFieldListFactory = (
const api = buildApi(
{
...titlesApi,
dataViews: dataViews$,
serializeState: () => {
const dataViewId = selectedDataViewId$.getValue();
const references: Reference[] = dataViewId
Expand Down Expand Up @@ -141,25 +154,12 @@ export const getFieldListFactory = (
return {
api,
Component: () => {
const [selectedDataViewId, selectedFieldNames] = useBatchedPublishingSubjects(
selectedDataViewId$,
const [renderDataViews, selectedFieldNames] = useBatchedPublishingSubjects(
dataViews$,
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]);
const selectedDataView = renderDataViews?.[0];

// On destroy
useEffect(() => {
Expand All @@ -178,7 +178,7 @@ export const getFieldListFactory = (
>
<DataViewPicker
dataViews={allDataViews}
selectedDataViewId={selectedDataViewId}
selectedDataViewId={selectedDataView?.id}
onChangeDataViewId={(nextSelection) => {
selectedDataViewId$.next(nextSelection);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
*/

import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
import { SerializedTitles } from '@kbn/presentation-publishing';
import { PublishesDataViews, SerializedTitles } from '@kbn/presentation-publishing';

export type FieldListSerializedStateState = SerializedTitles & {
dataViewId?: string;
selectedFieldNames?: string[];
};

export type FieldListApi = DefaultEmbeddableApi;
export type FieldListApi = DefaultEmbeddableApi & PublishesDataViews;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* Side Public License, v 1.
*/

import { apiCanAddNewPanel, EmbeddableApiContext } from '@kbn/presentation-publishing';
import { apiCanAddNewPanel } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { ADD_SEARCH_ACTION_ID, SEARCH_EMBEDDABLE_ID } from './constants';

Expand Down
3 changes: 2 additions & 1 deletion examples/embeddable_examples/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@kbn/content-management-utils",
"@kbn/core-lifecycle-browser",
"@kbn/presentation-util-plugin",
"@kbn/unified-field-list"
"@kbn/unified-field-list",
"@kbn/presentation-containers"
]
}
19 changes: 11 additions & 8 deletions packages/presentation/presentation_containers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
* Side Public License, v 1.
*/

export { apiCanAddNewPanel, type CanAddNewPanel } from './interfaces/can_add_new_panel';
export {
apiPublishesLastSavedState,
getLastSavedStateSubjectForChild,
type PublishesLastSavedState,
} from './interfaces/last_saved_state';
export {
apiCanDuplicatePanels,
apiCanExpandPanels,
Expand All @@ -15,16 +21,13 @@ export {
export {
apiIsPresentationContainer,
getContainerParentFromAPI,
listenForCompatibleApi,
type PanelPackage,
type PresentationContainer,
} from './interfaces/presentation_container';
export { tracksOverlays, type TracksOverlays } from './interfaces/tracks_overlays';
export {
type SerializedPanelState,
type HasSerializableState,
apiHasSerializableState,
type HasSerializableState,
type SerializedPanelState,
} from './interfaces/serialized_state';
export {
type PublishesLastSavedState,
apiPublishesLastSavedState,
getLastSavedStateSubjectForChild,
} from './interfaces/last_saved_state';
export { tracksOverlays, type TracksOverlays } from './interfaces/tracks_overlays';
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
* Side Public License, v 1.
*/

export interface PanelPackage {
panelType: string;
initialState?: object;
}
import { PanelPackage } from './presentation_container';

/**
* This API can add a new panel as a child.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,37 @@
*/

import {
apiCanAddNewPanel,
apiHasParentApi,
CanAddNewPanel,
PanelPackage,
apiHasUniqueId,
PublishesViewMode,
PublishingSubject,
} from '@kbn/presentation-publishing';
import { PublishesLastSavedState } from './last_saved_state';

export type PresentationContainer = Partial<PublishesViewMode> &
PublishesLastSavedState &
CanAddNewPanel & {
registerPanelApi: <ApiType extends unknown = unknown>(
panelId: string,
panelApi: ApiType
) => void;
removePanel: (panelId: string) => void;
canRemovePanels?: () => boolean;
replacePanel: (idToRemove: string, newPanel: PanelPackage) => Promise<string>;
getChildIds: () => string[];
getChild: (childId: string) => unknown;
};
import { apiCanAddNewPanel, CanAddNewPanel } from './can_add_new_panel';

export interface PanelPackage {
panelType: string;
initialState?: object;
}

export interface PresentationContainer extends Partial<PublishesViewMode>, CanAddNewPanel {
addNewPanel: <ApiType extends unknown = unknown>(
panel: PanelPackage,
displaySuccessMessage?: boolean
) => Promise<ApiType | undefined>;
removePanel: (panelId: string) => void;
canRemovePanels?: () => boolean;
replacePanel: (idToRemove: string, newPanel: PanelPackage) => Promise<string>;

children$: PublishingSubject<{ [key: string]: unknown }>;
}

export const apiIsPresentationContainer = (api: unknown | null): api is PresentationContainer => {
return (
return Boolean(
apiCanAddNewPanel(api) &&
Boolean(
typeof (api as PresentationContainer)?.removePanel === 'function' &&
typeof (api as PresentationContainer)?.registerPanelApi === 'function' &&
typeof (api as PresentationContainer)?.replacePanel === 'function' &&
typeof (api as PresentationContainer)?.getChildIds === 'function' &&
typeof (api as PresentationContainer)?.getChild === 'function'
)
typeof (api as PresentationContainer)?.replacePanel === 'function' &&
typeof (api as PresentationContainer)?.addNewPanel === 'function' &&
(api as PresentationContainer)?.children$
);
};

Expand All @@ -49,3 +48,33 @@ export const getContainerParentFromAPI = (
if (!apiParent) return undefined;
return apiIsPresentationContainer(apiParent) ? apiParent : undefined;
};

export const listenForCompatibleApi = <ApiType extends unknown>(
parent: unknown,
isCompatible: (api: unknown) => api is ApiType,
apiFound: (api: ApiType | undefined) => (() => void) | void
) => {
if (!parent || !apiIsPresentationContainer(parent)) return () => {};

let lastCleanupFunction: (() => void) | undefined;
let lastCompatibleUuid: string | null;
const subscription = parent.children$.subscribe((children) => {
lastCleanupFunction?.();
const compatibleApi = (() => {
for (const childId of Object.keys(children)) {
const child = children[childId];
if (isCompatible(child)) return child;
}
if (isCompatible(parent)) return parent;
return undefined;
})();
const nextId = apiHasUniqueId(compatibleApi) ? compatibleApi.uuid : null;
if (nextId === lastCompatibleUuid) return;
lastCompatibleUuid = nextId;
lastCleanupFunction = apiFound(compatibleApi) ?? undefined;
});
return () => {
subscription.unsubscribe();
lastCleanupFunction?.();
};
};
8 changes: 2 additions & 6 deletions packages/presentation/presentation_containers/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,14 @@
* Side Public License, v 1.
*/

import { Subject } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { PresentationContainer } from './interfaces/presentation_container';

export const getMockPresentationContainer = (): PresentationContainer => {
return {
removePanel: jest.fn(),
addNewPanel: jest.fn(),
replacePanel: jest.fn(),
registerPanelApi: jest.fn(),
lastSavedState: new Subject<void>(),
getLastSavedStateForChild: jest.fn(),
getChildIds: jest.fn(),
getChild: jest.fn(),
children$: new BehaviorSubject<{ [key: string]: unknown }>({}),
};
};
43 changes: 19 additions & 24 deletions packages/presentation/presentation_publishing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,28 @@ export {
useInheritedViewMode,
type CanAccessViewMode,
} from './interfaces/can_access_view_mode';
export { initializeTimeRange } from './interfaces/fetch/initialize_time_range';
export {
onFetchContextChanged,
type FetchContext,
} from './interfaces/fetch/on_fetch_context_changed';
export {
apiCanAddNewPanel,
type CanAddNewPanel,
type PanelPackage,
} from './interfaces/can_add_new_panel';
apiPublishesPartialUnifiedSearch,
apiPublishesTimeRange,
apiPublishesUnifiedSearch,
apiPublishesWritableUnifiedSearch,
type PublishesTimeRange,
type PublishesUnifiedSearch,
type PublishesWritableUnifiedSearch,
} from './interfaces/fetch/publishes_unified_search';
export { apiHasDisableTriggers, type HasDisableTriggers } from './interfaces/has_disable_triggers';
export { hasEditCapabilities, type HasEditCapabilities } from './interfaces/has_edit_capabilities';
export {
apiHasLegacyLibraryTransforms,
apiHasLibraryTransforms,
type HasLegacyLibraryTransforms,
type HasLibraryTransforms,
} from './interfaces/has_library_transforms';
export { apiHasParentApi, type HasParentApi } from './interfaces/has_parent_api';
export {
apiHasSupportedTriggers,
Expand Down Expand Up @@ -66,20 +81,6 @@ export {
type PhaseEventType,
type PublishesPhaseEvents,
} from './interfaces/publishes_phase_events';
export {
apiPublishesTimeRange,
apiPublishesUnifiedSearch,
apiPublishesPartialUnifiedSearch,
apiPublishesWritableUnifiedSearch,
type PublishesTimeRange,
type PublishesUnifiedSearch,
type PublishesWritableUnifiedSearch,
} from './interfaces/fetch/publishes_unified_search';
export { initializeTimeRange } from './interfaces/fetch/initialize_time_range';
export {
type FetchContext,
onFetchContextChanged,
} from './interfaces/fetch/on_fetch_context_changed';
export {
apiPublishesSavedObjectId,
type PublishesSavedObjectId,
Expand Down Expand Up @@ -109,12 +110,6 @@ export {
type PublishesWritablePanelTitle,
} from './interfaces/titles/publishes_panel_title';
export { initializeTitles, type SerializedTitles } from './interfaces/titles/titles_api';
export {
type HasLibraryTransforms,
apiHasLibraryTransforms,
type HasLegacyLibraryTransforms,
apiHasLegacyLibraryTransforms,
} from './interfaces/has_library_transforms';
export {
useBatchedPublishingSubjects,
usePublishingSubject,
Expand Down
Loading

0 comments on commit 3a1936f

Please sign in to comment.