DashboardScene: Support detecting and ignoring variable value changes (#81448)

* DashboardScene: Saving and ignoring variable value changes

* Update
This commit is contained in:
Torkel Ödegaard 2024-01-29 17:45:18 +01:00 committed by GitHub
parent 1b155a02fd
commit 2ce81c1a52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 190 additions and 50 deletions

View File

@ -2401,6 +2401,10 @@ exports[`better eslint`] = {
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"]
],
"public/app/features/dashboard-scene/saving/getSaveDashboardChange.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/features/dashboard-scene/saving/shared.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],

View File

@ -8,7 +8,7 @@ import { DashboardScene } from '../scene/DashboardScene';
import { SaveDashboardAsForm } from './SaveDashboardAsForm';
import { SaveDashboardForm } from './SaveDashboardForm';
import { getSaveDashboardChange } from './shared';
import { getSaveDashboardChange } from './getSaveDashboardChange';
interface SaveDashboardDrawerState extends SceneObjectState {
dashboardRef: SceneObjectRef<DashboardScene>;
@ -28,12 +28,12 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
};
public onToggleSaveVariables = () => {
this.setState({ saveTimeRange: !this.state.saveTimeRange });
this.setState({ saveVariables: !this.state.saveVariables });
};
static Component = ({ model }: SceneComponentProps<SaveDashboardDrawer>) => {
const { showDiff, saveAsCopy, saveTimeRange } = model.useState();
const changeInfo = getSaveDashboardChange(model.state.dashboardRef.resolve(), saveTimeRange);
const { showDiff, saveAsCopy, saveTimeRange, saveVariables } = model.useState();
const changeInfo = getSaveDashboardChange(model.state.dashboardRef.resolve(), saveTimeRange, saveVariables);
const { changedSaveModel, initialSaveModel, diffs, diffCount } = changeInfo;
const dashboard = model.state.dashboardRef.resolve();

View File

@ -25,7 +25,7 @@ export interface Props {
export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
const { saveVariables = false, saveTimeRange = false } = drawer.useState();
const { changedSaveModel, hasChanges, hasTimeChanged, hasVariableValuesChanged } = changeInfo;
const { changedSaveModel, hasChanges, hasTimeChanges, hasVariableValueChanges } = changeInfo;
const { state, onSaveDashboard } = useDashboardSave(false);
const [options, setOptions] = useState<SaveDashboardOptions>({
@ -102,7 +102,7 @@ export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
return (
<Stack gap={0} direction="column">
{hasTimeChanged && (
{hasTimeChanges && (
<Field label="Save current time range" description="Will make current time range the new default">
<Checkbox
id="save-timerange"
@ -112,12 +112,12 @@ export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
/>
</Field>
)}
{hasVariableValuesChanged && (
{hasVariableValueChanges && (
<Field label="Save current variable values" description="Will make the current values the new default">
<Checkbox
id="save-variables"
checked={saveVariables}
onChange={drawer.onToggleSaveVariables}
label="Save current variable values as dashboard default"
aria-label={selectors.pages.SaveDashboardModal.saveVariables}
/>
</Field>

View File

@ -0,0 +1,92 @@
import { MultiValueVariable, sceneGraph } from '@grafana/scenes';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { getSaveDashboardChange } from './getSaveDashboardChange';
describe('getSaveDashboardChange', () => {
it('Can detect no changes', () => {
const dashboard = setup();
const result = getSaveDashboardChange(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 = getSaveDashboardChange(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 = getSaveDashboardChange(dashboard, true);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(1);
});
it('Can detect variable change', () => {
const dashboard = setup();
const appVar = sceneGraph.lookupVariable('app', dashboard) as MultiValueVariable;
appVar.changeValueTo('app2');
const result = getSaveDashboardChange(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 = getSaveDashboardChange(dashboard, false, true);
expect(result.hasVariableValueChanges).toBe(true);
expect(result.hasChanges).toBe(true);
expect(result.diffCount).toBe(2);
});
});
function setup() {
const dashboard = transformSaveModelToScene({
dashboard: {
title: 'hello',
uid: 'my-uid',
schemaVersion: 30,
panels: [],
version: 10,
templating: {
list: [
{
name: 'app',
type: 'custom',
current: {
text: 'app1',
value: 'app1',
},
},
],
},
},
meta: {},
});
const initialSaveModel = transformSceneToSaveModel(dashboard);
dashboard.setInitialSaveModel(initialSaveModel);
return dashboard;
}

View File

@ -0,0 +1,83 @@
import { isEqual } from 'lodash';
import { AdHocVariableModel, TypedVariableModel } from '@grafana/data';
import { Dashboard } from '@grafana/schema';
import { DashboardScene } from '../scene/DashboardScene';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { jsonDiff } from '../settings/version-history/utils';
import { DashboardChangeInfo } from './shared';
export function getSaveDashboardChange(
dashboard: DashboardScene,
saveTimeRange?: boolean,
saveVariables?: boolean
): DashboardChangeInfo {
const initialSaveModel = dashboard.getInitialSaveModel()!;
const changedSaveModel = transformSceneToSaveModel(dashboard);
const hasTimeChanged = getHasTimeChanged(changedSaveModel, initialSaveModel);
const hasVariableValueChanges = applyVariableChanges(changedSaveModel, initialSaveModel, saveVariables);
if (!saveTimeRange) {
changedSaveModel.time = initialSaveModel.time;
}
const diff = jsonDiff(initialSaveModel, changedSaveModel);
let diffCount = 0;
for (const d of Object.values(diff)) {
diffCount += d.length;
}
return {
changedSaveModel,
initialSaveModel,
diffs: diff,
diffCount,
hasChanges: diffCount > 0,
hasTimeChanges: hasTimeChanged,
isNew: changedSaveModel.version === 0,
hasVariableValueChanges,
};
}
export function getHasTimeChanged(saveModel: Dashboard, originalSaveModel: Dashboard) {
return saveModel.time?.from !== originalSaveModel.time?.from || saveModel.time?.to !== originalSaveModel.time?.to;
}
export function applyVariableChanges(saveModel: Dashboard, originalSaveModel: Dashboard, saveVariables?: boolean) {
const originalVariables = originalSaveModel.templating?.list ?? [];
const variablesToSave = saveModel.templating?.list ?? [];
let hasVariableValueChanges = false;
for (const variable of variablesToSave) {
const original = originalVariables.find(({ name, type }) => name === variable.name && type === variable.type);
if (!original) {
continue;
}
// Old schema property that never should be in persisted model
if (original.current && Object.hasOwn(original.current, 'selected')) {
delete original.current.selected;
}
if (!isEqual(variable.current, original.current)) {
hasVariableValueChanges = true;
}
if (!saveVariables) {
const typed = variable as TypedVariableModel;
if (typed.type === 'adhoc') {
typed.filters = (original as AdHocVariableModel).filters;
} else {
variable.current = original.current;
variable.options = original.options;
}
}
}
return hasVariableValueChanges;
}

View File

@ -5,9 +5,7 @@ import { isFetchError } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { Alert, Box, Button, Stack } from '@grafana/ui';
import { DashboardScene } from '../scene/DashboardScene';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { Diffs, jsonDiff } from '../settings/version-history/utils';
import { Diffs } from '../settings/version-history/utils';
export interface DashboardChangeInfo {
changedSaveModel: Dashboard;
@ -15,48 +13,11 @@ export interface DashboardChangeInfo {
diffs: Diffs;
diffCount: number;
hasChanges: boolean;
hasTimeChanged: boolean;
hasVariableValuesChanged: boolean;
hasTimeChanges: boolean;
hasVariableValueChanges: boolean;
isNew?: boolean;
}
export function getSaveDashboardChange(dashboard: DashboardScene, saveTimeRange?: boolean): DashboardChangeInfo {
const initialSaveModel = dashboard.getInitialSaveModel()!;
const changedSaveModel = transformSceneToSaveModel(dashboard);
const hasTimeChanged = getHasTimeChanged(changedSaveModel, initialSaveModel);
const hasVariableValuesChanged = getVariableValueChanges(changedSaveModel, initialSaveModel);
if (!saveTimeRange) {
changedSaveModel.time = initialSaveModel.time;
}
const diff = jsonDiff(initialSaveModel, changedSaveModel);
let diffCount = 0;
for (const d of Object.values(diff)) {
diffCount += d.length;
}
return {
changedSaveModel,
initialSaveModel,
diffs: diff,
diffCount,
hasChanges: diffCount > 0,
hasTimeChanged,
isNew: changedSaveModel.version === 0,
hasVariableValuesChanged,
};
}
function getHasTimeChanged(saveModel: Dashboard, originalSaveModel: Dashboard) {
return saveModel.time?.from !== originalSaveModel.time?.from || saveModel.time?.to !== originalSaveModel.time?.to;
}
function getVariableValueChanges(saveModel: Dashboard, originalSaveModel: Dashboard) {
return false;
}
export function isVersionMismatchError(error?: Error) {
return isFetchError(error) && error.data && error.data.status === 'version-mismatch';
}