Skip to content

Commit

Permalink
[8.x] [controls] remove id from explicit input (elastic#211851) (elas…
Browse files Browse the repository at this point in the history
…tic#212994)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[controls] remove id from explicit input
(elastic#211851)](elastic#211851)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Nathan
Reese","email":"reese.nathan@elastic.co"},"sourceCommit":{"committedDate":"2025-03-03T21:31:42Z","message":"[controls]
remove id from explicit input (elastic#211851)\n\nPart of `EmbeddableInput`
type removal.\n\nPR removes `EmbeddableInput` from controls plugin. Part
of this effort\nis removing `id` key from
`controlConfig/explicitInput`.\n\nWhile investigating this PR, I found
it odd that\n`ControlGroupApi.serializeState` returned controls in shape
`[ { ...rest\n} ]` while `ControlGroupFactory.deserializeState` expected
to receive\ncontrols in the shape `[ { id, ...rest }]`. The only reason
this works\nis
that\nsrc/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts\n`controlGroupInputOut`
adds `id` to each object in `controls`. This PR\nalso resolves this and
updates `ControlGroupApi.serializeState` to\nreturn controls in shape `[
{ id, ...rest } ]`\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"decf5feba554df9c14301d4f47bba969d2bd727e","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Presentation","release_note:skip","project:embeddableRebuild","backport:version","v9.1.0","v8.19.0"],"title":"[controls]
remove id from explicit
input","number":211851,"url":"https://github.com/elastic/kibana/pull/211851","mergeCommit":{"message":"[controls]
remove id from explicit input (elastic#211851)\n\nPart of `EmbeddableInput`
type removal.\n\nPR removes `EmbeddableInput` from controls plugin. Part
of this effort\nis removing `id` key from
`controlConfig/explicitInput`.\n\nWhile investigating this PR, I found
it odd that\n`ControlGroupApi.serializeState` returned controls in shape
`[ { ...rest\n} ]` while `ControlGroupFactory.deserializeState` expected
to receive\ncontrols in the shape `[ { id, ...rest }]`. The only reason
this works\nis
that\nsrc/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts\n`controlGroupInputOut`
adds `id` to each object in `controls`. This PR\nalso resolves this and
updates `ControlGroupApi.serializeState` to\nreturn controls in shape `[
{ id, ...rest } ]`\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"decf5feba554df9c14301d4f47bba969d2bd727e"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/211851","number":211851,"mergeCommit":{"message":"[controls]
remove id from explicit input (elastic#211851)\n\nPart of `EmbeddableInput`
type removal.\n\nPR removes `EmbeddableInput` from controls plugin. Part
of this effort\nis removing `id` key from
`controlConfig/explicitInput`.\n\nWhile investigating this PR, I found
it odd that\n`ControlGroupApi.serializeState` returned controls in shape
`[ { ...rest\n} ]` while `ControlGroupFactory.deserializeState` expected
to receive\ncontrols in the shape `[ { id, ...rest }]`. The only reason
this works\nis
that\nsrc/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts\n`controlGroupInputOut`
adds `id` to each object in `controls`. This PR\nalso resolves this and
updates `ControlGroupApi.serializeState` to\nreturn controls in shape `[
{ id, ...rest } ]`\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"decf5feba554df9c14301d4f47bba969d2bd727e"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Nathan Reese <reese.nathan@elastic.co>
  • Loading branch information
2 people authored and SoniaSanzV committed Mar 4, 2025
1 parent 81e06dd commit 6e858d5
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
>;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/platform/plugins/shared/controls/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '@kbn/presentation-publishing';
import { BehaviorSubject, first, merge } from 'rxjs';
import type {
ControlGroupSerializedState,
ControlPanelState,
ControlPanelsState,
ControlWidth,
Expand Down Expand Up @@ -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);
Expand All @@ -160,7 +161,7 @@ export function initControlsManager(
}

const {
rawState: { grow, width, ...rest },
rawState: { grow, width, ...controlConfig },
references: controlReferences,
} = controlApi.serializeState();

Expand All @@ -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,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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'],
Expand All @@ -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),
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import { SavedObjectReference } from '@kbn/core/types';
import {
EmbeddableInput,
EmbeddablePersistableStateService,
EmbeddableStateWithType,
} from '@kbn/embeddable-plugin/common/types';
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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 };
Expand Down
3 changes: 1 addition & 2 deletions src/platform/plugins/shared/controls/server/mocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 6e858d5

Please sign in to comment.