Dashboards: Add RS tracking for Dynamic Dashboards (#111102)
This commit is contained in:
@@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Button, Menu, Stack, Text, useStyles2, Dropdown, Icon, IconButton } from '@grafana/ui';
|
||||
import { trackDeleteDashboardElement } from 'app/features/dashboard/utils/tracking';
|
||||
import { trackDeleteDashboardElement } from 'app/features/dashboard-scene/utils/tracking';
|
||||
|
||||
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { isDashboardV2Resource, isDashboardV2Spec, isV2StoredVersion } from 'app
|
||||
import { dashboardLoaderSrv, DashboardLoaderSrvV2 } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor';
|
||||
import { trackDashboardSceneLoaded } from 'app/features/dashboard/utils/tracking';
|
||||
import { trackDashboardSceneLoaded } from 'app/features/dashboard-scene/utils/tracking';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
import { ProvisioningPreview } from 'app/features/provisioning/types';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAsyncFn } from 'react-use';
|
||||
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@@ -15,6 +15,8 @@ import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { updateDashboardUidLastUsedDatasource } from '../../dashboard/utils/dashboard';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { trackDashboardSceneCreatedOrSaved } from '../utils/tracking';
|
||||
|
||||
export function useSaveDashboard(isCopy = false) {
|
||||
const dispatch = useDispatch();
|
||||
@@ -73,18 +75,14 @@ export function useSaveDashboard(isCopy = false) {
|
||||
appEvents.publish(new DashboardSavedEvent());
|
||||
notifyApp.success(t('dashboard-scene.use-save-dashboard.message-dashboard-saved', 'Dashboard saved'));
|
||||
|
||||
//Update local storage dashboard to handle things like last used datasource
|
||||
updateDashboardUidLastUsedDatasource(resultData.uid);
|
||||
|
||||
if (isCopy) {
|
||||
reportInteraction('grafana_dashboard_copied', {
|
||||
name: saveModel.title,
|
||||
url: resultData.url,
|
||||
});
|
||||
DashboardInteractions.dashboardCopied({ name: saveModel.title || '', url: resultData.url });
|
||||
} else {
|
||||
reportInteraction(`grafana_dashboard_${options.isNew ? 'created' : 'saved'}`, {
|
||||
name: saveModel.title,
|
||||
url: resultData.url,
|
||||
trackDashboardSceneCreatedOrSaved(!!options.isNew, scene, {
|
||||
name: saveModel.title || '',
|
||||
url: resultData.url || '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -716,6 +716,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
return this.serializer.getTrackingInformation(this);
|
||||
}
|
||||
|
||||
public getDynamicDashboardsTrackingInformation() {
|
||||
return this.serializer.getDynamicDashboardsTrackingInformation(this);
|
||||
}
|
||||
|
||||
public async onDashboardDelete() {
|
||||
// Need to mark it non dirty to navigate away without unsaved changes warning
|
||||
this.setState({ isDirty: false });
|
||||
|
||||
@@ -160,21 +160,30 @@ describe('NavToolbarActions', () => {
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is undefined', async () => {
|
||||
setup();
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: false });
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({
|
||||
dashboardUid: 'dash-1',
|
||||
outlineExpanded: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is false', async () => {
|
||||
localStorageMock.setItem('grafana.dashboard.edit-pane.outline.collapsed', 'false');
|
||||
setup();
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: true });
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({
|
||||
dashboardUid: 'dash-1',
|
||||
outlineExpanded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:false if grafana.dashboard.edit-pane.outline.collapsed is true', async () => {
|
||||
localStorageMock.setItem('grafana.dashboard.edit-pane.outline.collapsed', 'true');
|
||||
setup();
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: false });
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({
|
||||
dashboardUid: 'dash-1',
|
||||
outlineExpanded: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -321,7 +321,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
render: () => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
trackDashboardSceneEditButtonClicked();
|
||||
trackDashboardSceneEditButtonClicked(dashboard.state.uid);
|
||||
dashboard.onEnterEditMode();
|
||||
}}
|
||||
tooltip={
|
||||
@@ -347,7 +347,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
render: () => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
trackDashboardSceneEditButtonClicked();
|
||||
trackDashboardSceneEditButtonClicked(dashboard.state.uid);
|
||||
dashboard.onEnterEditMode();
|
||||
dashboard.setState({ editable: true, meta: { ...meta, canEdit: true } });
|
||||
}}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ export const EditDashboardSwitch = ({ dashboard }: ToolbarActionProps) => {
|
||||
evt.stopPropagation();
|
||||
|
||||
if (!dashboard.state.isEditing) {
|
||||
trackDashboardSceneEditButtonClicked();
|
||||
trackDashboardSceneEditButtonClicked(dashboard.state.uid);
|
||||
dashboard.onEnterEditMode();
|
||||
} else {
|
||||
DashboardInteractions.exitEditButtonClicked();
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export const MakeDashboardEditableButton = ({ dashboard }: ToolbarActionProps) =
|
||||
<Button
|
||||
disabled={playlistSrv.state.isPlaying}
|
||||
onClick={() => {
|
||||
trackDashboardSceneEditButtonClicked();
|
||||
trackDashboardSceneEditButtonClicked(dashboard.state.uid);
|
||||
dashboard.onEnterEditMode();
|
||||
dashboard.setState({ editable: true, meta: { ...dashboard.state.meta, canEdit: true } });
|
||||
}}
|
||||
|
||||
+33
-107
@@ -12,7 +12,6 @@ import {
|
||||
defaultSpec as defaultDashboardV2Spec,
|
||||
defaultDataQueryKind,
|
||||
defaultPanelSpec,
|
||||
defaultTimeSettingsSpec,
|
||||
GridLayoutKind,
|
||||
PanelKind,
|
||||
PanelSpec,
|
||||
@@ -25,13 +24,14 @@ import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/Dashboard
|
||||
|
||||
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
||||
import { getTestDashboardSceneFromSaveModel } from '../utils/test-utils';
|
||||
import { findVizPanelByKey } from '../utils/utils';
|
||||
|
||||
import { V1DashboardSerializer, V2DashboardSerializer } from './DashboardSceneSerializer';
|
||||
import { getPanelElement, transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene';
|
||||
import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2';
|
||||
import nestedDashboard from './testfiles/nested_dashboard.json';
|
||||
import { getPanelElement } from './transformSaveModelSchemaV2ToScene';
|
||||
import { transformSaveModelToScene } from './transformSaveModelToScene';
|
||||
import { transformSceneToSaveModel } from './transformSceneToSaveModel';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
@@ -672,30 +672,38 @@ describe('DashboardSceneSerializer', () => {
|
||||
expect(serializer.getTrackingInformation(dashboard)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('provides dashboard tracking information with from initial save model', () => {
|
||||
const dashboard = setupV2({
|
||||
timeSettings: {
|
||||
nowDelay: '10s',
|
||||
from: '',
|
||||
to: '',
|
||||
autoRefresh: '',
|
||||
autoRefreshIntervals: [],
|
||||
hideTimepicker: false,
|
||||
fiscalYearStartMonth: 0,
|
||||
timezone: '',
|
||||
},
|
||||
liveNow: true,
|
||||
});
|
||||
it('provides dashboard tracking information from initial save model', () => {
|
||||
const dashboard = setupV2(nestedDashboard as Partial<DashboardV2Spec>);
|
||||
|
||||
expect(dashboard.getTrackingInformation()).toEqual({
|
||||
uid: 'dashboard-test',
|
||||
title: 'hello',
|
||||
panels_count: 1,
|
||||
panel_type__count: 1,
|
||||
variable_type_custom_count: 1,
|
||||
title: 'Cloudwatch ec2 new layout',
|
||||
panels_count: 6,
|
||||
schemaVersion: DASHBOARD_SCHEMA_VERSION,
|
||||
settings_nowdelay: undefined,
|
||||
settings_livenow: true,
|
||||
schemaVersion: DASHBOARD_SCHEMA_VERSION,
|
||||
variable_type_custom_count: 1,
|
||||
variable_type_query_count: 1,
|
||||
panel_type_timeseries_count: 6,
|
||||
});
|
||||
|
||||
expect(dashboard.getDynamicDashboardsTrackingInformation()).toEqual({
|
||||
panelCount: 6,
|
||||
rowCount: 6,
|
||||
tabCount: 4,
|
||||
templateVariableCount: 2,
|
||||
maxNestingLevel: 3,
|
||||
dashStructure:
|
||||
'[{"kind":"row","children":[{"kind":"row","children":[{"kind":"tab","children":[{"kind":"panel"},{"kind":"panel"},{"kind":"panel"}]},{"kind":"tab","children":[]}]},{"kind":"row","children":[{"kind":"row","children":[{"kind":"panel"}]}]}]},{"kind":"row","children":[{"kind":"row","children":[{"kind":"tab","children":[{"kind":"panel"}]},{"kind":"tab","children":[{"kind":"panel"}]}]}]}]',
|
||||
conditionalRenderRulesCount: 3,
|
||||
autoLayoutCount: 3,
|
||||
customGridLayoutCount: 2,
|
||||
rowsLayoutCount: 4,
|
||||
tabsLayoutCount: 2,
|
||||
panelsByDatasourceType: {
|
||||
cloudwatch: 5,
|
||||
datasource: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1478,87 +1486,5 @@ function setup(override: Partial<Dashboard> = {}) {
|
||||
}
|
||||
|
||||
function setupV2(spec?: Partial<DashboardV2Spec>) {
|
||||
const dashboard = transformSaveModelSchemaV2ToScene({
|
||||
kind: 'DashboardWithAccessInfo',
|
||||
spec: {
|
||||
...defaultDashboardV2Spec(),
|
||||
title: 'hello',
|
||||
timeSettings: {
|
||||
...defaultTimeSettingsSpec(),
|
||||
autoRefresh: '10s',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
},
|
||||
elements: {
|
||||
'panel-1': {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
...defaultPanelSpec(),
|
||||
id: 1,
|
||||
title: 'Panel 1',
|
||||
},
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
kind: 'GridLayout',
|
||||
spec: {
|
||||
items: [
|
||||
{
|
||||
kind: 'GridLayoutItem',
|
||||
spec: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 12,
|
||||
height: 8,
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: 'panel-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
kind: 'CustomVariable',
|
||||
spec: {
|
||||
name: 'app',
|
||||
label: 'Query Variable',
|
||||
description: 'A query variable',
|
||||
skipUrlSync: false,
|
||||
hide: 'dontHide',
|
||||
options: [],
|
||||
multi: false,
|
||||
current: {
|
||||
text: 'app1',
|
||||
value: 'app1',
|
||||
},
|
||||
query: 'app1',
|
||||
allValue: '',
|
||||
includeAll: false,
|
||||
allowCustomValue: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
...spec,
|
||||
},
|
||||
apiVersion: 'v1',
|
||||
metadata: {
|
||||
name: 'dashboard-test',
|
||||
resourceVersion: '1',
|
||||
creationTimestamp: '2023-01-01T00:00:00Z',
|
||||
},
|
||||
access: {
|
||||
canEdit: true,
|
||||
canSave: true,
|
||||
canStar: true,
|
||||
canShare: true,
|
||||
},
|
||||
});
|
||||
|
||||
const initialSaveModel = transformSceneToSaveModelSchemaV2(dashboard);
|
||||
dashboard.setInitialSaveModel(initialSaveModel);
|
||||
|
||||
return dashboard;
|
||||
return getTestDashboardSceneFromSaveModel(spec);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface DashboardSceneSerializerLike<T, M, I = T, E = T | { error: unkn
|
||||
) => DashboardChangeInfo;
|
||||
onSaveComplete(saveModel: T, result: SaveDashboardResponseDTO): void;
|
||||
getTrackingInformation: (s: DashboardScene) => DashboardTrackingInfo | undefined;
|
||||
getDynamicDashboardsTrackingInformation: (s: DashboardScene) => DynamicDashboardsTrackingInformation | undefined;
|
||||
getSnapshotUrl: () => string | undefined;
|
||||
getPanelIdForElement: (elementId: string) => number | undefined;
|
||||
getElementIdForPanel: (panelId: number) => string | undefined;
|
||||
@@ -59,7 +60,7 @@ export interface DashboardSceneSerializerLike<T, M, I = T, E = T | { error: unkn
|
||||
getK8SMetadata: () => Partial<ObjectMeta> | undefined;
|
||||
}
|
||||
|
||||
interface DashboardTrackingInfo {
|
||||
export interface DashboardTrackingInfo {
|
||||
uid?: string;
|
||||
title?: string;
|
||||
schemaVersion: number;
|
||||
@@ -68,6 +69,34 @@ interface DashboardTrackingInfo {
|
||||
settings_livenow?: boolean;
|
||||
}
|
||||
|
||||
export interface DynamicDashboardsTrackingInformation {
|
||||
panelCount: number;
|
||||
rowCount: number;
|
||||
tabCount: number;
|
||||
templateVariableCount: number;
|
||||
maxNestingLevel: number;
|
||||
conditionalRenderRulesCount: number;
|
||||
autoLayoutCount: number;
|
||||
customGridLayoutCount: number;
|
||||
rowsLayoutCount: number;
|
||||
tabsLayoutCount: number;
|
||||
dashStructure: string;
|
||||
panelsByDatasourceType: Record<string, number>;
|
||||
}
|
||||
|
||||
interface DynamicDashboardTrackingInformationStructureNode {
|
||||
kind: string;
|
||||
children?: DynamicDashboardTrackingInformationStructureNode[];
|
||||
}
|
||||
|
||||
interface DynamicDashboardsTrackingInformationLayoutParsing
|
||||
extends Omit<
|
||||
DynamicDashboardsTrackingInformation,
|
||||
'dashStructure' | 'panelsByDatasourceType' | 'templateVariableCount'
|
||||
> {
|
||||
dashStructure: DynamicDashboardTrackingInformationStructureNode[];
|
||||
}
|
||||
|
||||
export interface DSReferencesMapping {
|
||||
panels: Map<string, Set<string>>;
|
||||
variables: Set<string>;
|
||||
@@ -212,6 +241,11 @@ export class V1DashboardSerializer
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getDynamicDashboardsTrackingInformation(): undefined {
|
||||
// We don't have dynamic dashboards in V1 schema
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getSnapshotUrl() {
|
||||
return this.initialSaveModel?.snapshot?.originalUrl;
|
||||
}
|
||||
@@ -407,9 +441,15 @@ export class V2DashboardSerializer
|
||||
|
||||
const panelPluginIds =
|
||||
'elements' in this.initialSaveModel
|
||||
? Object.values(this.initialSaveModel.elements)
|
||||
.filter((e) => e.kind === 'Panel')
|
||||
.map((p) => p.spec.vizConfig.group)
|
||||
? Object.values(this.initialSaveModel.elements).reduce<string[]>((acc, e) => {
|
||||
if (e.kind !== 'Panel') {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push(e.spec.vizConfig.group);
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
: [];
|
||||
const panels = getPanelPluginCounts(panelPluginIds);
|
||||
const variables =
|
||||
@@ -427,6 +467,56 @@ export class V2DashboardSerializer
|
||||
};
|
||||
}
|
||||
|
||||
getDynamicDashboardsTrackingInformation(): DynamicDashboardsTrackingInformation | undefined {
|
||||
if (!this.initialSaveModel || !isDashboardV2Spec(this.initialSaveModel)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dashStructure: DynamicDashboardTrackingInformationStructureNode[] = [];
|
||||
const result = this._parseDynamicDashboardsLayouts(
|
||||
{
|
||||
autoLayoutCount: 0,
|
||||
customGridLayoutCount: 0,
|
||||
rowsLayoutCount: 0,
|
||||
tabsLayoutCount: 0,
|
||||
panelCount: 0,
|
||||
rowCount: 0,
|
||||
tabCount: 0,
|
||||
maxNestingLevel: 0,
|
||||
conditionalRenderRulesCount: 0,
|
||||
dashStructure,
|
||||
},
|
||||
this.initialSaveModel.layout,
|
||||
0,
|
||||
dashStructure
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
dashStructure: JSON.stringify(result.dashStructure),
|
||||
templateVariableCount: this.initialSaveModel.variables?.length ?? 0,
|
||||
panelsByDatasourceType: Object.values(this.initialSaveModel.elements).reduce<Record<string, number>>(
|
||||
(panelsAcc, { kind, spec: panelSpec }) => {
|
||||
if (kind !== 'Panel') {
|
||||
return panelsAcc;
|
||||
}
|
||||
|
||||
return panelSpec.data.spec.queries.reduce((queriesAcc, { spec: querySpec }) => {
|
||||
if (!querySpec.query.datasource) {
|
||||
return queriesAcc;
|
||||
}
|
||||
|
||||
queriesAcc[querySpec.query.group] = queriesAcc[querySpec.query.group] ?? 0;
|
||||
queriesAcc[querySpec.query.group]++;
|
||||
|
||||
return queriesAcc;
|
||||
}, panelsAcc);
|
||||
},
|
||||
{}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
getSnapshotUrl() {
|
||||
return this.metadata?.annotations?.[AnnoKeyDashboardSnapshotOriginalUrl];
|
||||
}
|
||||
@@ -434,6 +524,62 @@ export class V2DashboardSerializer
|
||||
async makeExportableExternally(s: DashboardScene) {
|
||||
return await makeExportableV2(this.getSaveModel(s));
|
||||
}
|
||||
|
||||
private _parseDynamicDashboardsLayouts(
|
||||
result: DynamicDashboardsTrackingInformationLayoutParsing,
|
||||
layout: DashboardV2Spec['layout'],
|
||||
nestingLevel: number,
|
||||
structureTarget: DynamicDashboardTrackingInformationStructureNode[]
|
||||
): DynamicDashboardsTrackingInformationLayoutParsing {
|
||||
result.maxNestingLevel = Math.max(result.maxNestingLevel, nestingLevel);
|
||||
|
||||
switch (layout.kind) {
|
||||
case 'GridLayout':
|
||||
result.customGridLayoutCount++;
|
||||
result.panelCount += layout.spec.items.length;
|
||||
structureTarget.push(...layout.spec.items.map(() => ({ kind: 'panel' })));
|
||||
return result;
|
||||
|
||||
case 'AutoGridLayout':
|
||||
result.autoLayoutCount++;
|
||||
result.panelCount += layout.spec.items.length;
|
||||
structureTarget.push(...layout.spec.items.map(() => ({ kind: 'panel' })));
|
||||
result.conditionalRenderRulesCount = layout.spec.items.reduce(
|
||||
(acc, item) => acc + (item.spec.conditionalRendering?.spec?.items?.length || 0),
|
||||
result.conditionalRenderRulesCount
|
||||
);
|
||||
return result;
|
||||
|
||||
case 'RowsLayout':
|
||||
result.rowsLayoutCount++;
|
||||
result.rowCount += layout.spec.rows.length;
|
||||
const rowsNextingLevel = nestingLevel + 1;
|
||||
return layout.spec.rows.reduce((acc, row) => {
|
||||
acc.conditionalRenderRulesCount += row.spec.conditionalRendering?.spec?.items?.length || 0;
|
||||
const children: DynamicDashboardTrackingInformationStructureNode[] = [];
|
||||
structureTarget.push({ kind: 'row', children });
|
||||
return !row.spec.layout
|
||||
? acc
|
||||
: this._parseDynamicDashboardsLayouts(acc, row.spec.layout, rowsNextingLevel, children);
|
||||
}, result);
|
||||
|
||||
case 'TabsLayout':
|
||||
result.tabsLayoutCount++;
|
||||
result.tabCount += layout.spec.tabs.length;
|
||||
const tabsNextingLevel = nestingLevel + 1;
|
||||
return layout.spec.tabs.reduce((acc, tab) => {
|
||||
acc.conditionalRenderRulesCount += tab.spec.conditionalRendering?.spec?.items?.length || 0;
|
||||
const children: DynamicDashboardTrackingInformationStructureNode[] = [];
|
||||
structureTarget.push({ kind: 'tab', children });
|
||||
return !tab.spec.layout
|
||||
? acc
|
||||
: this._parseDynamicDashboardsLayouts(acc, tab.spec.layout, tabsNextingLevel, children);
|
||||
}, result);
|
||||
|
||||
default:
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getDashboardSceneSerializer(): DashboardSceneSerializerLike<
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,44 @@
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
|
||||
import { DashboardTrackingInfo, DynamicDashboardsTrackingInformation } from '../serialization/DashboardSceneSerializer';
|
||||
|
||||
let isScenesContextSet = false;
|
||||
|
||||
export const DashboardInteractions = {
|
||||
// Dashboard interactions:
|
||||
dashboardInitialized: (properties?: Record<string, unknown>) => {
|
||||
reportDashboardInteraction('init_dashboard_completed', { ...properties });
|
||||
dashboardInitialized: (
|
||||
properties: { theme: undefined; duration: number | undefined; isScene: boolean } & Partial<DashboardTrackingInfo> &
|
||||
Partial<DynamicDashboardsTrackingInformation> &
|
||||
Partial<{ version_before_migration: number | undefined }>
|
||||
) => {
|
||||
reportDashboardInteraction('init_dashboard_completed', properties);
|
||||
},
|
||||
|
||||
dashboardCopied: (properties: { name: string; url: string }) => {
|
||||
reportInteraction('grafana_dashboard_copied', properties);
|
||||
},
|
||||
|
||||
dashboardCreatedOrSaved: (
|
||||
isNew: boolean | undefined,
|
||||
properties:
|
||||
| { name: string; url: string }
|
||||
| {
|
||||
name: string;
|
||||
url: string;
|
||||
numPanels: number;
|
||||
uid: string;
|
||||
conditionalRenderRules: number;
|
||||
autoLayoutCount: number;
|
||||
customGridLayoutCount: number;
|
||||
panelsByDatasourceType: Record<string, number>;
|
||||
}
|
||||
) => {
|
||||
reportDashboardInteraction(isNew ? 'created' : 'saved', properties, 'grafana_dashboard');
|
||||
},
|
||||
|
||||
// grafana_dashboards_edit_button_clicked
|
||||
// when a user clicks the ‘edit’ or ‘make editable’ button in a dashboard view mode
|
||||
editButtonClicked: (properties: { outlineExpanded: boolean }) => {
|
||||
editButtonClicked: (properties: { outlineExpanded: boolean; dashboardUid?: string }) => {
|
||||
reportDashboardInteraction('edit_button_clicked', properties);
|
||||
},
|
||||
|
||||
@@ -192,14 +220,18 @@ export const DashboardInteractions = {
|
||||
},
|
||||
};
|
||||
|
||||
const reportDashboardInteraction: typeof reportInteraction = (name, properties) => {
|
||||
const reportDashboardInteraction = (
|
||||
name: string,
|
||||
properties?: Record<string, unknown>,
|
||||
interactionPrefix = 'dashboards'
|
||||
) => {
|
||||
const meta = isScenesContextSet ? { scenesView: true } : {};
|
||||
const isDynamicDashboard = config.featureToggles?.dashboardNewLayouts ?? false;
|
||||
|
||||
if (properties) {
|
||||
reportInteraction(`dashboards_${name}`, { ...properties, ...meta, isDynamicDashboard });
|
||||
reportInteraction(`${interactionPrefix}_${name}`, { ...properties, ...meta, isDynamicDashboard });
|
||||
} else {
|
||||
reportInteraction(`dashboards_${name}`, { isDynamicDashboard });
|
||||
reportInteraction(`${interactionPrefix}_${name}`, { isDynamicDashboard });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@ import {
|
||||
TestVariable,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import {
|
||||
defaultTimeSettingsSpec,
|
||||
defaultPanelSpec,
|
||||
Spec as DashboardV2Spec,
|
||||
defaultSpec as defaultDashboardV2Spec,
|
||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
|
||||
import { DashboardDTO } from 'app/types/dashboard';
|
||||
@@ -20,6 +26,8 @@ import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
|
||||
import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior';
|
||||
import { transformSaveModelSchemaV2ToScene } from '../serialization/transformSaveModelSchemaV2ToScene';
|
||||
import { transformSceneToSaveModelSchemaV2 } from '../serialization/transformSceneToSaveModelSchemaV2';
|
||||
|
||||
export function setupLoadDashboardMock(rsp: DeepPartial<DashboardDTO>, spy?: jest.Mock) {
|
||||
const loadDashboardMock = (spy || jest.fn()).mockResolvedValue(rsp);
|
||||
@@ -217,3 +225,89 @@ export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel
|
||||
|
||||
return { scene, repeater: withRepeat, row, variable: panelRepeatVariable };
|
||||
}
|
||||
|
||||
export function getTestDashboardSceneFromSaveModel(spec?: Partial<DashboardV2Spec>) {
|
||||
const dashboard = transformSaveModelSchemaV2ToScene({
|
||||
kind: 'DashboardWithAccessInfo',
|
||||
spec: {
|
||||
...defaultDashboardV2Spec(),
|
||||
title: 'hello',
|
||||
timeSettings: {
|
||||
...defaultTimeSettingsSpec(),
|
||||
autoRefresh: '10s',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
},
|
||||
elements: {
|
||||
'panel-1': {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
...defaultPanelSpec(),
|
||||
id: 1,
|
||||
title: 'Panel 1',
|
||||
},
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
kind: 'GridLayout',
|
||||
spec: {
|
||||
items: [
|
||||
{
|
||||
kind: 'GridLayoutItem',
|
||||
spec: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 12,
|
||||
height: 8,
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: 'panel-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
kind: 'CustomVariable',
|
||||
spec: {
|
||||
name: 'app',
|
||||
label: 'Query Variable',
|
||||
description: 'A query variable',
|
||||
skipUrlSync: false,
|
||||
hide: 'dontHide',
|
||||
options: [],
|
||||
multi: false,
|
||||
current: {
|
||||
text: 'app1',
|
||||
value: 'app1',
|
||||
},
|
||||
query: 'app1',
|
||||
allValue: '',
|
||||
includeAll: false,
|
||||
allowCustomValue: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
...spec,
|
||||
},
|
||||
apiVersion: 'v1',
|
||||
metadata: {
|
||||
name: 'dashboard-test',
|
||||
resourceVersion: '1',
|
||||
creationTimestamp: '2023-01-01T00:00:00Z',
|
||||
},
|
||||
access: {
|
||||
canEdit: true,
|
||||
canSave: true,
|
||||
canStar: true,
|
||||
canShare: true,
|
||||
},
|
||||
});
|
||||
|
||||
const initialSaveModel = transformSceneToSaveModelSchemaV2(dashboard);
|
||||
dashboard.setInitialSaveModel(initialSaveModel);
|
||||
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { getPanelPlugin } from '@grafana/data/test';
|
||||
import { reportInteraction, setPluginImportUtils } from '@grafana/runtime';
|
||||
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
|
||||
import nestedDashboard from '../serialization/testfiles/nested_dashboard.json';
|
||||
|
||||
import { getTestDashboardSceneFromSaveModel } from './test-utils';
|
||||
import { trackDashboardSceneCreatedOrSaved, trackDashboardSceneLoaded } from './tracking';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
reportInteraction: jest.fn(),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
featureToggles: {
|
||||
dashboardNewLayouts: true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// mock useSaveDashboardMutation
|
||||
jest.mock('app/features/browse-dashboards/api/browseDashboardsAPI', () => ({
|
||||
useSaveDashboardMutation: () => [() => Promise.resolve({ data: { version: 2, uid: 'new-uid' } })],
|
||||
}));
|
||||
|
||||
setPluginImportUtils({
|
||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||
getPanelPluginFromCache: (id: string) => undefined,
|
||||
});
|
||||
|
||||
export function buildTestScene() {
|
||||
const dashboard = getTestDashboardSceneFromSaveModel(nestedDashboard as Partial<DashboardV2Spec>);
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
describe('dashboard tracking', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('save v2 dashboard tracking', () => {
|
||||
it('should call report interaction with correct parameters when saving a new dashboard', async () => {
|
||||
const scene = buildTestScene();
|
||||
trackDashboardSceneCreatedOrSaved(true, scene, { name: 'new dashboard', url: 'new-url' });
|
||||
expect(reportInteraction).toHaveBeenCalledWith('grafana_dashboard_created', {
|
||||
isDynamicDashboard: true,
|
||||
uid: 'dashboard-test',
|
||||
name: 'new dashboard',
|
||||
url: 'new-url',
|
||||
numPanels: 6,
|
||||
conditionalRenderRules: 3,
|
||||
autoLayoutCount: 3,
|
||||
customGridLayoutCount: 2,
|
||||
panelsByDatasourceType: {
|
||||
cloudwatch: 5,
|
||||
datasource: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('init v2 dashboard tracking', () => {
|
||||
it('should call report interaction with correct parameters when a dashboard has been initialized', async () => {
|
||||
const scene = buildTestScene();
|
||||
trackDashboardSceneLoaded(scene, 42);
|
||||
expect(reportInteraction).toHaveBeenCalledWith('dashboards_init_dashboard_completed', {
|
||||
isDynamicDashboard: true,
|
||||
duration: 42,
|
||||
isScene: true,
|
||||
tabCount: 4,
|
||||
templateVariableCount: 2,
|
||||
maxNestingLevel: 3,
|
||||
panel_type_timeseries_count: 6,
|
||||
panels_count: 6,
|
||||
schemaVersion: 42,
|
||||
settings_livenow: true,
|
||||
settings_nowdelay: undefined,
|
||||
dashStructure:
|
||||
'[{"kind":"row","children":[{"kind":"row","children":[{"kind":"tab","children":[{"kind":"panel"},{"kind":"panel"},{"kind":"panel"}]},{"kind":"tab","children":[]}]},{"kind":"row","children":[{"kind":"row","children":[{"kind":"panel"}]}]}]},{"kind":"row","children":[{"kind":"row","children":[{"kind":"tab","children":[{"kind":"panel"}]},{"kind":"tab","children":[{"kind":"panel"}]}]}]}]',
|
||||
conditionalRenderRules: 3,
|
||||
autoLayoutCount: 3,
|
||||
customGridLayoutCount: 2,
|
||||
theme: undefined,
|
||||
title: 'Cloudwatch ec2 new layout',
|
||||
uid: 'dashboard-test',
|
||||
variable_type_custom_count: 1,
|
||||
variable_type_query_count: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,70 @@
|
||||
import { store } from '@grafana/data';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
|
||||
|
||||
import { DashboardInteractions } from './interactions';
|
||||
|
||||
export const trackDashboardSceneEditButtonClicked = () => {
|
||||
const outlineExpandedByDefault = !store.getBool('grafana.dashboard.edit-pane.outline.collapsed', true);
|
||||
export function trackDashboardSceneLoaded(dashboard: DashboardScene, duration?: number) {
|
||||
const dynamicDashboardsTrackingInformation = dashboard.getDynamicDashboardsTrackingInformation();
|
||||
|
||||
DashboardInteractions.dashboardInitialized({
|
||||
theme: undefined,
|
||||
duration,
|
||||
isScene: true,
|
||||
...(dashboard.getTrackingInformation() ?? {}),
|
||||
...(dynamicDashboardsTrackingInformation
|
||||
? {
|
||||
tabCount: dynamicDashboardsTrackingInformation.tabCount,
|
||||
templateVariableCount: dynamicDashboardsTrackingInformation.templateVariableCount,
|
||||
maxNestingLevel: dynamicDashboardsTrackingInformation.maxNestingLevel,
|
||||
dashStructure: dynamicDashboardsTrackingInformation.dashStructure,
|
||||
conditionalRenderRules: dynamicDashboardsTrackingInformation.conditionalRenderRulesCount,
|
||||
autoLayoutCount: dynamicDashboardsTrackingInformation.autoLayoutCount,
|
||||
customGridLayoutCount: dynamicDashboardsTrackingInformation.customGridLayoutCount,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
export const trackDeleteDashboardElement = (element: EditableDashboardElementInfo) => {
|
||||
switch (element?.typeName) {
|
||||
case 'Row':
|
||||
DashboardInteractions.trackRemoveRowClick();
|
||||
break;
|
||||
case 'Tab':
|
||||
DashboardInteractions.trackRemoveTabClick();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
export const trackDashboardSceneEditButtonClicked = (dashboardUid?: string) => {
|
||||
DashboardInteractions.editButtonClicked({
|
||||
outlineExpanded: outlineExpandedByDefault,
|
||||
outlineExpanded: !store.getBool('grafana.dashboard.edit-pane.outline.collapsed', true),
|
||||
dashboardUid,
|
||||
});
|
||||
};
|
||||
|
||||
export function trackDashboardSceneCreatedOrSaved(
|
||||
isNew: boolean,
|
||||
dashboard: DashboardScene,
|
||||
initialProperties: { name: string; url: string }
|
||||
) {
|
||||
const dynamicDashboardsTrackingInformation = dashboard.getDynamicDashboardsTrackingInformation();
|
||||
|
||||
DashboardInteractions.dashboardCreatedOrSaved(isNew, {
|
||||
...initialProperties,
|
||||
...(dynamicDashboardsTrackingInformation
|
||||
? {
|
||||
uid: dashboard.state.uid,
|
||||
numPanels: dynamicDashboardsTrackingInformation.panelCount,
|
||||
conditionalRenderRules: dynamicDashboardsTrackingInformation.conditionalRenderRulesCount,
|
||||
autoLayoutCount: dynamicDashboardsTrackingInformation.autoLayoutCount,
|
||||
customGridLayoutCount: dynamicDashboardsTrackingInformation.customGridLayoutCount,
|
||||
panelsByDatasourceType: dynamicDashboardsTrackingInformation.panelsByDatasourceType,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,17 +3,19 @@ import { useAsyncFn } from 'react-use';
|
||||
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { updateDashboardName } from 'app/core/reducers/navBarTree';
|
||||
import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
|
||||
import { DashboardSavedEvent } from 'app/types/events';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { updateDashboardUidLastUsedDatasource } from '../../utils/dashboard';
|
||||
import { trackDashboardCreatedOrSaved } from '../../utils/tracking';
|
||||
|
||||
import { SaveDashboardOptions } from './types';
|
||||
|
||||
@@ -57,19 +59,13 @@ export const useDashboardSave = (isCopy = false) => {
|
||||
appEvents.publish(new DashboardSavedEvent());
|
||||
notifyApp.success(t('dashboard.save-dashboard.message-dashboard-saved', 'Dashboard saved'));
|
||||
|
||||
//Update local storage dashboard to handle things like last used datasource
|
||||
// Update local storage dashboard to handle things like last used datasource
|
||||
updateDashboardUidLastUsedDatasource(result.uid);
|
||||
|
||||
if (isCopy) {
|
||||
reportInteraction('grafana_dashboard_copied', {
|
||||
name: dashboard.title,
|
||||
url: result.url,
|
||||
});
|
||||
DashboardInteractions.dashboardCopied({ name: dashboard.title || '', url: result.url });
|
||||
} else {
|
||||
reportInteraction(`grafana_dashboard_${dashboard.id ? 'saved' : 'created'}`, {
|
||||
name: dashboard.title,
|
||||
url: result.url,
|
||||
});
|
||||
trackDashboardCreatedOrSaved(!!dashboard.id, { name: dashboard.title, url: result.url });
|
||||
}
|
||||
|
||||
const currentPath = locationService.getLocation().pathname;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { VariableModel } from '@grafana/schema/dist/esm/index';
|
||||
import { VariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
||||
import { EditableDashboardElementInfo } from 'app/features/dashboard-scene/scene/types/EditableDashboardElement';
|
||||
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
|
||||
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
@@ -28,31 +26,11 @@ export function trackDashboardLoaded(dashboard: DashboardModel, duration?: numbe
|
||||
});
|
||||
}
|
||||
|
||||
export function trackDashboardSceneLoaded(dashboard: DashboardScene, duration?: number) {
|
||||
const trackingInformation = dashboard.getTrackingInformation();
|
||||
|
||||
DashboardInteractions.dashboardInitialized({
|
||||
theme: undefined,
|
||||
duration,
|
||||
isScene: true,
|
||||
...trackingInformation,
|
||||
});
|
||||
export function trackDashboardCreatedOrSaved(isNew: boolean | undefined, trackingProps: { name: string; url: string }) {
|
||||
DashboardInteractions.dashboardCreatedOrSaved(isNew, trackingProps);
|
||||
}
|
||||
|
||||
export const trackDeleteDashboardElement = (element: EditableDashboardElementInfo) => {
|
||||
switch (element?.typeName) {
|
||||
case 'Row':
|
||||
DashboardInteractions.trackRemoveRowClick();
|
||||
break;
|
||||
case 'Tab':
|
||||
DashboardInteractions.trackRemoveTabClick();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
export function getPanelPluginCounts(panels: string[] | string[]) {
|
||||
export function getPanelPluginCounts(panels: string[]) {
|
||||
return panels.reduce((r: Record<string, number>, p) => {
|
||||
r[panelName(p)] = 1 + r[panelName(p)] || 1;
|
||||
return r;
|
||||
|
||||
Reference in New Issue
Block a user