Skip to content

Commit

Permalink
[control group] implement lastUsedDataViewId$ (elastic#190269)
Browse files Browse the repository at this point in the history
PR added `lastUsedDataViewId$` to `ControlGroupApi`.
`lastUsedDataViewId$` is implemented in `initializeControlsManager`.

PR also cleaned up typings by removing `DataControlEditorState`.
`DataControlEditorState` was a weird smooshing together of
DefaultDataControlState and stuff used by the editor. Instead of
`DataControlEditorState`, values used by the editor are just passed in
as top level keys.

---------

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
nreese and elasticmachine authored Aug 15, 2024
1 parent d24105d commit 3ad861e
Show file tree
Hide file tree
Showing 13 changed files with 211 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ export const ReactControlExample = ({
addNewPanel: () => {
return Promise.resolve(undefined);
},
lastUsedDataViewId: new BehaviorSubject<string>(WEB_LOGS_DATA_VIEW_ID),
saveNotification$,
reload$,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,13 @@ export const getControlGroupEmbeddableFactory = (services: {
} = initialRuntimeState;

const autoApplySelections$ = new BehaviorSubject<boolean>(autoApplySelections);
const controlsManager = initControlsManager(initialChildControlState);
const parentDataViewId = apiPublishesDataViews(parentApi)
? parentApi.dataViews.value?.[0]?.id
: undefined;
const controlsManager = initControlsManager(
initialChildControlState,
parentDataViewId ?? (await services.dataViews.getDefaultId())
);
const selectionsManager = initSelectionsManager({
...controlsManager.api,
autoApplySelections$,
Expand Down Expand Up @@ -172,6 +178,7 @@ export const getControlGroupEmbeddableFactory = (services: {
initialState: {
grow: api.grow.getValue(),
width: api.width.getValue(),
dataViewId: controlsManager.api.lastUsedDataViewId$.value,
},
onSave: ({ type: controlType, state: initialState }) => {
api.addNewPanel({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@
*/

import { DefaultControlApi } from '../controls/types';
import { initControlsManager } from './init_controls_manager';
import { initControlsManager, getLastUsedDataViewId } from './init_controls_manager';

jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('delta'),
}));

const DEFAULT_DATA_VIEW_ID = 'myDataView';

describe('PresentationContainer api', () => {
test('addNewPanel should add control at end of controls', async () => {
const controlsManager = initControlsManager({
alpha: { type: 'testControl', order: 0 },
bravo: { type: 'testControl', order: 1 },
charlie: { type: 'testControl', order: 2 },
});
const controlsManager = initControlsManager(
{
alpha: { type: 'testControl', order: 0 },
bravo: { type: 'testControl', order: 1 },
charlie: { type: 'testControl', order: 2 },
},
DEFAULT_DATA_VIEW_ID
);
const addNewPanelPromise = controlsManager.api.addNewPanel({
panelType: 'testControl',
initialState: {},
Expand All @@ -35,11 +40,14 @@ describe('PresentationContainer api', () => {
});

test('removePanel should remove control', () => {
const controlsManager = initControlsManager({
alpha: { type: 'testControl', order: 0 },
bravo: { type: 'testControl', order: 1 },
charlie: { type: 'testControl', order: 2 },
});
const controlsManager = initControlsManager(
{
alpha: { type: 'testControl', order: 0 },
bravo: { type: 'testControl', order: 1 },
charlie: { type: 'testControl', order: 2 },
},
DEFAULT_DATA_VIEW_ID
);
controlsManager.api.removePanel('bravo');
expect(controlsManager.controlsInOrder$.value.map((element) => element.id)).toEqual([
'alpha',
Expand All @@ -48,11 +56,14 @@ describe('PresentationContainer api', () => {
});

test('replacePanel should replace control', async () => {
const controlsManager = initControlsManager({
alpha: { type: 'testControl', order: 0 },
bravo: { type: 'testControl', order: 1 },
charlie: { type: 'testControl', order: 2 },
});
const controlsManager = initControlsManager(
{
alpha: { type: 'testControl', order: 0 },
bravo: { type: 'testControl', order: 1 },
charlie: { type: 'testControl', order: 2 },
},
DEFAULT_DATA_VIEW_ID
);
const replacePanelPromise = controlsManager.api.replacePanel('bravo', {
panelType: 'testControl',
initialState: {},
Expand All @@ -68,10 +79,13 @@ describe('PresentationContainer api', () => {

describe('untilInitialized', () => {
test('should not resolve until all controls are initialized', async () => {
const controlsManager = initControlsManager({
alpha: { type: 'testControl', order: 0 },
bravo: { type: 'testControl', order: 1 },
});
const controlsManager = initControlsManager(
{
alpha: { type: 'testControl', order: 0 },
bravo: { type: 'testControl', order: 1 },
},
DEFAULT_DATA_VIEW_ID
);
let isDone = false;
controlsManager.api.untilInitialized().then(() => {
isDone = true;
Expand All @@ -89,10 +103,13 @@ describe('PresentationContainer api', () => {
});

test('should resolve when all control already initialized ', async () => {
const controlsManager = initControlsManager({
alpha: { type: 'testControl', order: 0 },
bravo: { type: 'testControl', order: 1 },
});
const controlsManager = initControlsManager(
{
alpha: { type: 'testControl', order: 0 },
bravo: { type: 'testControl', order: 1 },
},
DEFAULT_DATA_VIEW_ID
);
controlsManager.setControlApi('alpha', {} as unknown as DefaultControlApi);
controlsManager.setControlApi('bravo', {} as unknown as DefaultControlApi);

Expand All @@ -109,10 +126,13 @@ describe('PresentationContainer api', () => {

describe('snapshotControlsRuntimeState', () => {
test('should snapshot runtime state for all controls', async () => {
const controlsManager = initControlsManager({
alpha: { type: 'testControl', order: 1 },
bravo: { type: 'testControl', order: 0 },
});
const controlsManager = initControlsManager(
{
alpha: { type: 'testControl', order: 1 },
bravo: { type: 'testControl', order: 0 },
},
DEFAULT_DATA_VIEW_ID
);
controlsManager.setControlApi('alpha', {
snapshotRuntimeState: () => {
return { key1: 'alpha value' };
Expand All @@ -137,3 +157,33 @@ describe('snapshotControlsRuntimeState', () => {
});
});
});

describe('getLastUsedDataViewId', () => {
test('should return last used data view id', () => {
const dataViewId = getLastUsedDataViewId(
[
{ id: 'alpha', type: 'testControl' },
{ id: 'bravo', type: 'testControl' },
{ id: 'charlie', type: 'testControl' },
],
{
alpha: { dataViewId: '1', type: 'testControl', order: 0 },
bravo: { dataViewId: '2', type: 'testControl', order: 1 },
charlie: { type: 'testControl', order: 2 },
}
);
expect(dataViewId).toBe('2');
});

test('should return undefined when there are no controls', () => {
const dataViewId = getLastUsedDataViewId([], {});
expect(dataViewId).toBeUndefined();
});

test('should return undefined when there are no controls with data views', () => {
const dataViewId = getLastUsedDataViewId([{ id: 'alpha', type: 'testControl' }], {
alpha: { type: 'testControl', order: 0 },
});
expect(dataViewId).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import { BehaviorSubject, first, merge } from 'rxjs';
import { PublishingSubject, StateComparators } from '@kbn/presentation-publishing';
import { omit } from 'lodash';
import { apiHasSnapshottableState } from '@kbn/presentation-containers/interfaces/serialized_state';
import { ControlPanelsState, ControlPanelState } from './types';
import { ControlGroupApi, ControlPanelsState, ControlPanelState } from './types';
import { DefaultControlApi, DefaultControlState } from '../controls/types';
import { ControlGroupComparatorState } from './control_group_unsaved_changes_api';
import { DefaultDataControlState } from '../controls/data_controls/types';

export type ControlsInOrder = Array<{ id: string; type: string }>;

Expand All @@ -35,7 +36,10 @@ export function getControlsInOrder(initialControlPanelsState: ControlPanelsState
.map(({ id, type }) => ({ id, type })); // filter out `order`
}

export function initControlsManager(initialControlPanelsState: ControlPanelsState) {
export function initControlsManager(
initialControlPanelsState: ControlPanelsState,
defaultDataViewId: string | null
) {
const lastSavedControlsPanelState$ = new BehaviorSubject(initialControlPanelsState);
const initialControlIds = Object.keys(initialControlPanelsState);
const children$ = new BehaviorSubject<{ [key: string]: DefaultControlApi }>({});
Expand All @@ -45,6 +49,11 @@ export function initControlsManager(initialControlPanelsState: ControlPanelsStat
const controlsInOrder$ = new BehaviorSubject<ControlsInOrder>(
getControlsInOrder(initialControlPanelsState)
);
const lastUsedDataViewId$ = new BehaviorSubject<string | undefined>(
getLastUsedDataViewId(controlsInOrder$.value, initialControlPanelsState) ??
defaultDataViewId ??
undefined
);

function untilControlLoaded(
id: string
Expand Down Expand Up @@ -79,6 +88,9 @@ export function initControlsManager(initialControlPanelsState: ControlPanelsStat
{ panelType, initialState }: PanelPackage<DefaultControlState>,
index: number
) {
if ((initialState as DefaultDataControlState)?.dataViewId) {
lastUsedDataViewId$.next((initialState as DefaultDataControlState).dataViewId);
}
const id = generateId();
const nextControlsInOrder = [...controlsInOrder$.value];
nextControlsInOrder.splice(index, 0, {
Expand Down Expand Up @@ -156,6 +168,7 @@ export function initControlsManager(initialControlPanelsState: ControlPanelsStat
return controlsRuntimeState;
},
api: {
lastUsedDataViewId$: lastUsedDataViewId$ as PublishingSubject<string | undefined>,
getSerializedStateForChild: (childId: string) => {
const controlPanelState = controlsPanelState[childId];
return controlPanelState ? { rawState: controlPanelState } : undefined;
Expand Down Expand Up @@ -196,7 +209,8 @@ export function initControlsManager(initialControlPanelsState: ControlPanelsStat
});
},
} as PresentationContainer &
HasSerializedChildState<ControlPanelState> & { untilInitialized: () => Promise<void> },
HasSerializedChildState<ControlPanelState> &
Pick<ControlGroupApi, 'untilInitialized' | 'lastUsedDataViewId$'>,
comparators: {
controlsInOrder: [
controlsInOrder$,
Expand All @@ -222,3 +236,21 @@ export function initControlsManager(initialControlPanelsState: ControlPanelsStat
>,
};
}

export function getLastUsedDataViewId(
controlsInOrder: ControlsInOrder,
initialControlPanelsState: ControlPanelsState<
ControlPanelState & Partial<DefaultDataControlState>
>
) {
let dataViewId: string | undefined;
for (let i = controlsInOrder.length - 1; i >= 0; i--) {
const controlId = controlsInOrder[i].id;
const controlState = initialControlPanelsState[controlId];
if (controlState?.dataViewId) {
dataViewId = controlState.dataViewId;
break;
}
}
return dataViewId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type ControlGroupApi = PresentationContainer &
openAddDataControlFlyout: (settings?: {
controlInputTransform?: ControlInputTransform;
}) => void;
lastUsedDataViewId$: PublishingSubject<string | undefined>;
};

export interface ControlGroupRuntimeState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ jest.mock('../../control_factory_registry', () => ({
import { DEFAULT_CONTROL_GROW, DEFAULT_CONTROL_WIDTH } from '../../../../common';
import { ControlGroupApi } from '../../control_group/types';
import { DataControlEditor } from './data_control_editor';
import { DataControlEditorState } from './open_data_control_editor';
import {
getMockedOptionsListControlFactory,
getMockedRangeSliderControlFactory,
Expand Down Expand Up @@ -57,7 +56,6 @@ mockDataViews.get = jest.fn().mockResolvedValue(mockDataView);

const dashboardApi = {
timeRange$: new BehaviorSubject<TimeRange | undefined>(undefined),
lastUsedDataViewId$: new BehaviorSubject<string>(mockDataView.id!),
};
const controlGroupApi = {
parentApi: dashboardApi,
Expand All @@ -68,8 +66,14 @@ const controlGroupApi = {
describe('Data control editor', () => {
const mountComponent = async ({
initialState,
controlId,
controlType,
initialDefaultPanelTitle,
}: {
initialState?: Partial<DataControlEditorState>;
initialState?: Partial<DefaultDataControlState>;
controlId?: string;
controlType?: string;
initialDefaultPanelTitle?: string;
}) => {
mockDataViews.get = jest.fn().mockResolvedValue(mockDataView);

Expand All @@ -78,11 +82,14 @@ describe('Data control editor', () => {
<DataControlEditor
onCancel={() => {}}
onSave={() => {}}
parentApi={controlGroupApi}
controlGroupApi={controlGroupApi}
initialState={{
dataViewId: dashboardApi.lastUsedDataViewId$.getValue(),
dataViewId: mockDataView.id,
...initialState,
}}
controlId={controlId}
controlType={controlType}
initialDefaultPanelTitle={initialDefaultPanelTitle}
services={{ dataViews: mockDataViews }}
/>
</I18nProvider>
Expand Down Expand Up @@ -238,11 +245,11 @@ describe('Data control editor', () => {
test('auto-fills input with the default title', async () => {
const controlEditor = await mountComponent({
initialState: {
controlType: 'optionsList',
controlId: 'testId',
fieldName: 'machine.os.raw',
defaultPanelTitle: 'OS',
},
controlType: 'optionsList',
controlId: 'testId',
initialDefaultPanelTitle: 'OS',
});
const titleInput = await controlEditor.findByTestId('control-editor-title-input');
expect(titleInput.getAttribute('value')).toBe('OS');
Expand All @@ -252,11 +259,11 @@ describe('Data control editor', () => {
test('auto-fills input with the custom title', async () => {
const controlEditor = await mountComponent({
initialState: {
controlType: 'optionsList',
controlId: 'testId',
fieldName: 'machine.os.raw',
title: 'Custom title',
},
controlType: 'optionsList',
controlId: 'testId',
});
const titleInput = await controlEditor.findByTestId('control-editor-title-input');
expect(titleInput.getAttribute('value')).toBe('Custom title');
Expand All @@ -267,10 +274,10 @@ describe('Data control editor', () => {
test('selects the provided control type', async () => {
const controlEditor = await mountComponent({
initialState: {
controlType: 'rangeSlider',
controlId: 'testId',
fieldName: 'bytes',
},
controlType: 'rangeSlider',
controlId: 'testId',
});

expect(controlEditor.getByTestId('create__optionsList')).toBeEnabled();
Expand Down
Loading

0 comments on commit 3ad861e

Please sign in to comment.