[Controls] Collect Telemetry (#130498)

* collect telemetry for controls
This commit is contained in:
Devon Thomson 2022-04-20 17:29:07 -04:00 committed by GitHub
parent 0082e0cb06
commit 3b37b27826
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 567 additions and 137 deletions

View file

@ -115,7 +115,7 @@ pageLoadAssetSize:
reporting: 57003
visTypeHeatmap: 25340
expressionGauge: 25000
controls: 34788
controls: 40000
expressionPartitionVis: 26338
sharedUX: 16225
ux: 20784

View file

@ -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<ControlGroupInput, 'id'> => ({
panels: {},
defaultControlWidth: DEFAULT_CONTROL_WIDTH,
controlStyle: DEFAULT_CONTROL_STYLE,
chainingSystem: 'HIERARCHICAL',
ignoreParentSettings: {
ignoreFilters: false,
ignoreQuery: false,
ignoreTimerange: false,
ignoreValidations: false,
},
});

View file

@ -7,21 +7,12 @@
*/
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;
export const controlGroupInputToRawAttributes = (
controlGroupInput: Omit<ControlGroupInput, 'id'>
): RawControlGroupAttributes => {
return {
controlStyle: controlGroupInput.controlStyle,
chainingSystem: controlGroupInput.chainingSystem,
panelsJSON: JSON.stringify(controlGroupInput.panels),
ignoreParentSettingsJSON: JSON.stringify(controlGroupInput.ignoreParentSettings),
};
};
import { pick } from 'lodash';
import { ControlGroupInput } from '..';
import { DEFAULT_CONTROL_STYLE, DEFAULT_CONTROL_WIDTH } from './control_group_constants';
import { PersistableControlGroupInput, RawControlGroupAttributes } from './types';
const safeJSONParse = <OutType>(jsonString?: string): OutType | undefined => {
if (!jsonString && typeof jsonString !== 'string') return;
@ -32,7 +23,48 @@ const safeJSONParse = <OutType>(jsonString?: string): OutType | undefined => {
}
};
export const rawAttributesToControlGroupInput = (
export const getDefaultControlGroupInput = (): Omit<ControlGroupInput, 'id'> => ({
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<ControlGroupInput, 'id'>
): RawControlGroupAttributes => {
return {
controlStyle: controlGroupInput.controlStyle,
chainingSystem: controlGroupInput.chainingSystem,
panelsJSON: JSON.stringify(controlGroupInput.panels),
ignoreParentSettingsJSON: JSON.stringify(controlGroupInput.ignoreParentSettings),
};
};
export const rawControlGroupAttributesToControlGroupInput = (
rawControlGroupAttributes: RawControlGroupAttributes
): Omit<ControlGroupInput, 'id'> | undefined => {
const defaultControlGroupInput = getDefaultControlGroupInput();
@ -50,7 +82,7 @@ export const rawAttributesToControlGroupInput = (
};
};
export const rawAttributesToSerializable = (
export const rawControlGroupAttributesToSerializable = (
rawControlGroupAttributes: Omit<RawControlGroupAttributes, 'id'>
): SerializableRecord => {
const defaultControlGroupInput = getDefaultControlGroupInput();
@ -62,7 +94,7 @@ export const rawAttributesToSerializable = (
};
};
export const serializableToRawAttributes = (
export const serializableToRawControlGroupAttributes = (
serializable: SerializableRecord
): Omit<RawControlGroupAttributes, 'id' | 'type'> => {
return {

View file

@ -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 };
};
};
}

View file

@ -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';

View file

@ -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;

View file

@ -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;

View file

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

View file

@ -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<ControlGroupTelemetry> = {
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<ControlGroupTelemetry> = {
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<ControlGroupTelemetry>(
(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,
});
});
});

View file

@ -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<string, unknown>
): 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;
};

View file

@ -9,3 +9,5 @@
import { ControlsPlugin } from './plugin';
export const plugin = () => new ControlsPlugin();
export { initializeControlGroupTelemetry } from './control_group/control_group_telemetry';

View file

@ -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}`,

View file

@ -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';

View file

@ -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;

View file

@ -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<EmbeddableInput & { [k: string]: unknown }>;
};
controlGroupInput?: DashboardContainerControlGroupInput;
controlGroupInput?: PersistableControlGroupInput;
}

View file

@ -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,

View file

@ -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<ControlGroupInput, 'id'> | undefined => {
if (!dashboardSavedObject.controlGroupInput) return;
return rawAttributesToControlGroupInput(dashboardSavedObject.controlGroupInput);
return rawControlGroupAttributesToControlGroupInput(dashboardSavedObject.controlGroupInput);
};
export const combineDashboardFiltersWithControlGroupFilters = (

View file

@ -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
);

View file

@ -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<DashboardContainerControlGroupInput | undefined>
action: PayloadAction<PersistableControlGroupInput | undefined>
) => {
state.controlGroupInput = action.payload;
},

View file

@ -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;

View file

@ -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<DashboardState, 'panels'> & { panels: Saved
export interface DashboardContainerInput extends ContainerInput {
dashboardCapabilities?: DashboardAppCapabilities;
controlGroupInput?: DashboardContainerControlGroupInput;
controlGroupInput?: PersistableControlGroupInput;
refreshConfig?: RefreshInterval;
isEmbeddedExternally?: boolean;
isFullScreenMode: boolean;

View file

@ -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

View file

@ -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[];

View file

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

View file

@ -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"
}
}
}
}
}
}
}
},