diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 16a99469de7c5..2540976c106f5 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -115,7 +115,7 @@ pageLoadAssetSize: reporting: 57003 visTypeHeatmap: 25340 expressionGauge: 25000 - controls: 34788 + controls: 40000 expressionPartitionVis: 26338 sharedUX: 16225 ux: 20784 diff --git a/src/plugins/controls/common/control_group/control_group_constants.ts b/src/plugins/controls/common/control_group/control_group_constants.ts index 467394614e12c..604e411279bad 100644 --- a/src/plugins/controls/common/control_group/control_group_constants.ts +++ b/src/plugins/controls/common/control_group/control_group_constants.ts @@ -6,21 +6,7 @@ * Side Public License, v 1. */ -import { ControlGroupInput } from '..'; import { ControlStyle, ControlWidth } from '../types'; export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto'; export const DEFAULT_CONTROL_STYLE: ControlStyle = 'oneLine'; - -export const getDefaultControlGroupInput = (): Omit => ({ - panels: {}, - defaultControlWidth: DEFAULT_CONTROL_WIDTH, - controlStyle: DEFAULT_CONTROL_STYLE, - chainingSystem: 'HIERARCHICAL', - ignoreParentSettings: { - ignoreFilters: false, - ignoreQuery: false, - ignoreTimerange: false, - ignoreValidations: false, - }, -}); diff --git a/src/plugins/dashboard/common/embeddable/dashboard_control_group.ts b/src/plugins/controls/common/control_group/control_group_persistence.ts similarity index 65% rename from src/plugins/dashboard/common/embeddable/dashboard_control_group.ts rename to src/plugins/controls/common/control_group/control_group_persistence.ts index 908f50bbadd72..55a7ad4b5a854 100644 --- a/src/plugins/dashboard/common/embeddable/dashboard_control_group.ts +++ b/src/plugins/controls/common/control_group/control_group_persistence.ts @@ -7,12 +7,53 @@ */ import { SerializableRecord } from '@kbn/utility-types'; -import { ControlGroupInput, getDefaultControlGroupInput } from '@kbn/controls-plugin/common'; -import { RawControlGroupAttributes } from '../types'; +import deepEqual from 'fast-deep-equal'; -export const getDefaultDashboardControlGroupInput = getDefaultControlGroupInput; +import { pick } from 'lodash'; +import { ControlGroupInput } from '..'; +import { DEFAULT_CONTROL_STYLE, DEFAULT_CONTROL_WIDTH } from './control_group_constants'; +import { PersistableControlGroupInput, RawControlGroupAttributes } from './types'; -export const controlGroupInputToRawAttributes = ( +const safeJSONParse = (jsonString?: string): OutType | undefined => { + if (!jsonString && typeof jsonString !== 'string') return; + try { + return JSON.parse(jsonString) as OutType; + } catch { + return; + } +}; + +export const getDefaultControlGroupInput = (): Omit => ({ + panels: {}, + defaultControlWidth: DEFAULT_CONTROL_WIDTH, + controlStyle: DEFAULT_CONTROL_STYLE, + chainingSystem: 'HIERARCHICAL', + ignoreParentSettings: { + ignoreFilters: false, + ignoreQuery: false, + ignoreTimerange: false, + ignoreValidations: false, + }, +}); + +export const persistableControlGroupInputIsEqual = ( + a: PersistableControlGroupInput | undefined, + b: PersistableControlGroupInput | undefined +) => { + const defaultInput = getDefaultControlGroupInput(); + const inputA = { + ...defaultInput, + ...pick(a, ['panels', 'chainingSystem', 'controlStyle', 'ignoreParentSettings']), + }; + const inputB = { + ...defaultInput, + ...pick(b, ['panels', 'chainingSystem', 'controlStyle', 'ignoreParentSettings']), + }; + if (deepEqual(inputA, inputB)) return true; + return false; +}; + +export const controlGroupInputToRawControlGroupAttributes = ( controlGroupInput: Omit ): RawControlGroupAttributes => { return { @@ -23,16 +64,7 @@ export const controlGroupInputToRawAttributes = ( }; }; -const safeJSONParse = (jsonString?: string): OutType | undefined => { - if (!jsonString && typeof jsonString !== 'string') return; - try { - return JSON.parse(jsonString) as OutType; - } catch { - return; - } -}; - -export const rawAttributesToControlGroupInput = ( +export const rawControlGroupAttributesToControlGroupInput = ( rawControlGroupAttributes: RawControlGroupAttributes ): Omit | undefined => { const defaultControlGroupInput = getDefaultControlGroupInput(); @@ -50,7 +82,7 @@ export const rawAttributesToControlGroupInput = ( }; }; -export const rawAttributesToSerializable = ( +export const rawControlGroupAttributesToSerializable = ( rawControlGroupAttributes: Omit ): SerializableRecord => { const defaultControlGroupInput = getDefaultControlGroupInput(); @@ -62,7 +94,7 @@ export const rawAttributesToSerializable = ( }; }; -export const serializableToRawAttributes = ( +export const serializableToRawControlGroupAttributes = ( serializable: SerializableRecord ): Omit => { return { diff --git a/src/plugins/controls/common/control_group/types.ts b/src/plugins/controls/common/control_group/types.ts index 4bc5831649d08..9fbfc54b09a17 100644 --- a/src/plugins/controls/common/control_group/types.ts +++ b/src/plugins/controls/common/control_group/types.ts @@ -29,3 +29,36 @@ export interface ControlGroupInput extends EmbeddableInput, ControlInput { controlStyle: ControlStyle; panels: ControlsPanels; } + +// only parts of the Control Group Input should be persisted +export type PersistableControlGroupInput = Pick< + ControlGroupInput, + 'panels' | 'chainingSystem' | 'controlStyle' | 'ignoreParentSettings' +>; + +// panels are json stringified for storage in a saved object. +export type RawControlGroupAttributes = Omit< + PersistableControlGroupInput, + 'panels' | 'ignoreParentSettings' +> & { + ignoreParentSettingsJSON: string; + panelsJSON: string; +}; +export interface ControlGroupTelemetry { + total: number; + chaining_system: { + [key: string]: number; + }; + label_position: { + [key: string]: number; + }; + ignore_settings: { + [key: string]: number; + }; + by_type: { + [key: string]: { + total: number; + details: { [key: string]: number }; + }; + }; +} diff --git a/src/plugins/controls/common/index.ts b/src/plugins/controls/common/index.ts index ff2c39346f075..2956570f79189 100644 --- a/src/plugins/controls/common/index.ts +++ b/src/plugins/controls/common/index.ts @@ -7,14 +7,37 @@ */ export type { ControlWidth } from './types'; -export type { ControlPanelState, ControlsPanels, ControlGroupInput } from './control_group/types'; -export type { OptionsListEmbeddableInput } from './control_types/options_list/types'; -export type { RangeSliderEmbeddableInput } from './control_types/range_slider/types'; -export { CONTROL_GROUP_TYPE } from './control_group/types'; -export { OPTIONS_LIST_CONTROL } from './control_types/options_list/types'; -export { RANGE_SLIDER_CONTROL } from './control_types/range_slider/types'; - -export { getDefaultControlGroupInput } from './control_group/control_group_constants'; +// Control Group exports +export { + CONTROL_GROUP_TYPE, + type ControlPanelState, + type ControlsPanels, + type ControlGroupInput, + type ControlGroupTelemetry, + type RawControlGroupAttributes, + type PersistableControlGroupInput, +} from './control_group/types'; +export { + controlGroupInputToRawControlGroupAttributes, + rawControlGroupAttributesToControlGroupInput, + rawControlGroupAttributesToSerializable, + serializableToRawControlGroupAttributes, + persistableControlGroupInputIsEqual, + getDefaultControlGroupInput, +} from './control_group/control_group_persistence'; +export { + DEFAULT_CONTROL_WIDTH, + DEFAULT_CONTROL_STYLE, +} from './control_group/control_group_constants'; +// Control Type exports +export { + OPTIONS_LIST_CONTROL, + type OptionsListEmbeddableInput, +} from './control_types/options_list/types'; +export { + type RangeSliderEmbeddableInput, + RANGE_SLIDER_CONTROL, +} from './control_types/range_slider/types'; export { TIME_SLIDER_CONTROL } from './control_types/time_slider/types'; diff --git a/src/plugins/controls/public/control_group/editor/control_group_editor.tsx b/src/plugins/controls/public/control_group/editor/control_group_editor.tsx index 95e2066541b5f..8917769f6b151 100644 --- a/src/plugins/controls/public/control_group/editor/control_group_editor.tsx +++ b/src/plugins/controls/public/control_group/editor/control_group_editor.tsx @@ -44,10 +44,7 @@ import { ControlStyle, ControlWidth } from '../../types'; import { ParentIgnoreSettings } from '../..'; import { ControlsPanels } from '../types'; import { ControlGroupInput } from '..'; -import { - DEFAULT_CONTROL_WIDTH, - getDefaultControlGroupInput, -} from '../../../common/control_group/control_group_constants'; +import { DEFAULT_CONTROL_WIDTH, getDefaultControlGroupInput } from '../../../common'; interface EditControlGroupProps { initialInput: ControlGroupInput; diff --git a/src/plugins/controls/public/control_group/embeddable/control_group_container_factory.ts b/src/plugins/controls/public/control_group/embeddable/control_group_container_factory.ts index a8e31e6e2dbec..f55c49101dc40 100644 --- a/src/plugins/controls/public/control_group/embeddable/control_group_container_factory.ts +++ b/src/plugins/controls/public/control_group/embeddable/control_group_container_factory.ts @@ -23,7 +23,7 @@ import { createControlGroupExtract, createControlGroupInject, } from '../../../common/control_group/control_group_persistable_state'; -import { getDefaultControlGroupInput } from '../../../common/control_group/control_group_constants'; +import { getDefaultControlGroupInput } from '../../../common'; export class ControlGroupContainerFactory implements EmbeddableFactoryDefinition { public readonly isContainerType = true; diff --git a/src/plugins/controls/server/control_group/control_group_container_factory.ts b/src/plugins/controls/server/control_group/control_group_container_factory.ts index 806efd652db57..ba50652d3507a 100644 --- a/src/plugins/controls/server/control_group/control_group_container_factory.ts +++ b/src/plugins/controls/server/control_group/control_group_container_factory.ts @@ -14,6 +14,7 @@ import { createControlGroupInject, migrations, } from '../../common/control_group/control_group_persistable_state'; +import { controlGroupTelemetry } from './control_group_telemetry'; export const controlGroupContainerPersistableStateServiceFactory = ( persistableStateService: EmbeddablePersistableStateService @@ -22,6 +23,7 @@ export const controlGroupContainerPersistableStateServiceFactory = ( id: CONTROL_GROUP_TYPE, extract: createControlGroupExtract(persistableStateService), inject: createControlGroupInject(persistableStateService), + telemetry: controlGroupTelemetry, migrations, }; }; diff --git a/src/plugins/controls/server/control_group/control_group_telemetry.test.ts b/src/plugins/controls/server/control_group/control_group_telemetry.test.ts new file mode 100644 index 0000000000000..140b58fd790fc --- /dev/null +++ b/src/plugins/controls/server/control_group/control_group_telemetry.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { ControlGroupTelemetry, RawControlGroupAttributes } from '../../common'; +import { controlGroupTelemetry, initializeControlGroupTelemetry } from './control_group_telemetry'; + +// controls attributes with all settings ignored + 3 options lists + hierarchical chaining + label above +const rawControlAttributes1: RawControlGroupAttributes = { + controlStyle: 'twoLine', + chainingSystem: 'NONE', + panelsJSON: + '{"6fc71ac6-62f9-4ff4-bf5a-d1e066065376":{"order":0,"width":"auto","type":"optionsListControl","explicitInput":{"title":"Carrier","fieldName":"Carrier","id":"6fc71ac6-62f9-4ff4-bf5a-d1e066065376","enhancements":{}}},"1ca90451-908b-4eae-ac4d-535f2e30c4ad":{"order":2,"width":"auto","type":"optionsListControl","explicitInput":{"title":"DestAirportID","fieldName":"DestAirportID","id":"1ca90451-908b-4eae-ac4d-535f2e30c4ad","enhancements":{}}},"71086bac-316d-415f-8aa8-b9a921bc7f58":{"order":1,"width":"auto","type":"optionsListControl","explicitInput":{"title":"DestRegion","fieldName":"DestRegion","id":"71086bac-316d-415f-8aa8-b9a921bc7f58","enhancements":{}}}}', + ignoreParentSettingsJSON: + '{"ignoreFilters":true,"ignoreQuery":true,"ignoreTimerange":true,"ignoreValidations":true}', +}; + +// controls attributes with some settings ignored + 2 range sliders, 1 time slider + No chaining + label inline +const rawControlAttributes2: RawControlGroupAttributes = { + controlStyle: 'oneLine', + chainingSystem: 'NONE', + panelsJSON: + '{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"b47916fd-fc03-4dcd-bef1-5c3b7a315723":{"order":1,"width":"auto","type":"timeSlider","explicitInput":{"title":"timestamp","fieldName":"timestamp","id":"b47916fd-fc03-4dcd-bef1-5c3b7a315723","enhancements":{}}},"f6b076c6-9ef5-483e-b08d-d313d60d4b8c":{"order":2,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceMiles","fieldName":"DistanceMiles","id":"f6b076c6-9ef5-483e-b08d-d313d60d4b8c","enhancements":{}}}}', + ignoreParentSettingsJSON: + '{"ignoreFilters":true,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}', +}; + +// controls attributes with no settings ignored + 2 options lists, 1 range slider, 1 time slider + hierarchical chaining + label inline +const rawControlAttributes3: RawControlGroupAttributes = { + controlStyle: 'oneLine', + chainingSystem: 'HIERARCHICAL', + panelsJSON: + '{"9cf90205-e94d-43c9-a3aa-45f359a7522f":{"order":0,"width":"auto","type":"rangeSliderControl","explicitInput":{"title":"DistanceKilometers","fieldName":"DistanceKilometers","id":"9cf90205-e94d-43c9-a3aa-45f359a7522f","enhancements":{}}},"b47916fd-fc03-4dcd-bef1-5c3b7a315723":{"order":1,"width":"auto","type":"timeSlider","explicitInput":{"title":"timestamp","fieldName":"timestamp","id":"b47916fd-fc03-4dcd-bef1-5c3b7a315723","enhancements":{}}},"ee325e9e-6ec1-41f9-953f-423d59850d44":{"order":2,"width":"auto","type":"optionsListControl","explicitInput":{"title":"Carrier","fieldName":"Carrier","id":"ee325e9e-6ec1-41f9-953f-423d59850d44","enhancements":{}}},"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b":{"order":3,"width":"auto","type":"optionsListControl","explicitInput":{"title":"DestCityName","fieldName":"DestCityName","id":"cb0f5fcd-9ad9-4d4a-b489-b75bd060399b","enhancements":{}}}}', + ignoreParentSettingsJSON: + '{"ignoreFilters":false,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}', +}; + +describe('Initialize telemetry', () => { + test('initializes telemetry when given blank object', () => { + const initializedTelemetry = initializeControlGroupTelemetry({}); + expect(initializedTelemetry.total).toBe(0); + expect(initializedTelemetry.chaining_system).toEqual({}); + expect(initializedTelemetry.ignore_settings).toEqual({}); + expect(initializedTelemetry.by_type).toEqual({}); + }); + + test('initializes telemetry without overwriting any keys when given a partial telemetry object', () => { + const partialTelemetry: Partial = { + total: 77, + chaining_system: { TESTCHAIN: 10, OTHERCHAIN: 1 }, + by_type: { test1: { total: 10, details: {} } }, + }; + const initializedTelemetry = initializeControlGroupTelemetry(partialTelemetry); + expect(initializedTelemetry.total).toBe(77); + expect(initializedTelemetry.chaining_system).toEqual({ TESTCHAIN: 10, OTHERCHAIN: 1 }); + expect(initializedTelemetry.ignore_settings).toEqual({}); + expect(initializedTelemetry.by_type).toEqual({ test1: { total: 10, details: {} } }); + expect(initializedTelemetry.label_position).toEqual({}); + }); + + test('initiailizes telemetry without overwriting any keys when given a completed telemetry object', () => { + const partialTelemetry: Partial = { + total: 5, + chaining_system: { TESTCHAIN: 10, OTHERCHAIN: 1 }, + by_type: { test1: { total: 10, details: {} } }, + ignore_settings: { ignoreValidations: 12 }, + label_position: { inline: 10, above: 12 }, + }; + const initializedTelemetry = initializeControlGroupTelemetry(partialTelemetry); + expect(initializedTelemetry.total).toBe(5); + expect(initializedTelemetry.chaining_system).toEqual({ TESTCHAIN: 10, OTHERCHAIN: 1 }); + expect(initializedTelemetry.ignore_settings).toEqual({ ignoreValidations: 12 }); + expect(initializedTelemetry.by_type).toEqual({ test1: { total: 10, details: {} } }); + expect(initializedTelemetry.label_position).toEqual({ inline: 10, above: 12 }); + }); +}); + +describe('Control group telemetry function', () => { + let finalTelemetry: ControlGroupTelemetry; + + beforeAll(() => { + const allControlGroups = [rawControlAttributes1, rawControlAttributes2, rawControlAttributes3]; + + finalTelemetry = allControlGroups.reduce( + (telemetrySoFar, rawControlGroupAttributes) => { + return controlGroupTelemetry( + rawControlGroupAttributes, + telemetrySoFar + ) as ControlGroupTelemetry; + }, + {} as ControlGroupTelemetry + ); + }); + + test('counts all telemetry over multiple runs', () => { + expect(finalTelemetry.total).toBe(10); + }); + + test('counts control types over multiple runs.', () => { + expect(finalTelemetry.by_type).toEqual({ + optionsListControl: { + details: {}, + total: 5, + }, + rangeSliderControl: { + details: {}, + total: 3, + }, + timeSlider: { + details: {}, + total: 2, + }, + }); + }); + + test('collects ignore settings over multiple runs.', () => { + expect(finalTelemetry.ignore_settings).toEqual({ + ignoreFilters: 2, + ignoreQuery: 1, + ignoreTimerange: 1, + ignoreValidations: 1, + }); + }); + + test('counts various chaining systems over multiple runs.', () => { + expect(finalTelemetry.chaining_system).toEqual({ + HIERARCHICAL: 1, + NONE: 2, + }); + }); + + test('counts label positions over multiple runs.', () => { + expect(finalTelemetry.label_position).toEqual({ + oneLine: 2, + twoLine: 1, + }); + }); +}); diff --git a/src/plugins/controls/server/control_group/control_group_telemetry.ts b/src/plugins/controls/server/control_group/control_group_telemetry.ts new file mode 100644 index 0000000000000..83e363e081241 --- /dev/null +++ b/src/plugins/controls/server/control_group/control_group_telemetry.ts @@ -0,0 +1,122 @@ +/* + * 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 { set } from 'lodash'; +import { PersistableStateService } from '@kbn/kibana-utils-plugin/common'; +import { + ControlGroupTelemetry, + RawControlGroupAttributes, + rawControlGroupAttributesToControlGroupInput, +} from '../../common'; +import { ControlGroupInput } from '../../common/control_group/types'; + +export const initializeControlGroupTelemetry = ( + statsSoFar: Record +): ControlGroupTelemetry => { + return { + total: (statsSoFar?.total as number) ?? 0, + chaining_system: + (statsSoFar?.chaining_system as ControlGroupTelemetry['chaining_system']) ?? {}, + ignore_settings: + (statsSoFar?.ignore_settings as ControlGroupTelemetry['ignore_settings']) ?? {}, + label_position: (statsSoFar?.label_position as ControlGroupTelemetry['label_position']) ?? {}, + by_type: (statsSoFar?.by_type as ControlGroupTelemetry['by_type']) ?? {}, + }; +}; + +const reportChainingSystemInUse = ( + chainingSystemsStats: ControlGroupTelemetry['chaining_system'], + chainingSystem: ControlGroupInput['chainingSystem'] +): ControlGroupTelemetry['chaining_system'] => { + if (!chainingSystem) return chainingSystemsStats; + if (Boolean(chainingSystemsStats[chainingSystem])) { + chainingSystemsStats[chainingSystem]++; + } else { + chainingSystemsStats[chainingSystem] = 1; + } + return chainingSystemsStats; +}; + +const reportLabelPositionsInUse = ( + labelPositionStats: ControlGroupTelemetry['label_position'], + labelPosition: ControlGroupInput['controlStyle'] // controlStyle was renamed labelPosition +): ControlGroupTelemetry['label_position'] => { + if (!labelPosition) return labelPositionStats; + if (Boolean(labelPositionStats[labelPosition])) { + labelPositionStats[labelPosition]++; + } else { + labelPositionStats[labelPosition] = 1; + } + return labelPositionStats; +}; + +const reportIgnoreSettingsInUse = ( + settingsStats: ControlGroupTelemetry['ignore_settings'], + settings: ControlGroupInput['ignoreParentSettings'] +): ControlGroupTelemetry['ignore_settings'] => { + if (!settings) return settingsStats; + for (const [settingKey, settingValue] of Object.entries(settings)) { + if (settingValue) { + // only report ignore settings which are turned ON + const currentValueForSetting = settingsStats[settingKey] ?? 0; + set(settingsStats, settingKey, currentValueForSetting + 1); + } + } + return settingsStats; +}; + +const reportControlTypes = ( + controlTypeStats: ControlGroupTelemetry['by_type'], + panels: ControlGroupInput['panels'] +): ControlGroupTelemetry['by_type'] => { + for (const { type } of Object.values(panels)) { + const currentTypeCount = controlTypeStats[type]?.total ?? 0; + const currentTypeDetails = controlTypeStats[type]?.details ?? {}; + + // here if we need to start tracking details on specific control types, we can call embeddableService.telemetry + + set(controlTypeStats, `${type}.total`, currentTypeCount + 1); + set(controlTypeStats, `${type}.details`, currentTypeDetails); + } + return controlTypeStats; +}; + +export const controlGroupTelemetry: PersistableStateService['telemetry'] = ( + state, + stats +): ControlGroupTelemetry => { + const controlGroupStats = initializeControlGroupTelemetry(stats); + const controlGroupInput = rawControlGroupAttributesToControlGroupInput( + state as unknown as RawControlGroupAttributes + ); + if (!controlGroupInput) return controlGroupStats; + + controlGroupStats.total += Object.keys(controlGroupInput?.panels ?? {}).length; + + controlGroupStats.chaining_system = reportChainingSystemInUse( + controlGroupStats.chaining_system, + controlGroupInput.chainingSystem + ); + + controlGroupStats.label_position = reportLabelPositionsInUse( + controlGroupStats.label_position, + controlGroupInput.controlStyle + ); + + controlGroupStats.ignore_settings = reportIgnoreSettingsInUse( + controlGroupStats.ignore_settings, + controlGroupInput.ignoreParentSettings + ); + + controlGroupStats.by_type = reportControlTypes( + controlGroupStats.by_type, + controlGroupInput.panels + ); + + return controlGroupStats; +}; diff --git a/src/plugins/controls/server/index.ts b/src/plugins/controls/server/index.ts index 5928186715210..cec761ae80e20 100644 --- a/src/plugins/controls/server/index.ts +++ b/src/plugins/controls/server/index.ts @@ -9,3 +9,5 @@ import { ControlsPlugin } from './plugin'; export const plugin = () => new ControlsPlugin(); + +export { initializeControlGroupTelemetry } from './control_group/control_group_telemetry'; diff --git a/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts b/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts index cffe5c9b47042..5346932f7034c 100644 --- a/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts +++ b/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts @@ -12,12 +12,8 @@ import { EmbeddableStateWithType, } from '@kbn/embeddable-plugin/common'; import { SavedObjectReference } from '@kbn/core/types'; -import { CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/common'; -import { - DashboardContainerControlGroupInput, - DashboardContainerStateWithType, - DashboardPanelState, -} from '../types'; +import { CONTROL_GROUP_TYPE, PersistableControlGroupInput } from '@kbn/controls-plugin/common'; +import { DashboardContainerStateWithType, DashboardPanelState } from '../types'; const getPanelStatePrefix = (state: DashboardPanelState) => `${state.explicitInput.id}:`; @@ -95,7 +91,7 @@ export const createInject = ( controlGroupReferences ); workingState.controlGroupInput = - injectedControlGroupState as unknown as DashboardContainerControlGroupInput; + injectedControlGroupState as unknown as PersistableControlGroupInput; } return workingState as EmbeddableStateWithType; @@ -160,7 +156,7 @@ export const createExtract = ( id: controlGroupId, }); workingState.controlGroupInput = - extractedControlGroupState as unknown as DashboardContainerControlGroupInput; + extractedControlGroupState as unknown as PersistableControlGroupInput; const prefixedControlGroupReferences = controlGroupReferences.map((reference) => ({ ...reference, name: `${controlGroupReferencePrefix}${reference.name}`, diff --git a/src/plugins/dashboard/common/index.ts b/src/plugins/dashboard/common/index.ts index e99fe82ffabda..73e01693977d9 100644 --- a/src/plugins/dashboard/common/index.ts +++ b/src/plugins/dashboard/common/index.ts @@ -28,11 +28,3 @@ export { migratePanelsTo730 } from './migrate_to_730_panels'; export const UI_SETTINGS = { ENABLE_LABS_UI: 'labs:dashboard:enable_ui', }; - -export { - controlGroupInputToRawAttributes, - getDefaultDashboardControlGroupInput, - rawAttributesToControlGroupInput, - rawAttributesToSerializable, - serializableToRawAttributes, -} from './embeddable/dashboard_control_group'; diff --git a/src/plugins/dashboard/common/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts index 79339257d7c01..e3a3193dd85a1 100644 --- a/src/plugins/dashboard/common/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -9,11 +9,10 @@ import semverGt from 'semver/functions/gt'; import { SavedObjectAttributes, SavedObjectReference } from '@kbn/core/types'; import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common/types'; import { - DashboardContainerControlGroupInput, - DashboardContainerStateWithType, - DashboardPanelState, + PersistableControlGroupInput, RawControlGroupAttributes, -} from './types'; +} from '@kbn/controls-plugin/common'; +import { DashboardContainerStateWithType, DashboardPanelState } from './types'; import { convertPanelStateToSavedDashboardPanel, convertSavedDashboardPanelToPanelState, @@ -41,7 +40,7 @@ function dashboardAttributesToState(attributes: SavedObjectAttributes): { inputPanels = JSON.parse(attributes.panelsJSON) as SavedDashboardPanel[]; } - let controlGroupInput: DashboardContainerControlGroupInput | undefined; + let controlGroupInput: PersistableControlGroupInput | undefined; if (attributes.controlGroupInput) { const rawControlGroupInput = attributes.controlGroupInput as unknown as RawControlGroupAttributes; diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts index 719df2fbe0be0..941f9437e54e6 100644 --- a/src/plugins/dashboard/common/types.ts +++ b/src/plugins/dashboard/common/types.ts @@ -12,7 +12,7 @@ import { PanelState, } from '@kbn/embeddable-plugin/common/types'; import { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common/lib/saved_object_embeddable'; -import { ControlGroupInput } from '@kbn/controls-plugin/common'; +import { PersistableControlGroupInput } from '@kbn/controls-plugin/common'; import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel610, @@ -23,6 +23,7 @@ import { } from './bwc/types'; import { GridData } from './embeddable/types'; + export type PanelId = string; export type SavedObjectId = string; @@ -98,23 +99,9 @@ export type SavedDashboardPanel730ToLatest = Pick< // Making this interface because so much of the Container type from embeddable is tied up in public // Once that is all available from common, we should be able to move the dashboard_container type to our common as well -// dashboard only persists part of the Control Group Input -export type DashboardContainerControlGroupInput = Pick< - ControlGroupInput, - 'panels' | 'chainingSystem' | 'controlStyle' | 'ignoreParentSettings' ->; - -export type RawControlGroupAttributes = Omit< - DashboardContainerControlGroupInput, - 'panels' | 'ignoreParentSettings' -> & { - ignoreParentSettingsJSON: string; - panelsJSON: string; -}; - export interface DashboardContainerStateWithType extends EmbeddableStateWithType { panels: { [panelId: string]: DashboardPanelState; }; - controlGroupInput?: DashboardContainerControlGroupInput; + controlGroupInput?: PersistableControlGroupInput; } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx index 7009ea72f06a4..1077cf71454f9 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx @@ -16,6 +16,7 @@ import { ControlGroupOutput, CONTROL_GROUP_TYPE, } from '@kbn/controls-plugin/public'; +import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common'; import { DashboardContainerInput } from '../..'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; import type { DashboardContainer, DashboardContainerServices } from './dashboard_container'; @@ -31,8 +32,6 @@ import { createInject, } from '../../../common/embeddable/dashboard_container_persistable_state'; -import { getDefaultDashboardControlGroupInput } from '../../../common/embeddable/dashboard_control_group'; - export type DashboardContainerFactory = EmbeddableFactory< DashboardContainerInput, ContainerOutput, @@ -90,7 +89,7 @@ export class DashboardContainerFactoryDefinition const { filters, query, timeRange, viewMode, controlGroupInput, id } = initialInput; const controlGroup = await controlsGroupFactory?.create({ id: `control_group_${id ?? 'new_dashboard'}`, - ...getDefaultDashboardControlGroupInput(), + ...getDefaultControlGroupInput(), ...pickBy(controlGroupInput, identity), // undefined keys in initialInput should not overwrite defaults timeRange, viewMode, diff --git a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts index 38b1b8eeb9fa1..83497babc2654 100644 --- a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts +++ b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts @@ -11,16 +11,19 @@ import deepEqual from 'fast-deep-equal'; import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; import { debounceTime, distinctUntilChanged, distinctUntilKeyChanged } from 'rxjs/operators'; -import { pick } from 'lodash'; -import { ControlGroupContainer, ControlGroupInput } from '@kbn/controls-plugin/public'; -import { DashboardContainer, DashboardContainerControlGroupInput } from '..'; +import { + ControlGroupInput, + controlGroupInputToRawControlGroupAttributes, + getDefaultControlGroupInput, + persistableControlGroupInputIsEqual, + rawControlGroupAttributesToControlGroupInput, +} from '@kbn/controls-plugin/common'; +import { ControlGroupContainer } from '@kbn/controls-plugin/public'; + +import { DashboardContainer } from '..'; import { DashboardState } from '../../types'; import { DashboardContainerInput, DashboardSavedObject } from '../..'; -import { - controlGroupInputToRawAttributes, - getDefaultDashboardControlGroupInput, - rawAttributesToControlGroupInput, -} from '../../../common'; + interface DiffChecks { [key: string]: (a?: unknown, b?: unknown) => boolean; } @@ -45,7 +48,7 @@ export const syncDashboardControlGroup = async ({ const subscriptions = new Subscription(); const isControlGroupInputEqual = () => - controlGroupInputIsEqual( + persistableControlGroupInputIsEqual( controlGroup.getInput(), dashboardContainer.getInput().controlGroupInput ); @@ -122,7 +125,7 @@ export const syncDashboardControlGroup = async ({ .subscribe(() => { if (!isControlGroupInputEqual()) { if (!dashboardContainer.getInput().controlGroupInput) { - controlGroup.updateInput(getDefaultDashboardControlGroupInput()); + controlGroup.updateInput(getDefaultControlGroupInput()); return; } controlGroup.updateInput({ ...dashboardContainer.getInput().controlGroupInput }); @@ -152,39 +155,22 @@ export const syncDashboardControlGroup = async ({ }; }; -export const controlGroupInputIsEqual = ( - a: DashboardContainerControlGroupInput | undefined, - b: DashboardContainerControlGroupInput | undefined -) => { - const defaultInput = getDefaultDashboardControlGroupInput(); - const inputA = { - ...defaultInput, - ...pick(a, ['panels', 'chainingSystem', 'controlStyle', 'ignoreParentSettings']), - }; - const inputB = { - ...defaultInput, - ...pick(b, ['panels', 'chainingSystem', 'controlStyle', 'ignoreParentSettings']), - }; - if (deepEqual(inputA, inputB)) return true; - return false; -}; - export const serializeControlGroupToDashboardSavedObject = ( dashboardSavedObject: DashboardSavedObject, dashboardState: DashboardState ) => { // only save to saved object if control group is not default if ( - controlGroupInputIsEqual( + persistableControlGroupInputIsEqual( dashboardState.controlGroupInput, - getDefaultDashboardControlGroupInput() + getDefaultControlGroupInput() ) ) { dashboardSavedObject.controlGroupInput = undefined; return; } if (dashboardState.controlGroupInput) { - dashboardSavedObject.controlGroupInput = controlGroupInputToRawAttributes( + dashboardSavedObject.controlGroupInput = controlGroupInputToRawControlGroupAttributes( dashboardState.controlGroupInput ); } @@ -194,7 +180,7 @@ export const deserializeControlGroupFromDashboardSavedObject = ( dashboardSavedObject: DashboardSavedObject ): Omit | undefined => { if (!dashboardSavedObject.controlGroupInput) return; - return rawAttributesToControlGroupInput(dashboardSavedObject.controlGroupInput); + return rawControlGroupAttributesToControlGroupInput(dashboardSavedObject.controlGroupInput); }; export const combineDashboardFiltersWithControlGroupFilters = ( diff --git a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts index 85a8bf54c9266..ec42e18bad858 100644 --- a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts @@ -10,8 +10,8 @@ import { xor, omit, isEmpty } from 'lodash'; import fastIsEqual from 'fast-deep-equal'; import { compareFilters, COMPARE_ALL_OPTIONS, type Filter, isFilterPinned } from '@kbn/es-query'; +import { persistableControlGroupInputIsEqual } from '@kbn/controls-plugin/common'; import { DashboardContainerInput } from '../..'; -import { controlGroupInputIsEqual } from './dashboard_control_group'; import { DashboardOptions, DashboardPanelMap, DashboardState } from '../../types'; import { IEmbeddable } from '../../services/embeddable'; @@ -84,7 +84,7 @@ export const diffDashboardState = async ({ ); const optionsAreEqual = getOptionsAreEqual(originalState.options, newState.options); const filtersAreEqual = getFiltersAreEqual(originalState.filters, newState.filters, true); - const controlGroupIsEqual = controlGroupInputIsEqual( + const controlGroupIsEqual = persistableControlGroupInputIsEqual( originalState.controlGroupInput, newState.controlGroupInput ); diff --git a/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts b/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts index ee403939a9e8c..35f9789023ec5 100644 --- a/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts +++ b/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts @@ -7,11 +7,11 @@ */ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { PersistableControlGroupInput } from '@kbn/controls-plugin/common'; import { Filter, Query, TimeRange } from '../../services/data'; import { ViewMode } from '../../services/embeddable'; import { DashboardOptions, DashboardPanelMap, DashboardState } from '../../types'; -import { DashboardContainerControlGroupInput } from '../embeddable'; export const dashboardStateSlice = createSlice({ name: 'dashboardState', @@ -44,7 +44,7 @@ export const dashboardStateSlice = createSlice({ }, setControlGroupState: ( state, - action: PayloadAction + action: PayloadAction ) => { state.controlGroupInput = action.payload; }, diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts index 23aa9c493e5f4..cf79354719dbc 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts @@ -10,6 +10,7 @@ import { assign, cloneDeep } from 'lodash'; import { SavedObjectsClientContract } from '@kbn/core/public'; import type { ResolvedSimpleSavedObject } from '@kbn/core/public'; import { SavedObjectAttributes, SavedObjectReference } from '@kbn/core/types'; +import { RawControlGroupAttributes } from '@kbn/controls-plugin/common'; import { EmbeddableStart } from '../services/embeddable'; import { SavedObject, SavedObjectsStart } from '../services/saved_objects'; import { Filter, ISearchSource, Query, RefreshInterval } from '../services/data'; @@ -18,7 +19,6 @@ import { createDashboardEditUrl } from '../dashboard_constants'; import { extractReferences, injectReferences } from '../../common/saved_dashboard_references'; import { DashboardOptions } from '../types'; -import { RawControlGroupAttributes } from '../application'; export interface DashboardSavedObject extends SavedObject { id?: string; diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index 249f134239663..e250e44d5abce 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -23,6 +23,7 @@ import { BehaviorSubject, Subject } from 'rxjs'; import { UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; import { VisualizationsStart } from '@kbn/visualizations-plugin/public'; +import { PersistableControlGroupInput } from '@kbn/controls-plugin/common'; import { DataView } from './services/data_views'; import { SharePluginStart } from './services/share'; import { EmbeddableStart } from './services/embeddable'; @@ -30,11 +31,7 @@ import { DashboardSessionStorage } from './application/lib'; import { UsageCollectionSetup } from './services/usage_collection'; import { NavigationPublicPluginStart } from './services/navigation'; import { Query, RefreshInterval, TimeRange } from './services/data'; -import { - DashboardContainerControlGroupInput, - DashboardPanelState, - SavedDashboardPanel, -} from '../common/types'; +import { DashboardPanelState, SavedDashboardPanel } from '../common/types'; import { SavedObjectsTaggingApi } from './services/saved_objects_tagging_oss'; import { DataPublicPluginStart, DataViewsContract } from './services/data'; import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable'; @@ -74,7 +71,7 @@ export interface DashboardState { panels: DashboardPanelMap; timeRange?: TimeRange; - controlGroupInput?: DashboardContainerControlGroupInput; + controlGroupInput?: PersistableControlGroupInput; } /** @@ -84,7 +81,7 @@ export type RawDashboardState = Omit & { panels: Saved export interface DashboardContainerInput extends ContainerInput { dashboardCapabilities?: DashboardAppCapabilities; - controlGroupInput?: DashboardContainerControlGroupInput; + controlGroupInput?: PersistableControlGroupInput; refreshConfig?: RefreshInterval; isEmbeddedExternally?: boolean; isFullScreenMode: boolean; diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index 603fa4af71db4..b3625bec3e8a9 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -22,16 +22,15 @@ import { MigrateFunction, MigrateFunctionsObject, } from '@kbn/kibana-utils-plugin/common'; -import { CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/common'; +import { + CONTROL_GROUP_TYPE, + rawControlGroupAttributesToSerializable, + serializableToRawControlGroupAttributes, +} from '@kbn/controls-plugin/common'; import { migrations730 } from './migrations_730'; import { SavedDashboardPanel } from '../../common/types'; import { migrateMatchAllQuery } from './migrate_match_all_query'; -import { - serializableToRawAttributes, - DashboardDoc700To720, - DashboardDoc730ToLatest, - rawAttributesToSerializable, -} from '../../common'; +import { DashboardDoc700To720, DashboardDoc730ToLatest } from '../../common'; import { injectReferences, extractReferences } from '../../common/saved_dashboard_references'; import { convertPanelStateToSavedDashboardPanel, @@ -221,12 +220,15 @@ const migrateByValuePanels = const { attributes } = doc; if (attributes?.controlGroupInput) { - const controlGroupInput = rawAttributesToSerializable(attributes.controlGroupInput); + const controlGroupInput = rawControlGroupAttributesToSerializable( + attributes.controlGroupInput + ); const migratedControlGroupInput = migrate({ ...controlGroupInput, type: CONTROL_GROUP_TYPE, }); - attributes.controlGroupInput = serializableToRawAttributes(migratedControlGroupInput); + attributes.controlGroupInput = + serializableToRawControlGroupAttributes(migratedControlGroupInput); } // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when diff --git a/src/plugins/dashboard/server/usage/dashboard_telemetry.ts b/src/plugins/dashboard/server/usage/dashboard_telemetry.ts index 9f0c5e19fcd0e..dcb2ad9ba37eb 100644 --- a/src/plugins/dashboard/server/usage/dashboard_telemetry.ts +++ b/src/plugins/dashboard/server/usage/dashboard_telemetry.ts @@ -6,8 +6,16 @@ * Side Public License, v 1. */ +import { isEmpty } from 'lodash'; import { ISavedObjectsRepository, SavedObjectAttributes } from '@kbn/core/server'; import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common'; +import { + type ControlGroupTelemetry, + CONTROL_GROUP_TYPE, + RawControlGroupAttributes, +} from '@kbn/controls-plugin/common'; +import { initializeControlGroupTelemetry } from '@kbn/controls-plugin/server'; + import { SavedDashboardPanel730ToLatest } from '../../common'; import { injectReferences } from '../../common/saved_dashboard_references'; export interface DashboardCollectorData { @@ -26,6 +34,7 @@ export interface DashboardCollectorData { }; }; }; + controls: ControlGroupTelemetry; } export const getEmptyDashboardData = (): DashboardCollectorData => ({ @@ -35,6 +44,7 @@ export const getEmptyDashboardData = (): DashboardCollectorData => ({ by_value: 0, by_type: {}, }, + controls: initializeControlGroupTelemetry({}), }); export const getEmptyPanelTypeData = () => ({ @@ -92,6 +102,19 @@ export async function collectDashboardTelemetry( embeddablePersistableStateService: embeddableService, }); + const controlGroupAttributes: RawControlGroupAttributes | undefined = + attributes.controlGroupInput as unknown as RawControlGroupAttributes; + if (!isEmpty(controlGroupAttributes)) { + collectorData.controls = embeddableService.telemetry( + { + ...controlGroupAttributes, + type: CONTROL_GROUP_TYPE, + id: `DASHBOARD_${CONTROL_GROUP_TYPE}`, + }, + collectorData.controls + ) as ControlGroupTelemetry; + } + const panels = JSON.parse( attributes.panelsJSON as string ) as unknown as SavedDashboardPanel730ToLatest[]; diff --git a/src/plugins/dashboard/server/usage/register_collector.ts b/src/plugins/dashboard/server/usage/register_collector.ts index 7c77dd1473d85..9a7bcd31f8a84 100644 --- a/src/plugins/dashboard/server/usage/register_collector.ts +++ b/src/plugins/dashboard/server/usage/register_collector.ts @@ -60,6 +60,55 @@ export function registerDashboardUsageCollector( }, }, }, + controls: { + total: { type: 'long' }, + by_type: { + DYNAMIC_KEY: { + total: { + type: 'long', + _meta: { + description: 'The number of this type of control in all Control Groups', + }, + }, + details: { + DYNAMIC_KEY: { + type: 'long', + _meta: { + description: + 'Collection of telemetry metrics that embeddable service reports. Will be used for details which are specific to the current control type', + }, + }, + }, + }, + }, + ignore_settings: { + DYNAMIC_KEY: { + type: 'long', + _meta: { + description: + 'Collection of telemetry metrics that count the number of control groups which have this ignore setting turned on', + }, + }, + }, + chaining_system: { + DYNAMIC_KEY: { + type: 'long', + _meta: { + description: + 'Collection of telemetry metrics that count the number of control groups which are using this chaining system', + }, + }, + }, + label_position: { + DYNAMIC_KEY: { + type: 'long', + _meta: { + description: + 'Collection of telemetry metrics that count the number of control groups which have their labels in this position', + }, + }, + }, + }, }, }); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 7e8e570993102..ca9a57bb4462a 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -50,6 +50,67 @@ } } } + }, + "controls": { + "properties": { + "total": { + "type": "long" + }, + "by_type": { + "properties": { + "DYNAMIC_KEY": { + "properties": { + "total": { + "type": "long", + "_meta": { + "description": "The number of this type of control in all Control Groups" + } + }, + "details": { + "properties": { + "DYNAMIC_KEY": { + "type": "long", + "_meta": { + "description": "Collection of telemetry metrics that embeddable service reports. Will be used for details which are specific to the current control type" + } + } + } + } + } + } + } + }, + "ignore_settings": { + "properties": { + "DYNAMIC_KEY": { + "type": "long", + "_meta": { + "description": "Collection of telemetry metrics that count the number of control groups which have this ignore setting turned on" + } + } + } + }, + "chaining_system": { + "properties": { + "DYNAMIC_KEY": { + "type": "long", + "_meta": { + "description": "Collection of telemetry metrics that count the number of control groups which are using this chaining system" + } + } + } + }, + "label_position": { + "properties": { + "DYNAMIC_KEY": { + "type": "long", + "_meta": { + "description": "Collection of telemetry metrics that count the number of control groups which have their labels in this position" + } + } + } + } + } } } },