Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into task/avc-banner-pac…
Browse files Browse the repository at this point in the history
…kage
  • Loading branch information
parkiino committed Jul 17, 2024
2 parents 55d5113 + f48b5b4 commit 994d4df
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import React, { useEffect } from 'react';
import { BehaviorSubject } from 'rxjs';

import {
ControlGroupChainingSystem,
ControlWidth,
Expand All @@ -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: {
Expand All @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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$,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -238,7 +211,7 @@ export const getControlGroupEmbeddableFactory = (services: {
return {
api,
Component: () => {
const controlsInOrder = useStateFromPublishingSubject(controlsInOrder$);
const controlsInOrder = useStateFromPublishingSubject(controlsManager.controlsInOrder$);

useEffect(() => {
return () => {
Expand All @@ -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);
}}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
]);
});
});
Original file line number Diff line number Diff line change
@@ -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>,
};
}
Loading

0 comments on commit 994d4df

Please sign in to comment.