diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts index 36e9567786c0d..0ea8b883305c9 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts @@ -7,145 +7,31 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { v4 as uuidv4 } from 'uuid'; import { pick } from 'lodash'; -import type { Query } from '@kbn/es-query'; -import { - type ControlGroupChainingSystem, - type ControlLabelPosition, - type ControlPanelsState, - type SerializedControlState, - DEFAULT_AUTO_APPLY_SELECTIONS, - DEFAULT_CONTROL_CHAINING, - DEFAULT_CONTROL_GROW, - DEFAULT_CONTROL_LABEL_POSITION, - DEFAULT_CONTROL_WIDTH, - DEFAULT_IGNORE_PARENT_SETTINGS, -} from '@kbn/controls-plugin/common'; -import { SerializedSearchSourceFields, parseSearchSourceJSON } from '@kbn/data-plugin/common'; - import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-api-server'; import type { - ControlGroupAttributes, DashboardAttributes, DashboardGetOut, DashboardItem, - DashboardOptions, ItemAttrsToSavedObjectAttrsReturn, PartialDashboardItem, SavedObjectToItemReturn, } from './types'; -import type { - DashboardSavedObjectAttributes, - SavedDashboardPanel, -} from '../../dashboard_saved_object'; +import type { DashboardSavedObjectAttributes } from '../../dashboard_saved_object'; import type { ControlGroupAttributes as ControlGroupAttributesV2, DashboardCrudTypes as DashboardCrudTypesV2, } from '../../../common/content_management/v2'; -import { DEFAULT_DASHBOARD_OPTIONS } from '../../../common/content_management'; - -function controlGroupInputOut( - controlGroupInput?: DashboardSavedObjectAttributes['controlGroupInput'] -): ControlGroupAttributes | undefined { - if (!controlGroupInput) { - return; - } - const { - panelsJSON, - ignoreParentSettingsJSON, - controlStyle = DEFAULT_CONTROL_LABEL_POSITION, - chainingSystem = DEFAULT_CONTROL_CHAINING, - showApplySelections = !DEFAULT_AUTO_APPLY_SELECTIONS, - } = controlGroupInput; - const controls = panelsJSON - ? Object.entries(JSON.parse(panelsJSON) as ControlPanelsState).map( - ([ - id, - { - explicitInput, - type, - grow = DEFAULT_CONTROL_GROW, - width = DEFAULT_CONTROL_WIDTH, - order, - }, - ]) => ({ - controlConfig: explicitInput, - id, - grow, - order, - type, - width, - }) - ) - : []; - - const { - ignoreFilters = DEFAULT_IGNORE_PARENT_SETTINGS.ignoreFilters, - ignoreQuery = DEFAULT_IGNORE_PARENT_SETTINGS.ignoreQuery, - ignoreTimerange = DEFAULT_IGNORE_PARENT_SETTINGS.ignoreTimerange, - ignoreValidations = DEFAULT_IGNORE_PARENT_SETTINGS.ignoreValidations, - } = ignoreParentSettingsJSON ? JSON.parse(ignoreParentSettingsJSON) : {}; - - // try to maintain a consistent (alphabetical) order of keys - return { - autoApplySelections: !showApplySelections, - chainingSystem: chainingSystem as ControlGroupChainingSystem, - controls, - labelPosition: controlStyle as ControlLabelPosition, - ignoreParentSettings: { ignoreFilters, ignoreQuery, ignoreTimerange, ignoreValidations }, - }; -} - -function kibanaSavedObjectMetaOut( - kibanaSavedObjectMeta: DashboardSavedObjectAttributes['kibanaSavedObjectMeta'] -): DashboardAttributes['kibanaSavedObjectMeta'] { - const { searchSourceJSON } = kibanaSavedObjectMeta; - if (!searchSourceJSON) { - return {}; - } - // Dashboards do not yet support ES|QL (AggregateQuery) in the search source - return { - searchSource: parseSearchSourceJSON(searchSourceJSON) as Omit< - SerializedSearchSourceFields, - 'query' - > & { query?: Query }, - }; -} - -function optionsOut(optionsJSON: string): DashboardAttributes['options'] { - const { - hidePanelTitles = DEFAULT_DASHBOARD_OPTIONS.hidePanelTitles, - useMargins = DEFAULT_DASHBOARD_OPTIONS.useMargins, - syncColors = DEFAULT_DASHBOARD_OPTIONS.syncColors, - syncCursor = DEFAULT_DASHBOARD_OPTIONS.syncCursor, - syncTooltips = DEFAULT_DASHBOARD_OPTIONS.syncTooltips, - } = JSON.parse(optionsJSON) as DashboardOptions; - return { - hidePanelTitles, - useMargins, - syncColors, - syncCursor, - syncTooltips, - }; -} - -function panelsOut(panelsJSON: string): DashboardAttributes['panels'] { - const panels = JSON.parse(panelsJSON) as SavedDashboardPanel[]; - return panels.map( - ({ embeddableConfig, gridData, id, panelIndex, panelRefName, title, type, version }) => ({ - gridData, - id, - panelConfig: embeddableConfig, - panelIndex, - panelRefName, - title, - type, - version, - }) - ); -} +import { + transformControlGroupIn, + transformControlGroupOut, + transformOptionsOut, + transformPanelsIn, + transformPanelsOut, + transformSearchSourceIn, + transformSearchSourceOut, +} from './transforms'; export function dashboardAttributesOut( attributes: DashboardSavedObjectAttributes | Partial @@ -165,13 +51,13 @@ export function dashboardAttributesOut( } = attributes; // try to maintain a consistent (alphabetical) order of keys return { - ...(controlGroupInput && { controlGroupInput: controlGroupInputOut(controlGroupInput) }), + ...(controlGroupInput && { controlGroupInput: transformControlGroupOut(controlGroupInput) }), ...(description && { description }), ...(kibanaSavedObjectMeta && { - kibanaSavedObjectMeta: kibanaSavedObjectMetaOut(kibanaSavedObjectMeta), + kibanaSavedObjectMeta: transformSearchSourceOut(kibanaSavedObjectMeta), }), - ...(optionsJSON && { options: optionsOut(optionsJSON) }), - ...(panelsJSON && { panels: panelsOut(panelsJSON) }), + ...(optionsJSON && { options: transformOptionsOut(optionsJSON) }), + ...(panelsJSON && { panels: transformPanelsOut(panelsJSON) }), ...(refreshInterval && { refreshInterval: { pause: refreshInterval.pause, value: refreshInterval.value }, }), @@ -183,54 +69,6 @@ export function dashboardAttributesOut( }; } -function controlGroupInputIn( - controlGroupInput?: ControlGroupAttributes -): DashboardSavedObjectAttributes['controlGroupInput'] | undefined { - if (!controlGroupInput) { - return; - } - const { controls, ignoreParentSettings, labelPosition, chainingSystem, autoApplySelections } = - controlGroupInput; - const updatedControls = Object.fromEntries( - controls.map(({ controlConfig, id = uuidv4(), ...restOfControl }) => { - return [id, { ...restOfControl, explicitInput: controlConfig }]; - }) - ); - return { - chainingSystem, - controlStyle: labelPosition, - ignoreParentSettingsJSON: JSON.stringify(ignoreParentSettings), - panelsJSON: JSON.stringify(updatedControls), - showApplySelections: !autoApplySelections, - }; -} - -function panelsIn( - panels: DashboardAttributes['panels'] -): DashboardSavedObjectAttributes['panelsJSON'] { - const updatedPanels = panels.map(({ panelIndex, gridData, panelConfig, ...restPanel }) => { - const idx = panelIndex ?? uuidv4(); - return { - ...restPanel, - embeddableConfig: panelConfig, - panelIndex: idx, - gridData: { - ...gridData, - i: idx, - }, - }; - }); - - return JSON.stringify(updatedPanels); -} - -function kibanaSavedObjectMetaIn( - kibanaSavedObjectMeta: DashboardAttributes['kibanaSavedObjectMeta'] -) { - const { searchSource } = kibanaSavedObjectMeta; - return { searchSourceJSON: JSON.stringify(searchSource ?? {}) }; -} - export const getResultV3ToV2 = (result: DashboardGetOut): DashboardCrudTypesV2['GetOut'] => { const { meta, item } = result; const { attributes, ...rest } = item; @@ -250,14 +88,14 @@ export const getResultV3ToV2 = (result: DashboardGetOut): DashboardCrudTypesV2[' const v2Attributes = { ...(controlGroupInput && { - controlGroupInput: controlGroupInputIn(controlGroupInput) as ControlGroupAttributesV2, + controlGroupInput: transformControlGroupIn(controlGroupInput) as ControlGroupAttributesV2, }), description, ...(kibanaSavedObjectMeta && { - kibanaSavedObjectMeta: kibanaSavedObjectMetaIn(kibanaSavedObjectMeta), + kibanaSavedObjectMeta: transformSearchSourceIn(kibanaSavedObjectMeta), }), ...(options && { optionsJSON: JSON.stringify(options) }), - panelsJSON: panels ? panelsIn(panels) : '[]', + panelsJSON: panels ? transformPanelsIn(panels) : '[]', refreshInterval, ...(timeFrom && { timeFrom }), timeRestore, @@ -282,16 +120,16 @@ export const itemAttrsToSavedObjectAttrs = ( const soAttributes = { ...rest, ...(controlGroupInput && { - controlGroupInput: controlGroupInputIn(controlGroupInput), + controlGroupInput: transformControlGroupIn(controlGroupInput), }), ...(options && { optionsJSON: JSON.stringify(options), }), ...(panels && { - panelsJSON: panelsIn(panels), + panelsJSON: transformPanelsIn(panels), }), ...(kibanaSavedObjectMeta && { - kibanaSavedObjectMeta: kibanaSavedObjectMetaIn(kibanaSavedObjectMeta), + kibanaSavedObjectMeta: transformSearchSourceIn(kibanaSavedObjectMeta), }), }; return { attributes: soAttributes, error: null }; diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/control_group_in_transforms.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/control_group_in_transforms.test.ts new file mode 100644 index 0000000000000..6ca95eb5daf14 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/control_group_in_transforms.test.ts @@ -0,0 +1,106 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { transformControlGroupIn } from './control_group_in_transforms'; +import { ControlGroupAttributes } from '../../types'; +import { CONTROL_WIDTH_OPTIONS } from '@kbn/controls-plugin/common'; + +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'mock-uuid'), +})); + +describe('transformControlGroupIn', () => { + const mockControlGroupInput: ControlGroupAttributes = { + chainingSystem: 'NONE', + labelPosition: 'oneLine', + autoApplySelections: true, + ignoreParentSettings: { + ignoreFilters: true, + ignoreQuery: true, + ignoreTimerange: true, + ignoreValidations: true, + }, + controls: [ + { + id: 'control1', + type: 'type1', + width: CONTROL_WIDTH_OPTIONS.SMALL, + controlConfig: { bizz: 'buzz' }, + order: 0, + grow: false, + }, + { + type: 'type2', + grow: true, + width: CONTROL_WIDTH_OPTIONS.SMALL, + controlConfig: { boo: 'bear' }, + order: 1, + }, + ], + }; + + it('should return undefined if controlGroupInput is undefined', () => { + const result = transformControlGroupIn(undefined); + expect(result).toBeUndefined(); + }); + + it('should transform controlGroupInput correctly', () => { + const result = transformControlGroupIn(mockControlGroupInput); + + expect(result).toEqual({ + chainingSystem: 'NONE', + controlStyle: 'oneLine', + showApplySelections: false, + ignoreParentSettingsJSON: JSON.stringify({ + ignoreFilters: true, + ignoreQuery: true, + ignoreTimerange: true, + ignoreValidations: true, + }), + panelsJSON: JSON.stringify({ + control1: { + type: 'type1', + width: 'small', + order: 0, + grow: false, + explicitInput: { bizz: 'buzz' }, + }, + 'mock-uuid': { + type: 'type2', + grow: true, + width: 'small', + order: 1, + explicitInput: { boo: 'bear' }, + }, + }), + }); + }); + + it('should handle empty controls array', () => { + const controlGroupInput: ControlGroupAttributes = { + ...mockControlGroupInput, + controls: [], + }; + + const result = transformControlGroupIn(controlGroupInput); + + expect(result).toEqual({ + chainingSystem: 'NONE', + controlStyle: 'oneLine', + showApplySelections: false, + ignoreParentSettingsJSON: JSON.stringify({ + ignoreFilters: true, + ignoreQuery: true, + ignoreTimerange: true, + ignoreValidations: true, + }), + panelsJSON: JSON.stringify({}), + }); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/control_group_in_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/control_group_in_transforms.ts new file mode 100644 index 0000000000000..b1ab5224e8cbb --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/control_group_in_transforms.ts @@ -0,0 +1,65 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { flow } from 'lodash'; +import { v4 as uuidv4 } from 'uuid'; + +import { DashboardSavedObjectAttributes } from '../../../../dashboard_saved_object'; +import { ControlGroupAttributes } from '../../types'; + +export function transformControlGroupIn( + controlGroupInput?: ControlGroupAttributes +): DashboardSavedObjectAttributes['controlGroupInput'] | undefined { + if (!controlGroupInput) { + return; + } + return flow( + transformControlStyle, + transformShowApplySelections, + transformIgnoreParentSettings, + transformPanelsJSON + )(controlGroupInput); +} + +function transformControlStyle(controlGroupInput: ControlGroupAttributes) { + const { labelPosition, ...restControlGroupInput } = controlGroupInput; + return { + ...restControlGroupInput, + controlStyle: labelPosition, + }; +} + +function transformShowApplySelections(controlGroupInput: ControlGroupAttributes) { + const { autoApplySelections, ...restControlGroupInput } = controlGroupInput; + return { + ...restControlGroupInput, + showApplySelections: !autoApplySelections, + }; +} + +function transformIgnoreParentSettings(controlGroupInput: ControlGroupAttributes) { + const { ignoreParentSettings, ...restControlGroupInput } = controlGroupInput; + return { + ...restControlGroupInput, + ignoreParentSettingsJSON: JSON.stringify(ignoreParentSettings), + }; +} + +function transformPanelsJSON(controlGroupInput: ControlGroupAttributes) { + const { controls, ...restControlGroupInput } = controlGroupInput; + const updatedControls = Object.fromEntries( + controls.map(({ controlConfig, id = uuidv4(), ...restOfControl }) => { + return [id, { ...restOfControl, explicitInput: controlConfig }]; + }) + ); + return { + ...restControlGroupInput, + panelsJSON: JSON.stringify(updatedControls), + }; +} diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/index.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/index.ts new file mode 100644 index 0000000000000..9aed8dab85e29 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/index.ts @@ -0,0 +1,12 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { transformControlGroupIn } from './control_group_in_transforms'; +export { transformPanelsIn } from './panels_in_transforms'; +export { transformSearchSourceIn } from './search_source_in_transforms'; diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.test.ts new file mode 100644 index 0000000000000..e8e5e39a79ec7 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.test.ts @@ -0,0 +1,50 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { DashboardPanel } from '../../types'; +import { transformPanelsIn } from './panels_in_transforms'; + +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'mock-uuid'), +})); + +describe('transformPanelsIn', () => { + it('should transform panels', () => { + const panels = [ + { + type: 'foo', + panelIndex: '1', + gridData: { x: 0, y: 0, w: 12, h: 12, i: '1' }, + panelConfig: { foo: 'bar' }, + }, + { + type: 'bar', + gridData: { x: 0, y: 0, w: 12, h: 12 }, + panelConfig: { bizz: 'buzz' }, + }, + ]; + const result = transformPanelsIn(panels as DashboardPanel[]); + expect(result).toEqual( + JSON.stringify([ + { + type: 'foo', + embeddableConfig: { foo: 'bar' }, + panelIndex: '1', + gridData: { x: 0, y: 0, w: 12, h: 12, i: '1' }, + }, + { + type: 'bar', + embeddableConfig: { bizz: 'buzz' }, + panelIndex: 'mock-uuid', + gridData: { x: 0, y: 0, w: 12, h: 12, i: 'mock-uuid' }, + }, + ]) + ); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts new file mode 100644 index 0000000000000..71645e641b6b3 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts @@ -0,0 +1,32 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { v4 as uuidv4 } from 'uuid'; + +import { DashboardSavedObjectAttributes } from '../../../../dashboard_saved_object'; +import { DashboardAttributes } from '../../types'; + +export function transformPanelsIn( + panels: DashboardAttributes['panels'] +): DashboardSavedObjectAttributes['panelsJSON'] { + const updatedPanels = panels.map(({ panelIndex, gridData, panelConfig, ...restPanel }) => { + const idx = panelIndex ?? uuidv4(); + return { + ...restPanel, + embeddableConfig: panelConfig, + panelIndex: idx, + gridData: { + ...gridData, + i: idx, + }, + }; + }); + + return JSON.stringify(updatedPanels); +} diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/search_source_in_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/search_source_in_transforms.ts new file mode 100644 index 0000000000000..c27d1aaf9da83 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/search_source_in_transforms.ts @@ -0,0 +1,17 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { DashboardAttributes } from '../../types'; + +export function transformSearchSourceIn( + kibanaSavedObjectMeta: DashboardAttributes['kibanaSavedObjectMeta'] +) { + const { searchSource } = kibanaSavedObjectMeta; + return { searchSourceJSON: JSON.stringify(searchSource ?? {}) }; +} diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/index.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/index.ts new file mode 100644 index 0000000000000..b0f416249cfce --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/index.ts @@ -0,0 +1,17 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { + transformControlGroupOut, + transformOptionsOut, + transformPanelsOut, + transformSearchSourceOut, +} from './out'; + +export { transformControlGroupIn, transformPanelsIn, transformSearchSourceIn } from './in'; diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/control_group_out_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/control_group_out_transforms.ts new file mode 100644 index 0000000000000..c81593e40675d --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/control_group_out_transforms.ts @@ -0,0 +1,93 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { flow } from 'lodash'; +import { + ControlGroupChainingSystem, + ControlLabelPosition, + DEFAULT_AUTO_APPLY_SELECTIONS, + DEFAULT_CONTROL_CHAINING, + DEFAULT_CONTROL_LABEL_POSITION, + DEFAULT_IGNORE_PARENT_SETTINGS, + type ParentIgnoreSettings, +} from '@kbn/controls-plugin/common'; +import type { DashboardSavedObjectAttributes } from '../../../../dashboard_saved_object'; +import type { ControlGroupAttributes } from '../../types'; +import { transformControlsState } from './control_state_out_transforms'; + +export const transformControlGroupOut: ( + controlGroupInput: NonNullable +) => ControlGroupAttributes = flow( + transformControlGroupSetDefaults, + transformControlGroupProperties +); + +// TODO We may want to remove setting defaults in the future +function transformControlGroupSetDefaults( + controlGroupInput: NonNullable +) { + return { + controlStyle: DEFAULT_CONTROL_LABEL_POSITION, + chainingSystem: DEFAULT_CONTROL_CHAINING, + showApplySelections: !DEFAULT_AUTO_APPLY_SELECTIONS, + ...controlGroupInput, + }; +} + +function transformControlGroupProperties({ + controlStyle, + chainingSystem, + showApplySelections, + ignoreParentSettingsJSON, + panelsJSON, +}: Required< + NonNullable +>): ControlGroupAttributes { + return { + labelPosition: controlStyle as ControlLabelPosition, + chainingSystem: chainingSystem as ControlGroupChainingSystem, + autoApplySelections: !showApplySelections, + ignoreParentSettings: ignoreParentSettingsJSON + ? flow( + JSON.parse, + transformIgnoreParentSettingsSetDefaults, + transformIgnoreParentSettingsProperties + )(ignoreParentSettingsJSON) + : DEFAULT_IGNORE_PARENT_SETTINGS, + controls: panelsJSON ? transformControlsState(panelsJSON) : [], + }; +} + +// TODO We may want to remove setting defaults in the future +function transformIgnoreParentSettingsSetDefaults( + ignoreParentSettings: ParentIgnoreSettings +): ParentIgnoreSettings { + return { + ...DEFAULT_IGNORE_PARENT_SETTINGS, + ...ignoreParentSettings, + }; +} + +/** + * Explicitly extract and provide the expected properties ignoring any unsupported + * properties that may be in the saved object. + */ +function transformIgnoreParentSettingsProperties({ + ignoreFilters, + ignoreQuery, + ignoreTimerange, + ignoreValidations, +}: ParentIgnoreSettings): ParentIgnoreSettings { + return { + ignoreFilters, + ignoreQuery, + ignoreTimerange, + ignoreValidations, + }; +} diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/control_state_out_transforms.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/control_state_out_transforms.test.ts new file mode 100644 index 0000000000000..396d97912e5ea --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/control_state_out_transforms.test.ts @@ -0,0 +1,104 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { DEFAULT_CONTROL_GROW, DEFAULT_CONTROL_WIDTH } from '@kbn/controls-plugin/common'; +import { + transformControlObjectToArray, + transformControlsWidthAuto, + transformControlProperties, + transformControlsSetDefaults, + transformControlsState, +} from './control_state_out_transforms'; + +describe('control_state', () => { + const mockControls = { + control1: { type: 'type1', width: 'auto', explicitInput: { foo: 'bar' } }, + control2: { type: 'type2', width: 'small', explicitInput: { bizz: 'buzz' } }, + control3: { + type: 'type3', + grow: true, + explicitInput: { boo: 'bear' }, + unsupportedProperty: 'unsupported', + }, + }; + + describe('transformControlObjectToArray', () => { + it('should transform control object to array', () => { + const result = transformControlObjectToArray(mockControls); + expect(result).toHaveLength(3); + expect(result).toHaveProperty('0.id', 'control1'); + expect(result).toHaveProperty('1.id', 'control2'); + expect(result).toHaveProperty('2.id', 'control3'); + }); + }); + + describe('transformControlsWidthAuto', () => { + it('should transform controls with width auto to default width and grow = true', () => { + const controlsArray = transformControlObjectToArray(mockControls); + const result = transformControlsWidthAuto(controlsArray); + expect(result).toHaveProperty('0.width', DEFAULT_CONTROL_WIDTH); + expect(result).toHaveProperty('0.grow', true); + }); + }); + + describe('transformControlExplicitInput', () => { + it('should transform controls explicit input', () => { + const controlsArray = transformControlObjectToArray(mockControls); + const result = transformControlProperties(controlsArray); + expect(result).toHaveProperty('0.controlConfig', { foo: 'bar' }); + expect(result).not.toHaveProperty('0.explicitInput'); + + expect(result).toHaveProperty('1.controlConfig', { bizz: 'buzz' }); + expect(result).not.toHaveProperty('1.explicitInput'); + + expect(result).toHaveProperty('2.controlConfig', { boo: 'bear' }); + expect(result).not.toHaveProperty('2.explicitInput'); + expect(result).not.toHaveProperty('2.unsupportedProperty'); + }); + }); + + describe('transformControlsSetDefaults', () => { + it('should set default values for controls', () => { + const controlsArray = transformControlObjectToArray(mockControls); + const result = transformControlsSetDefaults(controlsArray); + expect(result).toHaveProperty('1.grow', DEFAULT_CONTROL_GROW); + expect(result).toHaveProperty('2.width', DEFAULT_CONTROL_WIDTH); + }); + }); + + describe('transformControlsState', () => { + it('should transform serialized control state to array with all transformations applied', () => { + const serializedControlState = JSON.stringify(mockControls); + const result = transformControlsState(serializedControlState); + expect(result).toEqual([ + { + id: 'control1', + type: 'type1', + width: DEFAULT_CONTROL_WIDTH, + grow: true, + controlConfig: { foo: 'bar' }, + }, + { + id: 'control2', + type: 'type2', + width: 'small', + grow: DEFAULT_CONTROL_GROW, + controlConfig: { bizz: 'buzz' }, + }, + { + id: 'control3', + type: 'type3', + width: DEFAULT_CONTROL_WIDTH, + grow: true, + controlConfig: { boo: 'bear' }, + }, + ]); + }); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/control_state_out_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/control_state_out_transforms.ts new file mode 100644 index 0000000000000..2bfc9e91e3d5d --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/control_state_out_transforms.ts @@ -0,0 +1,63 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { flow } from 'lodash'; +import { SerializableRecord } from '@kbn/utility-types'; +import { DEFAULT_CONTROL_GROW, DEFAULT_CONTROL_WIDTH } from '@kbn/controls-plugin/common'; +import { ControlGroupAttributes } from '../../types'; + +/** + * Transform functions for serialized controls state. + */ +export const transformControlsState: ( + serializedControlState: string +) => ControlGroupAttributes['controls'] = flow( + JSON.parse, + transformControlObjectToArray, + transformControlsWidthAuto, + transformControlsSetDefaults, + transformControlProperties +); + +export function transformControlObjectToArray(controls: Record) { + return Object.entries(controls).map(([id, control]) => ({ id, ...control })); +} + +/** + * Some controls were serialized with width set to 'auto'. This function will transform those controls + * to have the default width and grow set to true. See @link https://github.com/elastic/kibana/issues/211113. + */ +export function transformControlsWidthAuto(controls: SerializableRecord[]) { + return controls.map((control) => { + if (control.width === 'auto') { + return { ...control, width: DEFAULT_CONTROL_WIDTH, grow: true }; + } + return control; + }); +} + +// TODO We may want to remove setting defaults in the future +export function transformControlsSetDefaults(controls: SerializableRecord[]) { + return controls.map((control) => ({ + grow: DEFAULT_CONTROL_GROW, + width: DEFAULT_CONTROL_WIDTH, + ...control, + })); +} + +export function transformControlProperties(controls: SerializableRecord[]): SerializableRecord[] { + return controls.map(({ explicitInput, id, type, width, grow, order }) => ({ + controlConfig: explicitInput, + id, + grow, + order, + type, + width, + })); +} diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/index.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/index.ts new file mode 100644 index 0000000000000..9f39767e598ad --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/index.ts @@ -0,0 +1,13 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { transformControlGroupOut } from './control_group_out_transforms'; +export { transformSearchSourceOut } from './search_source_out_transforms'; +export { transformOptionsOut } from './options_out_transforms'; +export { transformPanelsOut } from './panels_out_transforms'; diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/options_out_transforms.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/options_out_transforms.test.ts new file mode 100644 index 0000000000000..9bb72f34aa2fc --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/options_out_transforms.test.ts @@ -0,0 +1,46 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { transformOptionsOut } from './options_out_transforms'; +import { DEFAULT_DASHBOARD_OPTIONS } from '../../../../../common/content_management'; + +describe('transformOptionsOut', () => { + it('should parse JSON and set default options', () => { + const optionsJSON = JSON.stringify({}); + const result = transformOptionsOut(optionsJSON); + expect(result).toEqual(DEFAULT_DASHBOARD_OPTIONS); + }); + + it('should override default options with provided options', () => { + const optionsJSON = JSON.stringify({ hidePanelTitles: true }); + const result = transformOptionsOut(optionsJSON); + expect(result).toEqual({ + ...DEFAULT_DASHBOARD_OPTIONS, + hidePanelTitles: true, + }); + }); + + it('should pick only supported options', () => { + const optionsJSON = JSON.stringify({ + hidePanelTitles: true, + unsupportedOption: 'value', + }); + const result = transformOptionsOut(optionsJSON); + expect(result).toEqual({ + ...DEFAULT_DASHBOARD_OPTIONS, + hidePanelTitles: true, + }); + expect(result).not.toHaveProperty('unsupportedOption'); + }); + + it('should handle invalid JSON gracefully', () => { + const optionsJSON = 'invalid JSON'; + expect(() => transformOptionsOut(optionsJSON)).toThrow(SyntaxError); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/options_out_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/options_out_transforms.ts new file mode 100644 index 0000000000000..0c65160cfb64b --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/options_out_transforms.ts @@ -0,0 +1,40 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { flow } from 'lodash'; +import { DEFAULT_DASHBOARD_OPTIONS } from '../../../../../common/content_management'; +import { DashboardAttributes } from '../../types'; + +export function transformOptionsOut(optionsJSON: string): DashboardAttributes['options'] { + return flow(JSON.parse, transformOptionsSetDefaults, transformOptionsProperties)(optionsJSON); +} + +// TODO We may want to remove setting defaults in the future +function transformOptionsSetDefaults(options: DashboardAttributes['options']) { + return { + ...DEFAULT_DASHBOARD_OPTIONS, + ...options, + }; +} + +function transformOptionsProperties({ + hidePanelTitles, + useMargins, + syncColors, + syncCursor, + syncTooltips, +}: DashboardAttributes['options']) { + return { + hidePanelTitles, + useMargins, + syncColors, + syncCursor, + syncTooltips, + }; +} diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts new file mode 100644 index 0000000000000..1f67c436ec9b4 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts @@ -0,0 +1,31 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { flow } from 'lodash'; +import { SavedDashboardPanel } from '../../../../dashboard_saved_object'; +import { DashboardAttributes } from '../../types'; + +export function transformPanelsOut(panelsJSON: string): DashboardAttributes['panels'] { + return flow(JSON.parse, transformPanelsProperties)(panelsJSON); +} + +function transformPanelsProperties(panels: SavedDashboardPanel[]) { + return panels.map( + ({ embeddableConfig, gridData, id, panelIndex, panelRefName, title, type, version }) => ({ + gridData, + id, + panelConfig: embeddableConfig, + panelIndex, + panelRefName, + title, + type, + version, + }) + ); +} diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/search_source_out_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/search_source_out_transforms.ts new file mode 100644 index 0000000000000..2cf8a88be1e30 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/search_source_out_transforms.ts @@ -0,0 +1,32 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + type Query, + type SerializedSearchSourceFields, + parseSearchSourceJSON, +} from '@kbn/data-plugin/common'; +import { DashboardSavedObjectAttributes } from '../../../../dashboard_saved_object'; +import { DashboardAttributes } from '../../types'; + +export function transformSearchSourceOut( + kibanaSavedObjectMeta: DashboardSavedObjectAttributes['kibanaSavedObjectMeta'] +): DashboardAttributes['kibanaSavedObjectMeta'] { + const { searchSourceJSON } = kibanaSavedObjectMeta; + if (!searchSourceJSON) { + return {}; + } + // Dashboards do not yet support ES|QL (AggregateQuery) in the search source + return { + searchSource: parseSearchSourceJSON(searchSourceJSON) as Omit< + SerializedSearchSourceFields, + 'query' + > & { query?: Query }, + }; +} diff --git a/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/controls_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/controls_migration_smoke_test.ts index 848be6a783849..276bb4542343e 100644 --- a/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/controls_migration_smoke_test.ts +++ b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/controls_migration_smoke_test.ts @@ -65,6 +65,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // There should be 0 error embeddables on the dashboard const errorEmbeddables = await testSubjects.findAll('embeddableStackError'); expect(errorEmbeddables.length).to.be(0); + + // There should be 2 controls on the dashboard and no errors on the controls + expect(await dashboardControls.getControlsCount()).to.be(2); + await testSubjects.missingOrFail('control-frame-error'); }); it('loads all controls from the saved dashboard', async () => {