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:
@@ -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();
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -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`;
|
||||
|
||||
Reference in New Issue
Block a user