kibana/packages/presentation/presentation_containers/interfaces/presentation_container.ts
Hannah Mudge 36f2ff409f
[Embeddable Rebuild] [Controls] Add control registry + example React control (#182842)
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>
2024-06-05 08:51:37 -06:00

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
)
);
})
);
};