mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
Closes https://github.com/elastic/kibana/issues/184373 ## Summary This PR marks the first step of the control group migration to the new React embeddable system. A few notes about this: - In the new system, each individual control will no longer be an "embeddable" - instead, we are creating a **new** control-specific registry for all controls. This is **modelled** after the embeddable registry, but it is locked down and much more controls-specific. - Most of the work accomplished in this PR is hidden away in the `examples` plugin - that way, user-facing code is not impacted. After some discussion, we decided to do it this way because refactoring the control group to work with both legacy and new controls (like we did for the dashboard container) felt like a very large undertaking for minimal benefit. Instead, all work will be contained in the example plugin (including building out the existing control types with the new framework) and we will do a final "swap" of the legacy control group with the new React control group as part of https://github.com/elastic/kibana/issues/174961 - This PR does **not** contain a fully functional control group embeddable - instead, the main point of this PR is to introduce the control registry and an example control. The current control group embeddable is provided just to give the **bare minimum** of functionality. - In order to find the new Search control example, navigate to Developer Examples > Controls > Register a new React control - The example search control only works on text fields. See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html and https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html for information on the two search techniques. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
/*
|
|
* 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 { apiHasParentApi, apiHasUniqueId, PublishingSubject } from '@kbn/presentation-publishing';
|
|
import { BehaviorSubject, combineLatest, isObservable, map, Observable, of, switchMap } from 'rxjs';
|
|
import { apiCanAddNewPanel, CanAddNewPanel } from './can_add_new_panel';
|
|
|
|
export interface PanelPackage<SerializedState extends object = object> {
|
|
panelType: string;
|
|
initialState?: SerializedState;
|
|
}
|
|
|
|
export interface PresentationContainer extends CanAddNewPanel {
|
|
/**
|
|
* Removes a panel from the container.
|
|
*/
|
|
removePanel: (panelId: string) => void;
|
|
|
|
/**
|
|
* Determines whether or not a container is capable of removing panels.
|
|
*/
|
|
canRemovePanels?: () => boolean;
|
|
|
|
/**
|
|
* Replaces a panel in the container with a new panel.
|
|
*/
|
|
replacePanel: <SerializedState extends object = object>(
|
|
idToRemove: string,
|
|
newPanel: PanelPackage<SerializedState>
|
|
) => Promise<string>;
|
|
|
|
/**
|
|
* Returns the number of panels in the container.
|
|
*/
|
|
getPanelCount: () => number;
|
|
|
|
/**
|
|
* A publishing subject containing the child APIs of the container. Note that
|
|
* children are created asynchronously. This means that the children$ observable might
|
|
* contain fewer children than the actual number of panels in the container.
|
|
*/
|
|
children$: PublishingSubject<{ [key: string]: unknown }>;
|
|
}
|
|
|
|
export const apiIsPresentationContainer = (api: unknown | null): api is PresentationContainer => {
|
|
return Boolean(
|
|
apiCanAddNewPanel(api) &&
|
|
typeof (api as PresentationContainer)?.removePanel === 'function' &&
|
|
typeof (api as PresentationContainer)?.replacePanel === 'function' &&
|
|
typeof (api as PresentationContainer)?.addNewPanel === 'function' &&
|
|
(api as PresentationContainer)?.children$
|
|
);
|
|
};
|
|
|
|
export const getContainerParentFromAPI = (
|
|
api: null | unknown
|
|
): PresentationContainer | undefined => {
|
|
const apiParent = apiHasParentApi(api) ? api.parentApi : null;
|
|
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?.();
|
|
};
|
|
};
|
|
|
|
export const combineCompatibleChildrenApis = <ApiType extends unknown, PublishingSubjectType>(
|
|
api: unknown,
|
|
observableKey: keyof ApiType,
|
|
isCompatible: (api: unknown) => api is ApiType,
|
|
emptyState: PublishingSubjectType,
|
|
flattenMethod?: (array: PublishingSubjectType[]) => PublishingSubjectType
|
|
): Observable<PublishingSubjectType> => {
|
|
if (!api || !apiIsPresentationContainer(api)) return of();
|
|
|
|
return api.children$.pipe(
|
|
switchMap((children) => {
|
|
const compatibleChildren: Array<Observable<PublishingSubjectType>> = [];
|
|
for (const child of Object.values(children)) {
|
|
if (isCompatible(child) && isObservable(child[observableKey]))
|
|
compatibleChildren.push(child[observableKey] as BehaviorSubject<PublishingSubjectType>);
|
|
}
|
|
|
|
if (compatibleChildren.length === 0) return of(emptyState);
|
|
|
|
return combineLatest(compatibleChildren).pipe(
|
|
map(
|
|
flattenMethod
|
|
? flattenMethod
|
|
: (nextCompatible) =>
|
|
nextCompatible.flat().filter((value) => Boolean(value)) as PublishingSubjectType
|
|
)
|
|
);
|
|
})
|
|
);
|
|
};
|