mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[embeddable rebuild][control group] implement PresentationContainer API (#188346)
This commit is contained in:
parent
6137f8185c
commit
f48b5b4b36
5 changed files with 253 additions and 111 deletions
|
@ -8,7 +8,6 @@
|
|||
|
||||
import React, { useEffect } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import {
|
||||
ControlGroupChainingSystem,
|
||||
ControlWidth,
|
||||
|
@ -32,21 +31,20 @@ import {
|
|||
PublishesDataViews,
|
||||
PublishesFilters,
|
||||
PublishesTimeslice,
|
||||
PublishingSubject,
|
||||
useStateFromPublishingSubject,
|
||||
} from '@kbn/presentation-publishing';
|
||||
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { ControlRenderer } from '../control_renderer';
|
||||
import { DefaultControlApi } from '../types';
|
||||
import { openEditControlGroupFlyout } from './open_edit_control_group_flyout';
|
||||
import { deserializeControlGroup, serializeControlGroup } from './serialization_utils';
|
||||
import { deserializeControlGroup } from './serialization_utils';
|
||||
import {
|
||||
ControlGroupApi,
|
||||
ControlGroupRuntimeState,
|
||||
ControlGroupSerializedState,
|
||||
ControlGroupUnsavedChanges,
|
||||
} from './types';
|
||||
import { initControlsManager } from './init_controls_manager';
|
||||
import { controlGroupFetch$, chaining$, controlFetch$ } from './control_fetch';
|
||||
|
||||
export const getControlGroupEmbeddableFactory = (services: {
|
||||
|
@ -62,7 +60,7 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
deserializeState: (state) => deserializeControlGroup(state),
|
||||
buildEmbeddable: async (initialState, buildApi, uuid, parentApi, setApi) => {
|
||||
const {
|
||||
initialChildControlState: childControlState,
|
||||
initialChildControlState,
|
||||
defaultControlGrow,
|
||||
defaultControlWidth,
|
||||
labelPosition,
|
||||
|
@ -71,12 +69,9 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
ignoreParentSettings,
|
||||
} = initialState;
|
||||
|
||||
const controlsManager = initControlsManager(initialChildControlState);
|
||||
const autoApplySelections$ = new BehaviorSubject<boolean>(autoApplySelections);
|
||||
const timeslice$ = new BehaviorSubject<[number, number] | undefined>(undefined);
|
||||
const children$ = new BehaviorSubject<{ [key: string]: DefaultControlApi }>({});
|
||||
function getControlApi(controlUuid: string) {
|
||||
return children$.value[controlUuid];
|
||||
}
|
||||
const filters$ = new BehaviorSubject<Filter[] | undefined>([]);
|
||||
const dataViews = new BehaviorSubject<DataView[] | undefined>(undefined);
|
||||
const chainingSystem$ = new BehaviorSubject<ControlGroupChainingSystem>(chainingSystem);
|
||||
|
@ -108,19 +103,16 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
undefined
|
||||
);
|
||||
|
||||
const controlsInOrder$ = new BehaviorSubject<Array<{ id: string; type: string }>>(
|
||||
Object.keys(childControlState)
|
||||
.map((key) => ({
|
||||
id: key,
|
||||
order: childControlState[key].order,
|
||||
type: childControlState[key].type,
|
||||
}))
|
||||
.sort((a, b) => (a.order > b.order ? 1 : -1))
|
||||
);
|
||||
const api = setApi({
|
||||
...controlsManager.api,
|
||||
controlFetch$: (controlUuid: string) =>
|
||||
controlFetch$(
|
||||
chaining$(controlUuid, chainingSystem$, controlsInOrder$, getControlApi),
|
||||
chaining$(
|
||||
controlUuid,
|
||||
chainingSystem$,
|
||||
controlsManager.controlsInOrder$,
|
||||
controlsManager.getControlApi
|
||||
),
|
||||
controlGroupFetch$(ignoreParentSettings$, parentApi ? parentApi : {})
|
||||
),
|
||||
ignoreParentSettings$,
|
||||
|
@ -134,9 +126,6 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
return {} as unknown as ControlGroupRuntimeState;
|
||||
},
|
||||
dataLoading: dataLoading$,
|
||||
children$: children$ as PublishingSubject<{
|
||||
[key: string]: unknown;
|
||||
}>,
|
||||
onEdit: async () => {
|
||||
openEditControlGroupFlyout(
|
||||
api,
|
||||
|
@ -154,34 +143,18 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
i18n.translate('controls.controlGroup.displayName', {
|
||||
defaultMessage: 'Controls',
|
||||
}),
|
||||
getSerializedStateForChild: (childId) => {
|
||||
return { rawState: childControlState[childId] };
|
||||
},
|
||||
serializeState: () => {
|
||||
return serializeControlGroup(
|
||||
children$.getValue(),
|
||||
controlsInOrder$.getValue().map(({ id }) => id),
|
||||
{
|
||||
labelPosition: labelPosition$.getValue(),
|
||||
const { panelsJSON, references } = controlsManager.serializeControls();
|
||||
return {
|
||||
rawState: {
|
||||
chainingSystem: chainingSystem$.getValue(),
|
||||
autoApplySelections: autoApplySelections$.getValue(),
|
||||
ignoreParentSettings: ignoreParentSettings$.getValue(),
|
||||
}
|
||||
);
|
||||
},
|
||||
getPanelCount: () => {
|
||||
return (Object.keys(children$.getValue()) ?? []).length;
|
||||
},
|
||||
addNewPanel: (panel) => {
|
||||
// TODO: Add a new child control
|
||||
return Promise.resolve(undefined);
|
||||
},
|
||||
removePanel: (panelId) => {
|
||||
// TODO: Remove a child control
|
||||
},
|
||||
replacePanel: async (panelId, newPanel) => {
|
||||
// TODO: Replace a child control
|
||||
return Promise.resolve(panelId);
|
||||
controlStyle: labelPosition$.getValue(), // Rename "labelPosition" to "controlStyle"
|
||||
showApplySelections: !autoApplySelections$.getValue(),
|
||||
ignoreParentSettingsJSON: JSON.stringify(ignoreParentSettings$.getValue()),
|
||||
panelsJSON,
|
||||
},
|
||||
references,
|
||||
};
|
||||
},
|
||||
grow,
|
||||
width,
|
||||
|
@ -238,7 +211,7 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
return {
|
||||
api,
|
||||
Component: () => {
|
||||
const controlsInOrder = useStateFromPublishingSubject(controlsInOrder$);
|
||||
const controlsInOrder = useStateFromPublishingSubject(controlsManager.controlsInOrder$);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
@ -253,14 +226,11 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
{controlsInOrder.map(({ id, type }) => (
|
||||
<ControlRenderer
|
||||
key={id}
|
||||
maybeId={id}
|
||||
uuid={id}
|
||||
type={type}
|
||||
getParentApi={() => api}
|
||||
onApiAvailable={(controlApi) => {
|
||||
children$.next({
|
||||
...children$.getValue(),
|
||||
[id]: controlApi,
|
||||
});
|
||||
controlsManager.setControlApi(id, controlApi);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { DefaultControlApi } from '../types';
|
||||
import { initControlsManager } from './init_controls_manager';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('delta'),
|
||||
}));
|
||||
|
||||
describe('PresentationContainer api', () => {
|
||||
test('addNewPanel should add control at end of controls', async () => {
|
||||
const controlsManager = initControlsManager({
|
||||
alpha: { type: 'whatever', order: 0 },
|
||||
bravo: { type: 'whatever', order: 1 },
|
||||
charlie: { type: 'whatever', order: 2 },
|
||||
});
|
||||
const addNewPanelPromise = controlsManager.api.addNewPanel({
|
||||
panelType: 'whatever',
|
||||
initialState: {},
|
||||
});
|
||||
controlsManager.setControlApi('delta', {} as unknown as DefaultControlApi);
|
||||
await addNewPanelPromise;
|
||||
expect(controlsManager.controlsInOrder$.value.map((element) => element.id)).toEqual([
|
||||
'alpha',
|
||||
'bravo',
|
||||
'charlie',
|
||||
'delta',
|
||||
]);
|
||||
});
|
||||
|
||||
test('removePanel should remove control', () => {
|
||||
const controlsManager = initControlsManager({
|
||||
alpha: { type: 'whatever', order: 0 },
|
||||
bravo: { type: 'whatever', order: 1 },
|
||||
charlie: { type: 'whatever', order: 2 },
|
||||
});
|
||||
controlsManager.api.removePanel('bravo');
|
||||
expect(controlsManager.controlsInOrder$.value.map((element) => element.id)).toEqual([
|
||||
'alpha',
|
||||
'charlie',
|
||||
]);
|
||||
});
|
||||
|
||||
test('replacePanel should replace control', async () => {
|
||||
const controlsManager = initControlsManager({
|
||||
alpha: { type: 'whatever', order: 0 },
|
||||
bravo: { type: 'whatever', order: 1 },
|
||||
charlie: { type: 'whatever', order: 2 },
|
||||
});
|
||||
const replacePanelPromise = controlsManager.api.replacePanel('bravo', {
|
||||
panelType: 'whatever',
|
||||
initialState: {},
|
||||
});
|
||||
controlsManager.setControlApi('delta', {} as unknown as DefaultControlApi);
|
||||
await replacePanelPromise;
|
||||
expect(controlsManager.controlsInOrder$.value.map((element) => element.id)).toEqual([
|
||||
'alpha',
|
||||
'delta',
|
||||
'charlie',
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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 { v4 as generateId } from 'uuid';
|
||||
import {
|
||||
HasSerializedChildState,
|
||||
PanelPackage,
|
||||
PresentationContainer,
|
||||
} from '@kbn/presentation-containers';
|
||||
import { Reference } from '@kbn/content-management-utils';
|
||||
import { BehaviorSubject, merge } from 'rxjs';
|
||||
import { PublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { omit } from 'lodash';
|
||||
import { ControlPanelsState, ControlPanelState } from './types';
|
||||
import { DefaultControlApi, DefaultControlState } from '../types';
|
||||
|
||||
export function initControlsManager(initialControlPanelsState: ControlPanelsState) {
|
||||
const children$ = new BehaviorSubject<{ [key: string]: DefaultControlApi }>({});
|
||||
const controlsPanelState: { [panelId: string]: DefaultControlState } = {
|
||||
...initialControlPanelsState,
|
||||
};
|
||||
const controlsInOrder$ = new BehaviorSubject<Array<{ id: string; type: string }>>(
|
||||
Object.keys(initialControlPanelsState)
|
||||
.map((key) => ({
|
||||
id: key,
|
||||
order: initialControlPanelsState[key].order,
|
||||
type: initialControlPanelsState[key].type,
|
||||
}))
|
||||
.sort((a, b) => (a.order > b.order ? 1 : -1))
|
||||
);
|
||||
|
||||
function untilControlLoaded(
|
||||
id: string
|
||||
): DefaultControlApi | Promise<DefaultControlApi | undefined> {
|
||||
if (children$.value[id]) {
|
||||
return children$.value[id];
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const subscription = merge(children$, controlsInOrder$).subscribe(() => {
|
||||
if (children$.value[id]) {
|
||||
subscription.unsubscribe();
|
||||
resolve(children$.value[id]);
|
||||
return;
|
||||
}
|
||||
|
||||
// control removed before the control finished loading.
|
||||
const controlState = controlsInOrder$.value.find((element) => element.id === id);
|
||||
if (!controlState) {
|
||||
subscription.unsubscribe();
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getControlApi(controlUuid: string) {
|
||||
return children$.value[controlUuid];
|
||||
}
|
||||
|
||||
async function addNewPanel(
|
||||
{ panelType, initialState }: PanelPackage<DefaultControlState>,
|
||||
index: number
|
||||
) {
|
||||
const id = generateId();
|
||||
const nextControlsInOrder = [...controlsInOrder$.value];
|
||||
nextControlsInOrder.splice(index, 0, {
|
||||
id,
|
||||
type: panelType,
|
||||
});
|
||||
controlsInOrder$.next(nextControlsInOrder);
|
||||
controlsPanelState[id] = initialState ?? {};
|
||||
return await untilControlLoaded(id);
|
||||
}
|
||||
|
||||
function removePanel(panelId: string) {
|
||||
delete controlsPanelState[panelId];
|
||||
controlsInOrder$.next(controlsInOrder$.value.filter(({ id }) => id !== panelId));
|
||||
children$.next(omit(children$.value, panelId));
|
||||
}
|
||||
|
||||
return {
|
||||
controlsInOrder$: controlsInOrder$ as PublishingSubject<Array<{ id: string; type: string }>>,
|
||||
getControlApi,
|
||||
setControlApi: (uuid: string, controlApi: DefaultControlApi) => {
|
||||
children$.next({
|
||||
...children$.getValue(),
|
||||
[uuid]: controlApi,
|
||||
});
|
||||
},
|
||||
serializeControls: () => {
|
||||
const references: Reference[] = [];
|
||||
const explicitInputPanels: {
|
||||
[panelId: string]: ControlPanelState & { explicitInput: object };
|
||||
} = {};
|
||||
|
||||
controlsInOrder$.getValue().forEach(({ id }, index) => {
|
||||
const controlApi = getControlApi(id);
|
||||
if (!controlApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
rawState: { grow, width, ...rest },
|
||||
references: controlReferences,
|
||||
} = controlApi.serializeState();
|
||||
|
||||
if (controlReferences && controlReferences.length > 0) {
|
||||
references.push(...controlReferences);
|
||||
}
|
||||
|
||||
explicitInputPanels[id] = {
|
||||
grow,
|
||||
order: index,
|
||||
type: controlApi.type,
|
||||
width,
|
||||
/** Re-add the `explicitInput` layer on serialize so control group saved object retains shape */
|
||||
explicitInput: rest,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
panelsJSON: JSON.stringify(explicitInputPanels),
|
||||
references,
|
||||
};
|
||||
},
|
||||
api: {
|
||||
getSerializedStateForChild: (childId: string) => {
|
||||
const controlPanelState = controlsPanelState[childId];
|
||||
return controlPanelState ? { rawState: controlPanelState } : undefined;
|
||||
},
|
||||
children$: children$ as PublishingSubject<{
|
||||
[key: string]: DefaultControlApi;
|
||||
}>,
|
||||
getPanelCount: () => {
|
||||
return controlsInOrder$.value.length;
|
||||
},
|
||||
addNewPanel: async (panel: PanelPackage<DefaultControlState>) => {
|
||||
return addNewPanel(panel, controlsInOrder$.value.length);
|
||||
},
|
||||
removePanel,
|
||||
replacePanel: async (panelId, newPanel) => {
|
||||
const index = controlsInOrder$.value.findIndex(({ id }) => id === panelId);
|
||||
removePanel(panelId);
|
||||
const controlApi = await addNewPanel(
|
||||
newPanel,
|
||||
index >= 0 ? index : controlsInOrder$.value.length
|
||||
);
|
||||
return controlApi ? controlApi.uuid : '';
|
||||
},
|
||||
} as PresentationContainer & HasSerializedChildState<ControlPanelState>,
|
||||
};
|
||||
}
|
|
@ -6,11 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Reference } from '@kbn/content-management-utils';
|
||||
import { DEFAULT_CONTROL_GROW, DEFAULT_CONTROL_WIDTH } from '@kbn/controls-plugin/common';
|
||||
import { SerializedPanelState } from '@kbn/presentation-containers';
|
||||
import { omit } from 'lodash';
|
||||
import { DefaultControlApi } from '../types';
|
||||
import { ControlGroupRuntimeState, ControlGroupSerializedState } from './types';
|
||||
|
||||
export const deserializeControlGroup = (
|
||||
|
@ -46,59 +44,9 @@ export const deserializeControlGroup = (
|
|||
autoApplySelections:
|
||||
typeof state.rawState.showApplySelections === 'boolean'
|
||||
? !state.rawState.showApplySelections
|
||||
: false,
|
||||
: false, // Rename "showApplySelections" to "autoApplySelections"
|
||||
labelPosition: state.rawState.controlStyle, // Rename "controlStyle" to "labelPosition"
|
||||
defaultControlGrow: DEFAULT_CONTROL_GROW,
|
||||
defaultControlWidth: DEFAULT_CONTROL_WIDTH,
|
||||
};
|
||||
};
|
||||
|
||||
export const serializeControlGroup = (
|
||||
children: {
|
||||
[key: string]: DefaultControlApi;
|
||||
},
|
||||
idsInOrder: string[],
|
||||
state: Omit<
|
||||
ControlGroupRuntimeState,
|
||||
| 'anyChildHasUnsavedChanges'
|
||||
| 'defaultControlGrow'
|
||||
| 'defaultControlWidth'
|
||||
| 'initialChildControlState'
|
||||
>
|
||||
): SerializedPanelState<ControlGroupSerializedState> => {
|
||||
let references: Reference[] = [];
|
||||
|
||||
/** Re-add the `explicitInput` layer on serialize so control group saved object retains shape */
|
||||
const explicitInputPanels = Object.keys(children).reduce((prev, panelId) => {
|
||||
const child: DefaultControlApi = children[panelId];
|
||||
const type = child.type;
|
||||
const {
|
||||
rawState: { grow, width, ...rest },
|
||||
references: childReferences,
|
||||
} = child.serializeState();
|
||||
|
||||
if (childReferences && childReferences.length > 0) {
|
||||
references = [...references, ...childReferences];
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: With legacy control embeddables, `grow` and `width` were duplicated under
|
||||
* explicit input - this is no longer the case.
|
||||
*/
|
||||
return {
|
||||
...prev,
|
||||
[panelId]: { grow, order: idsInOrder.indexOf(panelId), type, width, explicitInput: rest },
|
||||
};
|
||||
}, {});
|
||||
|
||||
return {
|
||||
rawState: {
|
||||
...omit(state, ['ignoreParentSettings', 'labelPosition']),
|
||||
controlStyle: state.labelPosition, // Rename "labelPosition" to "controlStyle"
|
||||
showApplySelections: !state.autoApplySelections,
|
||||
ignoreParentSettingsJSON: JSON.stringify(state.ignoreParentSettings),
|
||||
panelsJSON: JSON.stringify(explicitInputPanels),
|
||||
},
|
||||
references,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import React, { useImperativeHandle, useMemo } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { v4 as generateId } from 'uuid';
|
||||
|
||||
import { StateComparators } from '@kbn/presentation-publishing';
|
||||
|
||||
|
@ -25,12 +24,12 @@ export const ControlRenderer = <
|
|||
ApiType extends DefaultControlApi = DefaultControlApi
|
||||
>({
|
||||
type,
|
||||
maybeId,
|
||||
uuid,
|
||||
getParentApi,
|
||||
onApiAvailable,
|
||||
}: {
|
||||
type: string;
|
||||
maybeId?: string;
|
||||
uuid: string;
|
||||
getParentApi: () => ControlGroupApi;
|
||||
onApiAvailable?: (api: ApiType) => void;
|
||||
}) => {
|
||||
|
@ -38,7 +37,6 @@ export const ControlRenderer = <
|
|||
() =>
|
||||
(() => {
|
||||
const parentApi = getParentApi();
|
||||
const uuid = maybeId ?? generateId();
|
||||
const factory = getControlFactory<StateType, ApiType>(type);
|
||||
|
||||
const buildApi = (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue