Schema v2: Make DashboardScene accept v1 and v2 schema (#96931)

* WIP

* Refactor useSaveDashboard to make it dashboard type independent when used

* DashboardPrompt: makeit Dashboard type independent

* DashboardScene: accept both v1 and v2 schema

* Update save dashboard command interface

* Fix test

* Lint

* Refactor

* Add onSaveComplete to DashboardSceneSerializer

* Cleanup tests

* Remove unused code

* Refactor dashboard tracking information

* added a todo

* Fix betterer

* Update betterer results
This commit is contained in:
Dominik Prokop
2024-12-13 10:20:22 +01:00
committed by GitHub
parent 0f52065377
commit f8686be56a
21 changed files with 718 additions and 449 deletions
@@ -24,14 +24,14 @@ describe('DashboardPrompt', () => {
describe('when called without original dashboard', () => {
it('then it should return true', () => {
const scene = buildTestScene();
expect(ignoreChanges(scene, undefined)).toBe(true);
scene.setInitialSaveModel(undefined);
expect(ignoreChanges(scene)).toBe(true);
});
});
describe('when called without current dashboard', () => {
it('then it should return true', () => {
const scene = buildTestScene();
expect(ignoreChanges(null, scene.getInitialSaveModel())).toBe(true);
expect(ignoreChanges(null)).toBe(true);
});
});
@@ -45,7 +45,7 @@ describe('DashboardPrompt', () => {
});
contextSrv.isEditor = false;
expect(ignoreChanges(scene, scene.getInitialSaveModel())).toBe(true);
expect(ignoreChanges(scene)).toBe(true);
});
});
@@ -59,10 +59,11 @@ describe('DashboardPrompt', () => {
},
});
const initialSaveModel = transformSceneToSaveModel(scene);
scene.setInitialSaveModel(initialSaveModel);
contextSrv.isEditor = false;
expect(ignoreChanges(scene, initialSaveModel)).toBe(undefined);
expect(ignoreChanges(scene)).toBe(undefined);
});
});
@@ -75,9 +76,10 @@ describe('DashboardPrompt', () => {
},
});
const initialSaveModel = transformSceneToSaveModel(scene);
scene.setInitialSaveModel(initialSaveModel);
contextSrv.isSignedIn = false;
expect(ignoreChanges(scene, initialSaveModel)).toBe(true);
expect(ignoreChanges(scene)).toBe(true);
});
});
@@ -90,7 +92,9 @@ describe('DashboardPrompt', () => {
},
});
const initialSaveModel = transformSceneToSaveModel(scene);
expect(ignoreChanges(scene, initialSaveModel)).toBe(true);
scene.setInitialSaveModel(initialSaveModel);
expect(ignoreChanges(scene)).toBe(true);
});
});
@@ -104,7 +108,9 @@ describe('DashboardPrompt', () => {
},
});
const initialSaveModel = transformSceneToSaveModel(scene);
expect(ignoreChanges(scene, initialSaveModel)).toBe(true);
scene.setInitialSaveModel(initialSaveModel);
expect(ignoreChanges(scene)).toBe(true);
});
});
@@ -118,7 +124,9 @@ describe('DashboardPrompt', () => {
},
});
const initialSaveModel = transformSceneToSaveModel(scene);
expect(ignoreChanges(scene, initialSaveModel)).toBe(undefined);
scene.setInitialSaveModel(initialSaveModel);
expect(ignoreChanges(scene)).toBe(undefined);
});
});
});
@@ -3,13 +3,12 @@ import * as H from 'history';
import { memo, useContext, useEffect, useMemo } from 'react';
import { locationService } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
import { ModalsContext, Modal, Button, useStyles2 } from '@grafana/ui';
import { Prompt } from 'app/core/components/FormPrompt/Prompt';
import { contextSrv } from 'app/core/services/context_srv';
import { SaveLibraryVizPanelModal } from '../panel-edit/SaveLibraryVizPanelModal';
import { DashboardScene } from '../scene/DashboardScene';
import { DashboardScene, isV2Dashboard } from '../scene/DashboardScene';
import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
interface DashboardPromptProps {
@@ -23,7 +22,7 @@ export const DashboardPrompt = memo(({ dashboard }: DashboardPromptProps) => {
useEffect(() => {
const handleUnload = (event: BeforeUnloadEvent) => {
if (ignoreChanges(dashboard, dashboard.getInitialSaveModel())) {
if (ignoreChanges(dashboard)) {
return;
}
@@ -72,7 +71,7 @@ export const DashboardPrompt = memo(({ dashboard }: DashboardPromptProps) => {
return true;
}
if (ignoreChanges(dashboard, dashboard.getInitialSaveModel())) {
if (ignoreChanges(dashboard)) {
return true;
}
@@ -153,7 +152,13 @@ const getStyles = () => ({
/**
* For some dashboards and users changes should be ignored *
*/
export function ignoreChanges(current: DashboardScene | null, original?: Dashboard) {
export function ignoreChanges(scene: DashboardScene | null) {
const original = scene?.getInitialSaveModel();
if (original && isV2Dashboard(original)) {
throw new Error('isV2Dashboard is not implemented');
}
if (!original) {
return true;
}
@@ -168,11 +173,11 @@ export function ignoreChanges(current: DashboardScene | null, original?: Dashboa
return true;
}
if (!current) {
if (!scene) {
return true;
}
const { canSave, fromScript, fromFile } = current.state.meta;
const { canSave, fromScript, fromFile } = scene.state.meta;
if (!contextSrv.isEditor && !canSave) {
return true;
}
@@ -1,6 +1,6 @@
import { Dashboard } from '@grafana/schema';
import { getDashboardChanges } from './getDashboardChanges';
import { getRawDashboardChanges } from './getDashboardChanges';
function _debounce<T>(f: (...args: T[]) => void, timeout: number) {
let timeoutId: NodeJS.Timeout | undefined = undefined;
@@ -13,6 +13,6 @@ function _debounce<T>(f: (...args: T[]) => void, timeout: number) {
}
self.onmessage = _debounce((e: MessageEvent<{ initial: Dashboard; changed: Dashboard }>) => {
const result = getDashboardChanges(e.data.initial, e.data.changed, false, false, false);
const result = getRawDashboardChanges(e.data.initial, e.data.changed, false, false, false);
self.postMessage(result);
}, 500);
@@ -3,7 +3,6 @@ import { ChangeEvent, useState } from 'react';
import { UseFormSetValue, useForm } from 'react-hook-form';
import { selectors } from '@grafana/e2e-selectors';
import { Dashboard } from '@grafana/schema';
import { Button, Input, Switch, Field, Label, TextArea, Stack, Alert, Box } from '@grafana/ui';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
@@ -52,8 +51,17 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
const onSave = async (overwrite: boolean) => {
const data = getValues();
const dashboardToSave: Dashboard = getSaveAsDashboardSaveModel(changedSaveModel, data, changeInfo.isNew);
const result = await onSaveDashboard(dashboard, dashboardToSave, { overwrite, folderUid: data.folder.uid });
const result = await onSaveDashboard(dashboard, {
overwrite,
folderUid: data.folder.uid,
// save as config
saveAsCopy: true,
isNew: changeInfo.isNew,
copyTags: data.copyTags,
title: data.title,
description: data.description,
});
if (result.status === 'success') {
dashboard.closeModal();
@@ -98,11 +106,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
return (
<form onSubmit={handleSubmit(() => onSave(false))}>
<Field
label={<TitleFieldLabel dashboard={changedSaveModel} onChange={setValue} />}
invalid={!!errors.title}
error={errors.title?.message}
>
<Field label={<TitleFieldLabel onChange={setValue} />} invalid={!!errors.title} error={errors.title?.message}>
<Input
{...register('title', { required: 'Required', validate: validateDashboardName })}
aria-label="Save dashboard title field"
@@ -113,7 +117,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
/>
</Field>
<Field
label={<DescriptionLabel dashboard={changedSaveModel} onChange={setValue} />}
label={<DescriptionLabel onChange={setValue} />}
invalid={!!errors.description}
error={errors.description?.message}
>
@@ -149,7 +153,6 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
}
export interface TitleLabelProps {
dashboard: Dashboard;
onChange: UseFormSetValue<SaveDashboardAsFormDTO>;
}
@@ -168,7 +171,6 @@ export function TitleFieldLabel(props: TitleLabelProps) {
}
export interface DescriptionLabelProps {
dashboard: Dashboard;
onChange: UseFormSetValue<SaveDashboardAsFormDTO>;
}
@@ -198,15 +200,3 @@ async function validateDashboardName(title: string, formValues: SaveDashboardAsF
return e instanceof Error ? e.message : 'Dashboard name is invalid';
}
}
function getSaveAsDashboardSaveModel(source: Dashboard, form: SaveDashboardAsFormDTO, isNew?: boolean): Dashboard {
// TODO remove old alerts and thresholds when copying (See getSaveAsDashboardClone)
return {
...source,
id: null,
uid: '',
title: form.title,
description: form.description,
tags: isNew || form.copyTags ? source.tags : [],
};
}
@@ -7,7 +7,6 @@ import { DashboardScene } from '../scene/DashboardScene';
import { SaveDashboardAsForm } from './SaveDashboardAsForm';
import { SaveDashboardForm } from './SaveDashboardForm';
import { SaveProvisionedDashboardForm } from './SaveProvisionedDashboardForm';
import { getDashboardChangesFromScene } from './getDashboardChangesFromScene';
interface SaveDashboardDrawerState extends SceneObjectState {
dashboardRef: SceneObjectRef<DashboardScene>;
@@ -38,12 +37,11 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
static Component = ({ model }: SceneComponentProps<SaveDashboardDrawer>) => {
const { showDiff, saveAsCopy, saveTimeRange, saveVariables, saveRefresh } = model.useState();
const changeInfo = getDashboardChangesFromScene(
model.state.dashboardRef.resolve(),
saveTimeRange,
saveVariables,
saveRefresh
);
const changeInfo = model.state.dashboardRef
.resolve()
.getDashboardChanges(saveTimeRange, saveVariables, saveRefresh);
const { changedSaveModel, initialSaveModel, diffs, diffCount, hasFolderChanges } = changeInfo;
const changesCount = diffCount + (hasFolderChanges ? 1 : 0);
const dashboard = model.state.dashboardRef.resolve();
@@ -24,7 +24,7 @@ export interface Props {
}
export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
const { changedSaveModel, hasChanges } = changeInfo;
const { hasChanges } = changeInfo;
const { state, onSaveDashboard } = useSaveDashboard(false);
const [options, setOptions] = useState<SaveDashboardOptions>({
@@ -32,7 +32,7 @@ export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
});
const onSave = async (overwrite: boolean) => {
const result = await onSaveDashboard(dashboard, changedSaveModel, { ...options, overwrite });
const result = await onSaveDashboard(dashboard, { ...options, overwrite });
if (result.status === 'success') {
dashboard.closeModal();
drawer.state.onSaveSuccess?.();
@@ -1,7 +1,7 @@
import { AdHocVariableModel } from '@grafana/data';
import { Dashboard, Panel } from '@grafana/schema';
import { adHocVariableFiltersEqual, getDashboardChanges, getPanelChanges } from './getDashboardChanges';
import { adHocVariableFiltersEqual, getRawDashboardChanges, getPanelChanges } from './getDashboardChanges';
describe('adHocVariableFiltersEqual', () => {
it('should compare empty filters', () => {
@@ -134,7 +134,7 @@ describe('getDashboardChanges', () => {
hasRefreshChange: false,
};
const result = getDashboardChanges(initial, changed, false, false, false);
const result = getRawDashboardChanges(initial, changed, false, false, false);
expect(result).toEqual(expectedChanges);
});
@@ -165,7 +165,7 @@ describe('getDashboardChanges', () => {
hasRefreshChange: false,
};
const result = getDashboardChanges(newDashInitial, changed, false, false, false);
const result = getRawDashboardChanges(newDashInitial, changed, false, false, false);
expect(result).toEqual(expectedChanges);
});
@@ -195,7 +195,7 @@ describe('getDashboardChanges', () => {
hasRefreshChange: false,
};
const result = getDashboardChanges(initial, changed, false, false, false);
const result = getRawDashboardChanges(initial, changed, false, false, false);
expect(result).toEqual(expectedChanges);
});
@@ -236,7 +236,7 @@ describe('getDashboardChanges', () => {
hasRefreshChange: false,
};
const result = getDashboardChanges(initial, changed, true, false, false);
const result = getRawDashboardChanges(initial, changed, true, false, false);
expect(result).toEqual(expectedChanges);
});
@@ -263,7 +263,7 @@ describe('getDashboardChanges', () => {
hasRefreshChange: true,
};
const result = getDashboardChanges(initial, changed, false, false, false);
const result = getRawDashboardChanges(initial, changed, false, false, false);
expect(result).toEqual(expectedChanges);
});
@@ -301,7 +301,7 @@ describe('getDashboardChanges', () => {
hasRefreshChange: true,
};
const result = getDashboardChanges(initial, changed, false, false, true);
const result = getRawDashboardChanges(initial, changed, false, false, true);
expect(result).toEqual(expectedChanges);
});
@@ -341,7 +341,7 @@ describe('getDashboardChanges', () => {
hasRefreshChange: false,
};
const result = getDashboardChanges(initial, changed, false, false, false);
const result = getRawDashboardChanges(initial, changed, false, false, false);
expect(result).toEqual(expectedChanges);
});
@@ -392,7 +392,7 @@ describe('getDashboardChanges', () => {
hasRefreshChange: false,
};
const result = getDashboardChanges(initial, changed, false, true, false);
const result = getRawDashboardChanges(initial, changed, false, true, false);
expect(result).toEqual(expectedChanges);
});
@@ -1,8 +1,8 @@
// @ts-ignore
import jsonMap from 'json-source-map';
import type { AdHocVariableModel, TypedVariableModel } from '@grafana/data';
import { Dashboard, Panel, VariableOption } from '@grafana/schema';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
import { jsonDiff } from '../settings/version-history/utils';
@@ -30,7 +30,28 @@ export function isEqual(a: VariableOption | undefined, b: VariableOption | undef
return a === b || (a && b && a.selected === b.selected && deepEqual(a.text, b.text) && deepEqual(a.value, b.value));
}
export function getDashboardChanges(
// TODO[schema v2]
export function getRawDashboardV2Changes(
initial: DashboardV2Spec,
changed: DashboardV2Spec,
saveTimeRange?: boolean,
saveVariables?: boolean,
saveRefresh?: boolean
) {
return {
changedSaveModel: changed,
initialSaveModel: initial,
diffs: jsonDiff(initial, changed),
diffCount: 0,
hasChanges: false,
hasTimeChanges: false,
isNew: false,
hasVariableValueChanges: false,
hasRefreshChange: false,
};
}
export function getRawDashboardChanges(
initial: Dashboard,
changed: Dashboard,
saveTimeRange?: boolean,
@@ -1,287 +0,0 @@
import { config } from '@grafana/runtime';
import {
AdHocFiltersVariable,
GroupByVariable,
MultiValueVariable,
sceneGraph,
SceneRefreshPicker,
} from '@grafana/scenes';
import { VariableModel } from '@grafana/schema';
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { findVizPanelByKey } from '../utils/utils';
import { getDashboardChangesFromScene } from './getDashboardChangesFromScene';
describe('getDashboardChangesFromScene', () => {
it('Can detect no changes', () => {
const dashboard = setup();
const result = getDashboardChangesFromScene(dashboard, false);
expect(result.hasChanges).toBe(false);
expect(result.diffCount).toBe(0);
});
it('Can detect time changed', () => {
const dashboard = setup();
sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' });
const result = getDashboardChangesFromScene(dashboard, false);
expect(result.hasChanges).toBe(false);
expect(result.diffCount).toBe(0);
expect(result.hasTimeChanges).toBe(true);
});
it('Can save time change', () => {
const dashboard = setup();
sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' });
const result = getDashboardChangesFromScene(dashboard, true);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(1);
});
it('Can detect folder change', () => {
const dashboard = setup();
dashboard.state.meta.folderUid = 'folder-2';
const result = getDashboardChangesFromScene(dashboard, false);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(0); // Diff count is 0 because the diff contemplate only the model
expect(result.hasFolderChanges).toBe(true);
});
it('Can detect refresh changed', () => {
const dashboard = setup();
const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker);
if (refreshPicker instanceof SceneRefreshPicker) {
refreshPicker.setState({ refresh: '5s' });
}
const result = getDashboardChangesFromScene(dashboard, false, false, false);
expect(result.hasChanges).toBe(false);
expect(result.diffCount).toBe(0);
expect(result.hasRefreshChange).toBe(true);
});
it('Can save refresh change', () => {
const dashboard = setup();
const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker);
if (refreshPicker instanceof SceneRefreshPicker) {
refreshPicker.setState({ refresh: '5s' });
}
const result = getDashboardChangesFromScene(dashboard, false, false, true);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(1);
});
describe('variable changes', () => {
it('Can detect variable change', () => {
const dashboard = setup();
const appVar = sceneGraph.lookupVariable('app', dashboard) as MultiValueVariable;
appVar.changeValueTo('app2');
const result = getDashboardChangesFromScene(dashboard, false, false);
expect(result.hasVariableValueChanges).toBe(true);
expect(result.hasChanges).toBe(false);
expect(result.diffCount).toBe(0);
});
it('Can save variable value change', () => {
const dashboard = setup();
const appVar = sceneGraph.lookupVariable('app', dashboard) as MultiValueVariable;
appVar.changeValueTo('app2');
const result = getDashboardChangesFromScene(dashboard, false, true);
expect(result.hasVariableValueChanges).toBe(true);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(2);
});
describe('Experimental variables', () => {
beforeAll(() => {
config.featureToggles.groupByVariable = true;
});
afterAll(() => {
config.featureToggles.groupByVariable = false;
});
it('Can detect group by static options change', () => {
const dashboard = transformSaveModelToScene({
dashboard: {
title: 'hello',
uid: 'my-uid',
schemaVersion: 30,
panels: [
{
id: 1,
title: 'Panel 1',
type: 'text',
},
],
version: 10,
templating: {
list: [
{
type: 'groupby',
datasource: {
type: 'ds',
uid: 'ds-uid',
},
name: 'GroupBy',
options: [
{
text: 'Host',
value: 'host',
},
{
text: 'Region',
value: 'region',
},
],
},
],
},
},
meta: {},
});
const initialSaveModel = transformSceneToSaveModel(dashboard);
dashboard.setInitialSaveModel(initialSaveModel);
const variable = sceneGraph.lookupVariable('GroupBy', dashboard) as GroupByVariable;
variable.setState({ defaultOptions: [{ text: 'Host', value: 'host' }] });
const result = getDashboardChangesFromScene(dashboard, false, true);
expect(result.hasVariableValueChanges).toBe(false);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(1);
});
it('Can detect adhoc filter static options change', () => {
const adhocVar = {
id: 'adhoc',
name: 'adhoc',
label: 'Adhoc Label',
description: 'Adhoc Description',
type: 'adhoc',
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
filters: [],
baseFilters: [],
defaultKeys: [
{
text: 'Host',
value: 'host',
},
{
text: 'Region',
value: 'region',
},
],
} as VariableModel;
const dashboard = transformSaveModelToScene({
dashboard: {
title: 'hello',
uid: 'my-uid',
schemaVersion: 30,
panels: [
{
id: 1,
title: 'Panel 1',
type: 'text',
},
],
version: 10,
templating: {
list: [adhocVar],
},
},
meta: {},
});
const initialSaveModel = transformSceneToSaveModel(dashboard);
dashboard.setInitialSaveModel(initialSaveModel);
const variable = sceneGraph.lookupVariable('adhoc', dashboard) as AdHocFiltersVariable;
variable.setState({ defaultKeys: [{ text: 'Host', value: 'host' }] });
const result = getDashboardChangesFromScene(dashboard, false, false);
expect(result.hasVariableValueChanges).toBe(false);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(1);
});
});
});
describe('Saving from panel edit', () => {
it('Should commit panel edit changes', () => {
const dashboard = setup();
const panel = findVizPanelByKey(dashboard, 'panel-1')!;
const editScene = buildPanelEditScene(panel);
dashboard.onEnterEditMode();
dashboard.setState({ editPanel: editScene });
editScene.state.panelRef.resolve().setState({ title: 'changed title' });
const result = getDashboardChangesFromScene(dashboard, false, true);
const panelSaveModel = result.changedSaveModel.panels![0];
expect(panelSaveModel.title).toBe('changed title');
});
});
});
interface ScenarioOptions {
fromPanelEdit?: boolean;
}
function setup(options: ScenarioOptions = {}) {
const dashboard = transformSaveModelToScene({
dashboard: {
title: 'hello',
uid: 'my-uid',
schemaVersion: 30,
panels: [
{
id: 1,
title: 'Panel 1',
type: 'text',
},
],
version: 10,
templating: {
list: [
{
name: 'app',
type: 'custom',
current: {
text: 'app1',
value: 'app1',
},
},
],
},
},
meta: {},
});
const initialSaveModel = transformSceneToSaveModel(dashboard);
dashboard.setInitialSaveModel(initialSaveModel);
return dashboard;
}
@@ -1,35 +0,0 @@
import { DashboardScene } from '../scene/DashboardScene';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { getDashboardChanges as getDashboardSaveModelChanges } from './getDashboardChanges';
/**
* Get changes between the initial save model and the current scene.
* It also checks if the folder has changed.
* @param scene DashboardScene object
* @param saveTimeRange if true, compare the time range
* @param saveVariables if true, compare the variables
* @param saveRefresh if true, compare the refresh interval
* @returns
*/
export function getDashboardChangesFromScene(
scene: DashboardScene,
saveTimeRange?: boolean,
saveVariables?: boolean,
saveRefresh?: boolean
) {
const changeInfo = getDashboardSaveModelChanges(
scene.getInitialSaveModel()!,
transformSceneToSaveModel(scene),
saveTimeRange,
saveVariables,
saveRefresh
);
const hasFolderChanges = scene.getInitialState()?.meta.folderUid !== scene.state.meta.folderUid;
return {
...changeInfo,
hasFolderChanges,
hasChanges: changeInfo.hasChanges || hasFolderChanges,
};
}
@@ -3,14 +3,15 @@ import * as React from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { config, isFetchError } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
import { Alert, Box, Button, Stack } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { Diffs } from '../settings/version-history/utils';
export interface DashboardChangeInfo {
changedSaveModel: Dashboard;
initialSaveModel: Dashboard;
changedSaveModel: Dashboard | DashboardV2Spec;
initialSaveModel: Dashboard | DashboardV2Spec;
diffs: Diffs;
diffCount: number;
hasChanges: boolean;
@@ -7,7 +7,7 @@ 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 { SaveDashboardOptions } from 'app/features/dashboard/components/SaveDashboard/types';
import { SaveDashboardAsOptions, SaveDashboardOptions } from 'app/features/dashboard/components/SaveDashboard/types';
import { useDispatch } from 'app/types';
import { DashboardSavedEvent } from 'app/types/events';
@@ -20,8 +20,26 @@ export function useSaveDashboard(isCopy = false) {
const [saveDashboardRtkQuery] = useSaveDashboardMutation();
const [state, onSaveDashboard] = useAsyncFn(
async (scene: DashboardScene, saveModel: Dashboard, options: SaveDashboardOptions) => {
async (
scene: DashboardScene,
options: SaveDashboardOptions &
SaveDashboardAsOptions & {
// When provided, will take precedence over the scene's save model
rawDashboardJSON?: Dashboard;
}
) => {
{
let saveModel = options.rawDashboardJSON ?? scene.getSaveModel();
if (options.saveAsCopy) {
saveModel = scene.getSaveAsModel({
isNew: options.isNew,
title: options.title,
description: options.description,
copyTags: options.copyTags,
});
}
const result = await saveDashboardRtkQuery({
dashboard: saveModel,
folderUid: options.folderUid,
@@ -37,20 +37,6 @@ import { RowActions } from './row-actions/RowActions';
jest.mock('../settings/version-history/HistorySrv');
jest.mock('../serialization/transformSaveModelToScene');
jest.mock('../saving/getDashboardChangesFromScene', () => ({
// It compares the initial and changed save models and returns the differences
// By default we assume there are differences to have the dirty state test logic tested
getDashboardChangesFromScene: jest.fn(() => ({
changedSaveModel: {},
initialSaveModel: {},
diffs: [],
diffCount: 0,
hasChanges: true,
hasTimeChanges: false,
isNew: false,
hasVariableValueChanges: false,
})),
}));
jest.mock('../serialization/transformSceneToSaveModel');
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
@@ -25,11 +25,13 @@ import {
VizPanel,
} from '@grafana/scenes';
import { Dashboard, DashboardLink, LibraryPanel } from '@grafana/schema';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
import appEvents from 'app/core/app_events';
import { ScrollRefElement } from 'app/core/components/NativeScrollbar';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { getNavModel } from 'app/core/selectors/navModel';
import store from 'app/core/store';
import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
@@ -43,6 +45,8 @@ import { DashboardEditPane } from '../edit-pane/DashboardEditPane';
import { PanelEditor } from '../panel-edit/PanelEditor';
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
import { DashboardChangeInfo } from '../saving/shared';
import { DashboardSceneSerializerLike, getDashboardSceneSerializer } from '../serialization/DashboardSceneSerializer';
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel';
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
@@ -149,10 +153,6 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
* State before editing started
*/
private _initialState?: DashboardSceneState;
/**
* The save model which the scene was originally created from
*/
private _initialSaveModel?: Dashboard;
/**
* Url state before editing started
*/
@@ -172,6 +172,9 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
private _scrollRef?: ScrollRefElement;
private _prevScrollPos?: number;
// TODO: use feature toggle to allow v2 serializer
private _serializer: DashboardSceneSerializerLike<Dashboard | DashboardV2Spec> = getDashboardSceneSerializer(true);
public constructor(state: Partial<DashboardSceneState>) {
super({
title: 'Dashboard',
@@ -261,13 +264,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
this._changeTracker.startTrackingChanges();
};
public saveCompleted(saveModel: Dashboard, result: SaveDashboardResponseDTO, folderUid?: string) {
this._initialSaveModel = {
...saveModel,
id: result.id,
uid: result.uid,
version: result.version,
};
public saveCompleted(saveModel: Dashboard | DashboardV2Spec, result: SaveDashboardResponseDTO, folderUid?: string) {
this._serializer.onSaveComplete(saveModel, result);
this._changeTracker.stopTrackingChanges();
@@ -640,12 +638,16 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
}
public getInitialSaveModel() {
return this._initialSaveModel;
return this._serializer.initialSaveModel;
}
/** Hacky temp function until we refactor transformSaveModelToScene a bit */
public setInitialSaveModel(saveModel: Dashboard) {
this._initialSaveModel = saveModel;
public setInitialSaveModel(saveModel?: Dashboard | DashboardV2Spec) {
this._serializer.initialSaveModel = saveModel;
}
public getTrackingInformation() {
return this._serializer.getTrackingInformation();
}
public async onDashboardDelete() {
@@ -688,6 +690,18 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
this._scrollRef?.scrollTo(0, this._prevScrollPos!);
}
}
getSaveModel(): Dashboard | DashboardV2Spec {
return this._serializer.getSaveModel(this);
}
getSaveAsModel(options: SaveDashboardAsOptions): Dashboard | DashboardV2Spec {
return this._serializer.getSaveAsModel(this, options);
}
getDashboardChanges(saveTimeRange?: boolean, saveVariables?: boolean, saveRefresh?: boolean): DashboardChangeInfo {
return this._serializer.getDashboardChangesFromScene(this, { saveTimeRange, saveVariables, saveRefresh });
}
}
export class DashboardVariableDependency implements SceneVariableDependencyConfigLike {
@@ -751,3 +765,7 @@ export class DashboardVariableDependency implements SceneVariableDependencyConfi
}
}
}
export function isV2Dashboard(model: Dashboard | DashboardV2Spec): model is DashboardV2Spec {
return 'elements' in model;
}
@@ -34,7 +34,7 @@ import { DashboardInteractions } from '../utils/interactions';
import { DynamicDashNavButtonModel, dynamicDashNavActions } from '../utils/registerDynamicDashNavAction';
import { isLibraryPanel } from '../utils/utils';
import { DashboardScene } from './DashboardScene';
import { DashboardScene, isV2Dashboard } from './DashboardScene';
import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton';
interface Props {
@@ -140,12 +140,17 @@ export function ToolbarActions({ dashboard }: Props) {
toolbarActions.push({
group: 'icon-actions',
condition: meta.isSnapshot && !meta.dashboardNotFound && !isEditing,
render: () => (
<GoToSnapshotOriginButton
key="go-to-snapshot-origin"
originalURL={dashboard.getInitialSaveModel()?.snapshot?.originalUrl ?? ''}
/>
),
render: () => {
const saveModel = dashboard.getInitialSaveModel();
if (saveModel && isV2Dashboard(saveModel)) {
throw new Error('v2 schema not implemented');
}
return (
<GoToSnapshotOriginButton key="go-to-snapshot-origin" originalURL={saveModel?.snapshot?.originalUrl ?? ''} />
);
},
});
if (!isEditingPanel && !isEditing) {
@@ -0,0 +1,384 @@
import { config } from '@grafana/runtime';
import {
AdHocFiltersVariable,
GroupByVariable,
MultiValueVariable,
sceneGraph,
SceneRefreshPicker,
} from '@grafana/scenes';
import { Dashboard, VariableModel } from '@grafana/schema';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { findVizPanelByKey } from '../utils/utils';
import { V1DashboardSerializer, V2DashboardSerializer } from './DashboardSceneSerializer';
describe('DashboardSceneSerializer', () => {
describe('v1 schema', () => {
it('Can detect no changes', () => {
const dashboard = setup();
const result = dashboard.getDashboardChanges(false);
expect(result.hasChanges).toBe(false);
expect(result.diffCount).toBe(0);
});
it('Can detect time changed', () => {
const dashboard = setup();
sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' });
const result = dashboard.getDashboardChanges(false);
expect(result.hasChanges).toBe(false);
expect(result.diffCount).toBe(0);
expect(result.hasTimeChanges).toBe(true);
});
it('Can save time change', () => {
const dashboard = setup();
sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' });
const result = dashboard.getDashboardChanges(true);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(1);
});
it('Can detect folder change', () => {
const dashboard = setup();
dashboard.state.meta.folderUid = 'folder-2';
const result = dashboard.getDashboardChanges(false);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(0); // Diff count is 0 because the diff contemplate only the model
expect(result.hasFolderChanges).toBe(true);
});
it('Can detect refresh changed', () => {
const dashboard = setup();
const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker);
if (refreshPicker instanceof SceneRefreshPicker) {
refreshPicker.setState({ refresh: '5s' });
}
const result = dashboard.getDashboardChanges(false, false, false);
expect(result.hasChanges).toBe(false);
expect(result.diffCount).toBe(0);
expect(result.hasRefreshChange).toBe(true);
});
it('Can save refresh change', () => {
const dashboard = setup();
const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker);
if (refreshPicker instanceof SceneRefreshPicker) {
refreshPicker.setState({ refresh: '5s' });
}
const result = dashboard.getDashboardChanges(false, false, true);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(1);
});
describe('variable changes', () => {
it('Can detect variable change', () => {
const dashboard = setup();
const appVar = sceneGraph.lookupVariable('app', dashboard) as MultiValueVariable;
appVar.changeValueTo('app2');
const result = dashboard.getDashboardChanges(false, false);
expect(result.hasVariableValueChanges).toBe(true);
expect(result.hasChanges).toBe(false);
expect(result.diffCount).toBe(0);
});
it('Can save variable value change', () => {
const dashboard = setup();
const appVar = sceneGraph.lookupVariable('app', dashboard) as MultiValueVariable;
appVar.changeValueTo('app2');
const result = dashboard.getDashboardChanges(false, true);
expect(result.hasVariableValueChanges).toBe(true);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(2);
});
describe('Experimental variables', () => {
beforeAll(() => {
config.featureToggles.groupByVariable = true;
});
afterAll(() => {
config.featureToggles.groupByVariable = false;
});
it('Can detect group by static options change', () => {
const dashboard = transformSaveModelToScene({
dashboard: {
title: 'hello',
uid: 'my-uid',
schemaVersion: 30,
panels: [
{
id: 1,
title: 'Panel 1',
type: 'text',
},
],
version: 10,
templating: {
list: [
{
type: 'groupby',
datasource: {
type: 'ds',
uid: 'ds-uid',
},
name: 'GroupBy',
options: [
{
text: 'Host',
value: 'host',
},
{
text: 'Region',
value: 'region',
},
],
},
],
},
},
meta: {},
});
const initialSaveModel = transformSceneToSaveModel(dashboard);
dashboard.setInitialSaveModel(initialSaveModel);
const variable = sceneGraph.lookupVariable('GroupBy', dashboard) as GroupByVariable;
variable.setState({ defaultOptions: [{ text: 'Host', value: 'host' }] });
const result = dashboard.getDashboardChanges(false, true);
expect(result.hasVariableValueChanges).toBe(false);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(1);
});
it('Can detect adhoc filter static options change', () => {
const adhocVar = {
id: 'adhoc',
name: 'adhoc',
label: 'Adhoc Label',
description: 'Adhoc Description',
type: 'adhoc',
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
filters: [],
baseFilters: [],
defaultKeys: [
{
text: 'Host',
value: 'host',
},
{
text: 'Region',
value: 'region',
},
],
} as VariableModel;
const dashboard = transformSaveModelToScene({
dashboard: {
title: 'hello',
uid: 'my-uid',
schemaVersion: 30,
panels: [
{
id: 1,
title: 'Panel 1',
type: 'text',
},
],
version: 10,
templating: {
list: [adhocVar],
},
},
meta: {},
});
const initialSaveModel = transformSceneToSaveModel(dashboard);
dashboard.setInitialSaveModel(initialSaveModel);
const variable = sceneGraph.lookupVariable('adhoc', dashboard) as AdHocFiltersVariable;
variable.setState({ defaultKeys: [{ text: 'Host', value: 'host' }] });
const result = dashboard.getDashboardChanges(false, false);
expect(result.hasVariableValueChanges).toBe(false);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(1);
});
});
});
describe('Saving from panel edit', () => {
it('Should commit panel edit changes', () => {
const dashboard = setup();
const panel = findVizPanelByKey(dashboard, 'panel-1')!;
const editScene = buildPanelEditScene(panel);
dashboard.onEnterEditMode();
dashboard.setState({ editPanel: editScene });
editScene.state.panelRef.resolve().setState({ title: 'changed title' });
const result = dashboard.getDashboardChanges(false, true);
const panelSaveModel = (result.changedSaveModel as Dashboard).panels![0];
expect(panelSaveModel.title).toBe('changed title');
});
});
describe('tracking information', () => {
it('provides dashboard tracking information with no initial save model', () => {
const serializer = new V1DashboardSerializer();
expect(serializer.getTrackingInformation()).toBe(undefined);
});
it('provides dashboard tracking information with from initial save model', () => {
const serializer = new V1DashboardSerializer();
serializer.initialSaveModel = {
schemaVersion: 30,
version: 10,
uid: 'my-uid',
title: 'hello',
liveNow: true,
panels: [
{
type: 'text',
},
{
type: 'text',
},
{
type: 'timeseries',
},
],
templating: {
list: [
{
type: 'query',
name: 'server',
},
{
type: 'query',
name: 'host',
},
{
type: 'textbox',
name: 'search',
},
],
},
};
expect(serializer.getTrackingInformation()).toEqual({
uid: 'my-uid',
title: 'hello',
schemaVersion: 30,
panels_count: 3,
panel_type_text_count: 2,
panel_type_timeseries_count: 1,
variable_type_query_count: 2,
variable_type_textbox_count: 1,
settings_nowdelay: undefined,
settings_livenow: true,
version_before_migration: 10,
});
});
});
});
describe('v2 schema', () => {
it('should throw on getSaveAsModel', () => {
const serializer = new V2DashboardSerializer();
const dashboard = setup();
expect(() => serializer.getSaveAsModel(dashboard, {})).toThrow('Method not implemented.');
});
it('should throw on getDashboardChangesFromScene', () => {
const serializer = new V2DashboardSerializer();
const dashboard = setup();
expect(() => serializer.getDashboardChangesFromScene(dashboard)).toThrow('Method not implemented.');
});
it('should throw on onSaveComplete', () => {
const serializer = new V2DashboardSerializer();
expect(() =>
serializer.onSaveComplete({} as DashboardV2Spec, {
id: 1,
uid: 'aa',
slug: 'slug',
url: 'url',
version: 2,
status: 'status',
})
).toThrow('Method not implemented.');
});
it('should throw on getDashboardChangesFromScene', () => {
const serializer = new V2DashboardSerializer();
expect(() => serializer.getTrackingInformation()).toThrow('Method not implemented.');
});
});
});
interface ScenarioOptions {
fromPanelEdit?: boolean;
}
function setup(options: ScenarioOptions = {}) {
const dashboard = transformSaveModelToScene({
dashboard: {
title: 'hello',
uid: 'my-uid',
schemaVersion: 30,
panels: [
{
id: 1,
title: 'Panel 1',
type: 'text',
},
],
version: 10,
templating: {
list: [
{
name: 'app',
type: 'custom',
current: {
text: 'app1',
value: 'app1',
},
},
],
},
},
meta: {},
});
const initialSaveModel = transformSceneToSaveModel(dashboard);
dashboard.setInitialSaveModel(initialSaveModel);
return dashboard;
}
@@ -0,0 +1,158 @@
import { config } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types';
import { getV1SchemaPanelCounts, getV1SchemaVariables } from 'app/features/dashboard/utils/tracking';
import { SaveDashboardResponseDTO } from 'app/types';
import { getRawDashboardChanges } from '../saving/getDashboardChanges';
import { DashboardChangeInfo } from '../saving/shared';
import { DashboardScene } from '../scene/DashboardScene';
import { transformSceneToSaveModel } from './transformSceneToSaveModel';
import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2';
export interface DashboardSceneSerializerLike<T> {
/**
* The save model which the dashboard scene was originally created from
*/
initialSaveModel?: T;
getSaveModel: (s: DashboardScene) => T;
getSaveAsModel: (s: DashboardScene, options: SaveDashboardAsOptions) => T;
getDashboardChangesFromScene: (
scene: DashboardScene,
options: {
saveTimeRange?: boolean;
saveVariables?: boolean;
saveRefresh?: boolean;
}
) => DashboardChangeInfo;
onSaveComplete(saveModel: T, result: SaveDashboardResponseDTO): void;
getTrackingInformation: () => DashboardTrackingInfo | undefined;
}
interface DashboardTrackingInfo {
uid?: string;
title?: string;
schemaVersion: number;
version_before_migration?: number;
panels_count: number;
settings_nowdelay?: number;
settings_livenow?: boolean;
}
export class V1DashboardSerializer implements DashboardSceneSerializerLike<Dashboard> {
initialSaveModel?: Dashboard;
getSaveModel(s: DashboardScene) {
return transformSceneToSaveModel(s);
}
getSaveAsModel(s: DashboardScene, options: SaveDashboardAsOptions) {
const saveModel = this.getSaveModel(s);
return {
...saveModel,
id: null,
uid: '',
title: options.title || '',
description: options.description || '',
tags: options.isNew || options.copyTags ? saveModel.tags : [],
};
}
getDashboardChangesFromScene(
scene: DashboardScene,
options: { saveTimeRange?: boolean; saveVariables?: boolean; saveRefresh?: boolean }
) {
const changedSaveModel = this.getSaveModel(scene);
const changeInfo = getRawDashboardChanges(
this.initialSaveModel!,
changedSaveModel,
options.saveTimeRange,
options.saveVariables,
options.saveRefresh
);
const hasFolderChanges = scene.getInitialState()?.meta.folderUid !== scene.state.meta.folderUid;
return {
...changeInfo,
hasFolderChanges,
hasChanges: changeInfo.hasChanges || hasFolderChanges,
};
}
onSaveComplete(saveModel: Dashboard, result: SaveDashboardResponseDTO): void {
this.initialSaveModel = {
...saveModel,
id: result.id,
uid: result.uid,
version: result.version,
};
}
getTrackingInformation(): DashboardTrackingInfo | undefined {
const panels = getV1SchemaPanelCounts(this.initialSaveModel?.panels || []);
const variables = getV1SchemaVariables(this.initialSaveModel?.templating?.list || []);
if (this.initialSaveModel) {
return {
uid: this.initialSaveModel.uid,
title: this.initialSaveModel.title,
schemaVersion: this.initialSaveModel.schemaVersion,
version_before_migration: this.initialSaveModel.version,
panels_count: this.initialSaveModel.panels?.length || 0,
settings_nowdelay: undefined,
settings_livenow: !!this.initialSaveModel.liveNow,
...panels,
...variables,
};
}
return undefined;
}
}
export class V2DashboardSerializer implements DashboardSceneSerializerLike<DashboardV2Spec> {
initialSaveModel?: DashboardV2Spec;
getSaveModel(s: DashboardScene) {
return transformSceneToSaveModelSchemaV2(s);
}
getSaveAsModel(s: DashboardScene, options: SaveDashboardAsOptions) {
throw new Error('Method not implemented.');
// eslint-disable-next-line
return {} as DashboardV2Spec;
}
getDashboardChangesFromScene(scene: DashboardScene) {
throw new Error('v2 schema: Method not implemented.');
// eslint-disable-next-line
return {} as DashboardChangeInfo;
}
onSaveComplete(saveModel: DashboardV2Spec, result: SaveDashboardResponseDTO): void {
throw new Error('v2 schema: Method not implemented.');
}
getTrackingInformation() {
throw new Error('v2 schema: Method not implemented.');
return undefined;
}
}
export function getDashboardSceneSerializer(
forceLegacy?: boolean
): DashboardSceneSerializerLike<Dashboard | DashboardV2Spec> {
// When we have end-to-end v2 API integration, this will be controlled by a feature toggle, no need for forceLegacy
if (forceLegacy) {
return new V1DashboardSerializer();
}
if (config.featureToggles.dashboardSchemaV2) {
return new V2DashboardSerializer();
}
return new V1DashboardSerializer();
}
@@ -57,7 +57,7 @@ type DeepPartial<T> = T extends object
}
: T;
export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnapshot = false): Partial<DashboardV2Spec> {
export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnapshot = false): DashboardV2Spec {
const oldDash = scene.state;
const timeRange = oldDash.$timeRange!.state;
@@ -90,9 +90,10 @@ export class JsonModelEditView extends SceneObjectBase<JsonModelEditViewState> i
const { jsonText } = model.useState();
const onSave = async (overwrite: boolean) => {
const result = await onSaveDashboard(dashboard, JSON.parse(model.state.jsonText), {
const result = await onSaveDashboard(dashboard, {
folderUid: dashboard.state.meta.folderUid,
overwrite,
rawDashboardJSON: JSON.parse(model.state.jsonText),
});
setIsSaving(true);
@@ -1,4 +1,5 @@
import { Dashboard } from '@grafana/schema';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
import { ObjectMeta } from 'app/features/apiserver/types';
import { CloneOptions, DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { Diffs } from 'app/features/dashboard-scene/settings/version-history/utils';
@@ -18,8 +19,16 @@ export interface SaveDashboardOptions extends CloneOptions {
makeEditable?: boolean;
}
export interface SaveDashboardAsOptions {
saveAsCopy?: boolean;
isNew?: boolean;
copyTags?: boolean;
title?: string;
description?: string;
}
export interface SaveDashboardCommand {
dashboard: Dashboard;
dashboard: Dashboard | DashboardV2Spec;
message?: string;
folderUid?: string;
overwrite?: boolean;
+13 -24
View File
@@ -7,9 +7,9 @@ import { PanelModel } from '../state/PanelModel';
export function trackDashboardLoaded(dashboard: DashboardModel, duration?: number, versionBeforeMigration?: number) {
// Count the different types of variables
const variables = getVariables(dashboard.templating.list);
const variables = getV1SchemaVariables(dashboard.templating.list);
// Count the different types of panels
const panels = getPanelCounts(dashboard.panels);
const panels = getV1SchemaPanelCounts(dashboard.panels);
DashboardInteractions.dashboardInitialized({
uid: dashboard.uid,
@@ -28,28 +28,17 @@ export function trackDashboardLoaded(dashboard: DashboardModel, duration?: numbe
}
export function trackDashboardSceneLoaded(dashboard: DashboardScene, duration?: number) {
const initialSaveModel = dashboard.getInitialSaveModel();
if (initialSaveModel) {
const panels = getPanelCounts(initialSaveModel.panels || []);
const variables = getVariables(initialSaveModel.templating?.list || []);
DashboardInteractions.dashboardInitialized({
uid: initialSaveModel.uid,
title: initialSaveModel.title,
theme: undefined,
schemaVersion: initialSaveModel.schemaVersion,
version_before_migration: initialSaveModel.version,
panels_count: initialSaveModel.panels?.length || 0,
...panels,
...variables,
settings_nowdelay: undefined,
settings_livenow: !!initialSaveModel.liveNow,
duration,
isScene: true,
});
}
const trackingInformation = dashboard.getTrackingInformation();
DashboardInteractions.dashboardInitialized({
theme: undefined,
duration,
isScene: true,
...trackingInformation,
});
}
function getPanelCounts(panels: Panel[] | PanelModel[]) {
export function getV1SchemaPanelCounts(panels: Panel[] | PanelModel[]) {
return panels
.map((p) => p.type)
.reduce((r: Record<string, number>, p) => {
@@ -58,7 +47,7 @@ function getPanelCounts(panels: Panel[] | PanelModel[]) {
}, {});
}
function getVariables(variableList: VariableModel[]) {
export function getV1SchemaVariables(variableList: VariableModel[]) {
return variableList
.map((v) => v.type)
.reduce((r: Record<string, number>, k) => {
@@ -67,5 +56,5 @@ function getVariables(variableList: VariableModel[]) {
}, {});
}
const variableName = (type: string) => `variable_type_${type}_count`;
export const variableName = (type: string) => `variable_type_${type}_count`;
const panelName = (type: string) => `panel_type_${type}_count`;