[controls] remove id from explicit input (#211851)

Part of `EmbeddableInput` type removal.

PR removes `EmbeddableInput` from controls plugin. Part of this effort
is removing `id` key from `controlConfig/explicitInput`.

While investigating this PR, I found it odd that
`ControlGroupApi.serializeState` returned controls in shape `[ { ...rest
} ]` while `ControlGroupFactory.deserializeState` expected to receive
controls in the shape `[ { id, ...rest }]`. The only reason this works
is that
src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts
`controlGroupInputOut` adds `id` to each object in `controls`. This PR
also resolves this and updates `ControlGroupApi.serializeState` to
return controls in shape `[ { id, ...rest } ]`

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2025-03-03 14:31:42 -07:00 committed by GitHub
parent bee6ba88c9
commit decf5feba5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 75 additions and 91 deletions

View file

@ -51,7 +51,12 @@ export interface ControlGroupSerializedState
// In runtime state, we refer to this property as `initialChildControlState`, but in
// the serialized state we transform the state object into an array of state objects
// to make it easier for API consumers to add new controls without specifying a uuid key.
controls: Array<ControlPanelState & { id?: string }>;
controls: Array<
ControlPanelState & {
id?: string;
controlConfig?: object;
}
>;
}
/**

View file

@ -31,7 +31,7 @@ export interface DefaultControlState {
export interface SerializedControlState<ControlStateType extends object = object>
extends DefaultControlState {
type: string;
explicitInput: { id: string } & ControlStateType;
explicitInput: ControlStateType;
}
export interface DefaultDataControlState extends DefaultControlState {

View file

@ -24,6 +24,7 @@ import {
} from '@kbn/presentation-publishing';
import { BehaviorSubject, first, merge } from 'rxjs';
import type {
ControlGroupSerializedState,
ControlPanelState,
ControlPanelsState,
ControlWidth,
@ -151,7 +152,7 @@ export function initControlsManager(
serializeControls: () => {
const references: Reference[] = [];
const controls: Array<ControlPanelState & { controlConfig: object }> = [];
const controls: ControlGroupSerializedState['controls'] = [];
controlsInOrder$.getValue().forEach(({ id }, index) => {
const controlApi = getControlApi(id);
@ -160,7 +161,7 @@ export function initControlsManager(
}
const {
rawState: { grow, width, ...rest },
rawState: { grow, width, ...controlConfig },
references: controlReferences,
} = controlApi.serializeState();
@ -169,12 +170,13 @@ export function initControlsManager(
}
controls.push({
id,
grow,
order: index,
type: controlApi.type,
width,
/** Re-add the `controlConfig` layer on serialize so control group saved object retains shape */
controlConfig: { id, ...rest },
controlConfig,
});
});

View file

@ -7,40 +7,40 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { omit } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { SerializedPanelState } from '@kbn/presentation-publishing';
import type { ControlGroupRuntimeState, ControlGroupSerializedState } from '../../../common';
import type {
ControlGroupRuntimeState,
ControlGroupSerializedState,
ControlPanelsState,
} from '../../../common';
import { parseReferenceName } from '../../controls/data_controls/reference_name_utils';
export const deserializeControlGroup = (
state: SerializedPanelState<ControlGroupSerializedState>
): ControlGroupRuntimeState => {
const { controls } = state.rawState;
const controlsMap = Object.fromEntries(controls.map(({ id, ...rest }) => [id, rest]));
const initialChildControlState: ControlPanelsState = {};
(state.rawState.controls ?? []).forEach((controlSeriailizedState) => {
const { controlConfig, id, ...rest } = controlSeriailizedState;
initialChildControlState[id ?? uuidv4()] = {
...rest,
...(controlConfig ?? {}),
};
});
/** Inject data view references into each individual control */
// Inject data view references into each individual control
// TODO move reference injection into control factory to avoid leaking implemenation details like dataViewId to ControlGroup
const references = state.references ?? [];
references.forEach((reference) => {
const referenceName = reference.name;
const { controlId } = parseReferenceName(referenceName);
if (controlsMap[controlId]) {
controlsMap[controlId].dataViewId = reference.id;
if (initialChildControlState[controlId]) {
(initialChildControlState[controlId] as { dataViewId?: string }).dataViewId = reference.id;
}
});
/** Flatten the state of each control by removing `controlConfig` */
const flattenedControls = Object.keys(controlsMap).reduce((prev, controlId) => {
const currentControl = controlsMap[controlId];
const currentControlExplicitInput = controlsMap[controlId].controlConfig;
return {
...prev,
[controlId]: { ...omit(currentControl, 'controlConfig'), ...currentControlExplicitInput },
};
}, {});
return {
...state.rawState,
initialChildControlState: flattenedControls,
initialChildControlState,
};
};

View file

@ -7,13 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { SerializableRecord } from '@kbn/utility-types';
import {
OPTIONS_LIST_CONTROL,
RANGE_SLIDER_CONTROL,
type ControlPanelState,
type ControlWidth,
type DefaultDataControlState,
type SerializedControlState,
} from '../../common';
import { OptionsListControlState } from '../../common/options_list';
import { mockDataControlState, mockOptionsListControlState } from '../mocks';
@ -22,87 +21,67 @@ import { getDefaultControlGroupState } from './control_group_persistence';
import type { SerializableControlGroupState } from './types';
describe('migrate control group', () => {
const getOptionsListControl = (
order: number,
input?: Partial<OptionsListControlState & { id: string }>
) => {
const getOptionsListControl = (order: number, input?: Partial<OptionsListControlState>) => {
return {
type: OPTIONS_LIST_CONTROL,
order,
width: 'small' as ControlWidth,
grow: true,
explicitInput: { ...mockOptionsListControlState, ...input },
} as ControlPanelState<SerializedControlState<OptionsListControlState>>;
} as unknown as SerializableRecord;
};
const getRangeSliderControl = (
order: number,
input?: Partial<DefaultDataControlState & { id: string }>
) => {
const getRangeSliderControl = (order: number, input?: Partial<DefaultDataControlState>) => {
return {
type: RANGE_SLIDER_CONTROL,
order,
width: 'medium' as ControlWidth,
grow: false,
explicitInput: { ...mockDataControlState, ...input },
} as ControlPanelState<SerializedControlState<DefaultDataControlState>>;
};
const getControlGroupState = (
panels: Array<ControlPanelState<SerializedControlState>>
): SerializableControlGroupState => {
const panelsObjects = panels.reduce((acc, panel) => {
return { ...acc, [panel.explicitInput.id]: panel };
}, {});
return {
...getDefaultControlGroupState(),
panels: panelsObjects,
};
} as unknown as SerializableRecord;
};
describe('remove hideExclude and hideExists', () => {
test('should migrate single options list control', () => {
const migratedControlGroupState: SerializableControlGroupState =
removeHideExcludeAndHideExists(
getControlGroupState([getOptionsListControl(0, { id: 'testPanelId', hideExclude: true })])
);
removeHideExcludeAndHideExists({
...getDefaultControlGroupState(),
panels: {
testPanelId: getOptionsListControl(0, { hideExclude: true }),
},
});
expect(migratedControlGroupState.panels).toEqual({
testPanelId: getOptionsListControl(0, { id: 'testPanelId' }),
testPanelId: getOptionsListControl(0),
});
});
test('should migrate multiple options list controls', () => {
const migratedControlGroupInput: SerializableControlGroupState =
removeHideExcludeAndHideExists(
getControlGroupState([
getOptionsListControl(0, { id: 'testPanelId1' }),
getOptionsListControl(1, { id: 'testPanelId2', hideExclude: false }),
getOptionsListControl(2, { id: 'testPanelId3', hideExists: true }),
getOptionsListControl(3, {
id: 'testPanelId4',
removeHideExcludeAndHideExists({
...getDefaultControlGroupState(),
panels: {
testPanelId1: getOptionsListControl(0),
testPanelId2: getOptionsListControl(1, { hideExclude: false }),
testPanelId3: getOptionsListControl(2, { hideExists: true }),
testPanelId4: getOptionsListControl(3, {
hideExclude: true,
hideExists: false,
}),
getOptionsListControl(4, {
id: 'testPanelId5',
testPanelId5: getOptionsListControl(4, {
hideExists: true,
hideExclude: false,
singleSelect: true,
runPastTimeout: true,
selectedOptions: ['test'],
}),
])
);
},
});
expect(migratedControlGroupInput.panels).toEqual({
testPanelId1: getOptionsListControl(0, { id: 'testPanelId1' }),
testPanelId2: getOptionsListControl(1, { id: 'testPanelId2' }),
testPanelId3: getOptionsListControl(2, { id: 'testPanelId3' }),
testPanelId4: getOptionsListControl(3, {
id: 'testPanelId4',
}),
testPanelId1: getOptionsListControl(0),
testPanelId2: getOptionsListControl(1),
testPanelId3: getOptionsListControl(2),
testPanelId4: getOptionsListControl(3),
testPanelId5: getOptionsListControl(4, {
id: 'testPanelId5',
singleSelect: true,
runPastTimeout: true,
selectedOptions: ['test'],
@ -112,20 +91,20 @@ describe('migrate control group', () => {
test('should migrate multiple different types of controls', () => {
const migratedControlGroupInput: SerializableControlGroupState =
removeHideExcludeAndHideExists(
getControlGroupState([
getOptionsListControl(0, {
id: 'testPanelId1',
removeHideExcludeAndHideExists({
...getDefaultControlGroupState(),
panels: {
testPanelId1: getOptionsListControl(0, {
hideExists: true,
hideExclude: true,
runPastTimeout: true,
}),
getRangeSliderControl(1, { id: 'testPanelId2' }),
])
);
testPanelId2: getRangeSliderControl(1),
},
});
expect(migratedControlGroupInput.panels).toEqual({
testPanelId1: getOptionsListControl(0, { id: 'testPanelId1', runPastTimeout: true }),
testPanelId2: getRangeSliderControl(1, { id: 'testPanelId2' }),
testPanelId1: getOptionsListControl(0, { runPastTimeout: true }),
testPanelId2: getRangeSliderControl(1),
});
});
});

View file

@ -9,7 +9,6 @@
import { SavedObjectReference } from '@kbn/core/types';
import {
EmbeddableInput,
EmbeddablePersistableStateService,
EmbeddableStateWithType,
} from '@kbn/embeddable-plugin/common/types';
@ -22,7 +21,7 @@ import {
} from './control_group_migrations';
import { SerializableControlGroupState } from './types';
const getPanelStatePrefix = (state: SerializedControlState) => `${state.explicitInput.id}:`;
const getPanelStatePrefix = (panelId: string) => `${panelId}:`;
export const createControlGroupInject = (
persistableStateService: EmbeddablePersistableStateService
@ -39,7 +38,7 @@ export const createControlGroupInject = (
...panel,
};
// Find the references for this panel
const prefix = getPanelStatePrefix(panel);
const prefix = getPanelStatePrefix(key);
const filteredReferences = references
.filter((reference) => reference.name.indexOf(prefix) === 0)
@ -51,7 +50,7 @@ export const createControlGroupInject = (
{ ...workingPanels[key].explicitInput, type: workingPanels[key].type },
panelReferences
);
workingPanels[key].explicitInput = injectedState as EmbeddableInput;
workingPanels[key].explicitInput = injectedState;
}
}
return { ...workingState, panels: workingPanels } as unknown as EmbeddableStateWithType;
@ -70,7 +69,7 @@ export const createControlGroupExtract = (
// Run every panel through the state service to get the nested references
for (const [key, panel] of Object.entries(workingState.panels)) {
const prefix = getPanelStatePrefix(panel);
const prefix = getPanelStatePrefix(key);
const { state: panelState, references: panelReferences } = persistableStateService.extract({
...panel.explicitInput,
@ -87,7 +86,7 @@ export const createControlGroupExtract = (
const { type, ...restOfState } = panelState;
(workingState.panels as ControlPanelsState<SerializedControlState>)[key].explicitInput =
restOfState as EmbeddableInput;
restOfState;
}
}
return { state: workingState as EmbeddableStateWithType, references };

View file

@ -11,11 +11,10 @@ import type { OptionsListControlState } from '../common/options_list';
import type { DefaultDataControlState } from '../common/types';
export const mockDataControlState = {
id: 'id',
fieldName: 'sample field',
dataViewId: 'sample id',
value: ['0', '10'],
} as DefaultDataControlState & { id: string };
} as DefaultDataControlState;
export const mockOptionsListControlState = {
...mockDataControlState,

View file

@ -265,7 +265,7 @@ describe('itemAttrsToSavedObjectAttrs', () => {
"chainingSystem": "NONE",
"controlStyle": "twoLine",
"ignoreParentSettingsJSON": "{\\"ignoreFilters\\":true,\\"ignoreQuery\\":true,\\"ignoreTimerange\\":true,\\"ignoreValidations\\":true}",
"panelsJSON": "{\\"foo\\":{\\"grow\\":false,\\"order\\":0,\\"type\\":\\"type1\\",\\"width\\":\\"small\\",\\"explicitInput\\":{\\"anyKey\\":\\"some value\\",\\"id\\":\\"foo\\"}}}",
"panelsJSON": "{\\"foo\\":{\\"grow\\":false,\\"order\\":0,\\"type\\":\\"type1\\",\\"width\\":\\"small\\",\\"explicitInput\\":{\\"anyKey\\":\\"some value\\"}}}",
"showApplySelections": true,
},
"description": "description",
@ -587,7 +587,7 @@ describe('getResultV3ToV2', () => {
// Check transformed attributes
expect(output.item.attributes.controlGroupInput!.panelsJSON).toMatchInlineSnapshot(
`"{\\"foo\\":{\\"grow\\":false,\\"order\\":0,\\"type\\":\\"type1\\",\\"width\\":\\"small\\",\\"explicitInput\\":{\\"bizz\\":\\"buzz\\",\\"id\\":\\"foo\\"}}}"`
`"{\\"foo\\":{\\"grow\\":false,\\"order\\":0,\\"type\\":\\"type1\\",\\"width\\":\\"small\\",\\"explicitInput\\":{\\"bizz\\":\\"buzz\\"}}}"`
);
expect(
output.item.attributes.controlGroupInput!.ignoreParentSettingsJSON

View file

@ -193,7 +193,7 @@ function controlGroupInputIn(
controlGroupInput;
const updatedControls = Object.fromEntries(
controls.map(({ controlConfig, id = uuidv4(), ...restOfControl }) => {
return [id, { ...restOfControl, explicitInput: { ...controlConfig, id } }];
return [id, { ...restOfControl, explicitInput: controlConfig }];
})
);
return {