mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Implement detect changes tracking to V2 Schema (#98153)
Co-authored-by: Haris Rozajac <haris.rozajac12@gmail.com>
This commit is contained in:
parent
148289258f
commit
a06779614e
@ -3103,7 +3103,8 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/saving/shared.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
@ -3192,10 +3193,11 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
|
@ -27,10 +27,10 @@ export interface DashboardV2Spec {
|
||||
// Links with references to other dashboards or external websites.
|
||||
links: DashboardLink[];
|
||||
// Tags associated with dashboard.
|
||||
tags?: string[];
|
||||
tags: string[];
|
||||
timeSettings: TimeSettingsSpec;
|
||||
// Configured template variables.
|
||||
variables: (QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind)[];
|
||||
variables: VariableKind[];
|
||||
// |* more element types in the future
|
||||
elements: Record<string, PanelKind>;
|
||||
annotations: AnnotationQueryKind[];
|
||||
@ -49,6 +49,7 @@ export const defaultDashboardV2Spec = (): DashboardV2Spec => ({
|
||||
preload: false,
|
||||
editable: true,
|
||||
links: [],
|
||||
tags: [],
|
||||
timeSettings: defaultTimeSettingsSpec(),
|
||||
variables: [],
|
||||
elements: {},
|
||||
@ -801,6 +802,10 @@ export type VariableType = "query" | "adhoc" | "groupby" | "constant" | "datasou
|
||||
|
||||
export const defaultVariableType = (): VariableType => ("query");
|
||||
|
||||
export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind;
|
||||
|
||||
export const defaultVariableKind = (): VariableKind => (defaultQueryVariableKind());
|
||||
|
||||
// Sort variable options
|
||||
// Accepted values are:
|
||||
// `disabled`: No sorting
|
||||
|
@ -36,12 +36,12 @@ DashboardV2Spec: {
|
||||
links: [...DashboardLink]
|
||||
|
||||
// Tags associated with dashboard.
|
||||
tags?: [...string]
|
||||
tags: [...string]
|
||||
|
||||
timeSettings: TimeSettingsSpec
|
||||
|
||||
// Configured template variables.
|
||||
variables: [...QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind]
|
||||
variables: [...VariableKind]
|
||||
|
||||
elements: [ElementReference.name]: PanelKind // |* more element types in the future
|
||||
|
||||
@ -548,6 +548,8 @@ VariableCustomFormatterFn: {
|
||||
VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
||||
"system" | "snapshot"
|
||||
|
||||
VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind
|
||||
|
||||
// Sort variable options
|
||||
// Accepted values are:
|
||||
// `disabled`: No sorting
|
||||
|
@ -1,32 +1,21 @@
|
||||
import { AdHocVariableModel } from '@grafana/data';
|
||||
import { Dashboard, Panel } from '@grafana/schema';
|
||||
|
||||
import { adHocVariableFiltersEqual, getRawDashboardChanges, getPanelChanges } from './getDashboardChanges';
|
||||
|
||||
describe('adHocVariableFiltersEqual', () => {
|
||||
it('should compare empty filters', () => {
|
||||
expect(
|
||||
adHocVariableFiltersEqual(
|
||||
{ filters: [] } as unknown as AdHocVariableModel,
|
||||
{ filters: [] } as unknown as AdHocVariableModel
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(adHocVariableFiltersEqual([], [])).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should compare different length filter arrays', () => {
|
||||
expect(
|
||||
adHocVariableFiltersEqual(
|
||||
{ filters: [] } as unknown as AdHocVariableModel,
|
||||
{ filters: [{ value: '', key: '', operator: '' }] } as unknown as AdHocVariableModel
|
||||
)
|
||||
).toBeFalsy();
|
||||
expect(adHocVariableFiltersEqual([], [{ value: '', key: '', operator: '' }])).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should compare equal filter arrays', () => {
|
||||
expect(
|
||||
adHocVariableFiltersEqual(
|
||||
{ filters: [{ value: 'asd', key: 'qwe', operator: 'wer' }] } as unknown as AdHocVariableModel,
|
||||
{ filters: [{ value: 'asd', key: 'qwe', operator: 'wer' }] } as unknown as AdHocVariableModel
|
||||
[{ value: 'asd', key: 'qwe', operator: 'wer' }],
|
||||
[{ value: 'asd', key: 'qwe', operator: 'wer' }]
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
@ -34,8 +23,8 @@ describe('adHocVariableFiltersEqual', () => {
|
||||
it('should compare different filter arrays where operator differs', () => {
|
||||
expect(
|
||||
adHocVariableFiltersEqual(
|
||||
{ filters: [{ value: 'asd', key: 'qwe', operator: 'wer' }] } as unknown as AdHocVariableModel,
|
||||
{ filters: [{ value: 'asd', key: 'qwe', operator: 'weee' }] } as unknown as AdHocVariableModel
|
||||
[{ value: 'asd', key: 'qwe', operator: 'wer' }],
|
||||
[{ value: 'asd', key: 'qwe', operator: 'weee' }]
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
@ -43,8 +32,8 @@ describe('adHocVariableFiltersEqual', () => {
|
||||
it('should compare different filter arrays where key differs', () => {
|
||||
expect(
|
||||
adHocVariableFiltersEqual(
|
||||
{ filters: [{ value: 'asd', key: 'qwe', operator: 'wer' }] } as unknown as AdHocVariableModel,
|
||||
{ filters: [{ value: 'asd', key: 'qwer', operator: 'wer' }] } as unknown as AdHocVariableModel
|
||||
[{ value: 'asd', key: 'qwe', operator: 'wer' }],
|
||||
[{ value: 'asd', key: 'qwer', operator: 'wer' }]
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
@ -52,8 +41,8 @@ describe('adHocVariableFiltersEqual', () => {
|
||||
it('should compare different filter arrays where value differs', () => {
|
||||
expect(
|
||||
adHocVariableFiltersEqual(
|
||||
{ filters: [{ value: 'asd', key: 'qwe', operator: 'wer' }] } as unknown as AdHocVariableModel,
|
||||
{ filters: [{ value: 'asdio', key: 'qwe', operator: 'wer' }] } as unknown as AdHocVariableModel
|
||||
[{ value: 'asd', key: 'qwe', operator: 'wer' }],
|
||||
[{ value: 'asdio', key: 'qwe', operator: 'wer' }]
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
@ -65,23 +54,14 @@ describe('adHocVariableFiltersEqual', () => {
|
||||
|
||||
it('should compare two adhoc variables where both are missing the filter property and return true', () => {
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementationOnce(() => {});
|
||||
expect(
|
||||
adHocVariableFiltersEqual({} as unknown as AdHocVariableModel, {} as unknown as AdHocVariableModel)
|
||||
).toBeTruthy();
|
||||
expect(adHocVariableFiltersEqual(undefined, undefined)).toBeTruthy();
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith('Adhoc variable filter property is undefined');
|
||||
});
|
||||
|
||||
it('should compare two adhoc variables where one has no filter property and return false', () => {
|
||||
it('should compare two adhoc variables where one is undefined and return false', () => {
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementationOnce(() => {});
|
||||
expect(
|
||||
adHocVariableFiltersEqual(
|
||||
{} as unknown as AdHocVariableModel,
|
||||
{
|
||||
filters: [{ value: 'asdio', key: 'qwe', operator: 'wer' }],
|
||||
} as unknown as AdHocVariableModel
|
||||
)
|
||||
).toBeFalsy();
|
||||
expect(adHocVariableFiltersEqual(undefined, [{ value: 'asdio', key: 'qwe', operator: 'wer' }])).toBeFalsy();
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith('Adhoc variable filter property is undefined');
|
||||
});
|
||||
|
@ -2,7 +2,12 @@
|
||||
|
||||
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 {
|
||||
AdHocFilterWithLabels,
|
||||
AdhocVariableSpec,
|
||||
DashboardV2Spec,
|
||||
VariableKind,
|
||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
|
||||
|
||||
import { jsonDiff } from '../settings/version-history/utils';
|
||||
|
||||
@ -30,7 +35,6 @@ 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));
|
||||
}
|
||||
|
||||
// TODO[schema v2]
|
||||
export function getRawDashboardV2Changes(
|
||||
initial: DashboardV2Spec,
|
||||
changed: DashboardV2Spec,
|
||||
@ -38,16 +42,33 @@ export function getRawDashboardV2Changes(
|
||||
saveVariables?: boolean,
|
||||
saveRefresh?: boolean
|
||||
) {
|
||||
const initialSaveModel = initial;
|
||||
const changedSaveModel = changed;
|
||||
const hasTimeChanged = getHasTimeChanged(changedSaveModel.timeSettings, initialSaveModel.timeSettings);
|
||||
const hasVariableValueChanges = applyVariableChangesV2(changedSaveModel, initialSaveModel, saveVariables);
|
||||
const hasRefreshChanged = changedSaveModel.timeSettings.autoRefresh !== initialSaveModel.timeSettings.autoRefresh;
|
||||
|
||||
if (!saveTimeRange) {
|
||||
changedSaveModel.timeSettings.from = initialSaveModel.timeSettings.from;
|
||||
changedSaveModel.timeSettings.to = initialSaveModel.timeSettings.to;
|
||||
}
|
||||
|
||||
if (!saveRefresh) {
|
||||
changedSaveModel.timeSettings.autoRefresh = initialSaveModel.timeSettings.autoRefresh;
|
||||
}
|
||||
|
||||
const diff = jsonDiff(initialSaveModel, changedSaveModel);
|
||||
const diffCount = Object.values(diff).reduce((acc, cur) => acc + cur.length, 0);
|
||||
|
||||
return {
|
||||
changedSaveModel: changed,
|
||||
initialSaveModel: initial,
|
||||
diffs: jsonDiff(initial, changed),
|
||||
diffCount: 0,
|
||||
hasChanges: false,
|
||||
hasTimeChanges: false,
|
||||
isNew: false,
|
||||
hasVariableValueChanges: false,
|
||||
hasRefreshChange: false,
|
||||
changedSaveModel,
|
||||
initialSaveModel,
|
||||
diffs: diff,
|
||||
diffCount,
|
||||
hasChanges: diffCount > 0,
|
||||
hasTimeChanges: hasTimeChanged,
|
||||
hasVariableValueChanges,
|
||||
hasRefreshChange: hasRefreshChanged,
|
||||
};
|
||||
}
|
||||
|
||||
@ -60,7 +81,7 @@ export function getRawDashboardChanges(
|
||||
) {
|
||||
const initialSaveModel = initial;
|
||||
const changedSaveModel = changed;
|
||||
const hasTimeChanged = getHasTimeChanged(changedSaveModel, initialSaveModel);
|
||||
const hasTimeChanged = getHasTimeChanged(changedSaveModel.time, initialSaveModel.time);
|
||||
const hasVariableValueChanges = applyVariableChanges(changedSaveModel, initialSaveModel, saveVariables);
|
||||
const hasRefreshChanged = changedSaveModel.refresh !== initialSaveModel.refresh;
|
||||
|
||||
@ -88,35 +109,107 @@ export function getRawDashboardChanges(
|
||||
};
|
||||
}
|
||||
|
||||
export function getHasTimeChanged(saveModel: Dashboard, originalSaveModel: Dashboard) {
|
||||
return saveModel.time?.from !== originalSaveModel.time?.from || saveModel.time?.to !== originalSaveModel.time?.to;
|
||||
interface DefaultPersistedTimeValue {
|
||||
from?: string;
|
||||
to?: string;
|
||||
}
|
||||
export function getHasTimeChanged(
|
||||
newRange: DefaultPersistedTimeValue = {},
|
||||
previousRange: DefaultPersistedTimeValue = {}
|
||||
) {
|
||||
return newRange.from !== previousRange.from || newRange.to !== previousRange.to;
|
||||
}
|
||||
|
||||
export function adHocVariableFiltersEqual(a: AdHocVariableModel, b: AdHocVariableModel) {
|
||||
if (a.filters === undefined && b.filters === undefined) {
|
||||
export function adHocVariableFiltersEqual(filtersA?: AdHocFilterWithLabels[], filtersB?: AdHocFilterWithLabels[]) {
|
||||
if (filtersA === undefined && filtersB === undefined) {
|
||||
console.warn('Adhoc variable filter property is undefined');
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((a.filters === undefined && b.filters !== undefined) || (b.filters === undefined && a.filters !== undefined)) {
|
||||
if ((filtersA === undefined && filtersB !== undefined) || (filtersB === undefined && filtersA !== undefined)) {
|
||||
console.warn('Adhoc variable filter property is undefined');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (a.filters.length !== b.filters.length) {
|
||||
if (filtersA?.length !== filtersB?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.filters.length; i++) {
|
||||
const aFilter = a.filters[i];
|
||||
const bFilter = b.filters[i];
|
||||
if (aFilter.key !== bFilter.key || aFilter.operator !== bFilter.operator || aFilter.value !== bFilter.value) {
|
||||
for (let i = 0; i < (filtersA?.length ?? 0); i++) {
|
||||
const aFilter = filtersA?.[i];
|
||||
const bFilter = filtersB?.[i];
|
||||
if (aFilter?.key !== bFilter?.key || aFilter?.operator !== bFilter?.operator || aFilter?.value !== bFilter?.value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function applyVariableChangesV2(
|
||||
saveModel: DashboardV2Spec,
|
||||
originalSaveModel: DashboardV2Spec,
|
||||
saveVariables?: boolean
|
||||
) {
|
||||
const originalVariables = originalSaveModel.variables ?? [];
|
||||
const variablesToSave = saveModel.variables ?? [];
|
||||
let hasVariableValueChanges = false;
|
||||
|
||||
for (const variable of variablesToSave) {
|
||||
const hasCurrentValueToSave = (v: VariableKind) =>
|
||||
v.kind === 'QueryVariable' ||
|
||||
v.kind === 'CustomVariable' ||
|
||||
v.kind === 'DatasourceVariable' ||
|
||||
v.kind === 'ConstantVariable' ||
|
||||
v.kind === 'IntervalVariable' ||
|
||||
v.kind === 'TextVariable' ||
|
||||
v.kind === 'GroupByVariable';
|
||||
|
||||
const hasOptionsToSave = (v: VariableKind) =>
|
||||
v.kind === 'QueryVariable' ||
|
||||
v.kind === 'CustomVariable' ||
|
||||
v.kind === 'DatasourceVariable' ||
|
||||
v.kind === 'IntervalVariable' ||
|
||||
v.kind === 'GroupByVariable';
|
||||
|
||||
const original = originalVariables.find(
|
||||
({ spec, kind }) => spec.name === variable.spec.name && kind === variable.kind
|
||||
);
|
||||
|
||||
if (!original) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
hasCurrentValueToSave(variable) &&
|
||||
hasCurrentValueToSave(original) &&
|
||||
!isEqual(variable.spec.current, original.spec.current)
|
||||
) {
|
||||
hasVariableValueChanges = true;
|
||||
} else if (
|
||||
variable.kind === 'AdhocVariable' &&
|
||||
original.kind === 'AdhocVariable' &&
|
||||
!adHocVariableFiltersEqual(variable.spec.filters, original.spec.filters)
|
||||
) {
|
||||
hasVariableValueChanges = true;
|
||||
}
|
||||
|
||||
if (!saveVariables) {
|
||||
if (variable.kind === 'AdhocVariable') {
|
||||
variable.spec.filters = (original.spec as AdhocVariableSpec).filters;
|
||||
} else {
|
||||
if (hasCurrentValueToSave(variable) && hasCurrentValueToSave(original)) {
|
||||
variable.spec.current = original.spec.current;
|
||||
}
|
||||
if (hasOptionsToSave(variable) && hasOptionsToSave(original)) {
|
||||
variable.spec.options = original.spec.options;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasVariableValueChanges;
|
||||
}
|
||||
|
||||
export function applyVariableChanges(saveModel: Dashboard, originalSaveModel: Dashboard, saveVariables?: boolean) {
|
||||
const originalVariables = originalSaveModel.templating?.list ?? [];
|
||||
const variablesToSave = saveModel.templating?.list ?? [];
|
||||
@ -138,7 +231,10 @@ export function applyVariableChanges(saveModel: Dashboard, originalSaveModel: Da
|
||||
hasVariableValueChanges = true;
|
||||
} else if (
|
||||
variable.type === 'adhoc' &&
|
||||
!adHocVariableFiltersEqual(variable as AdHocVariableModel, original as AdHocVariableModel)
|
||||
!adHocVariableFiltersEqual(
|
||||
(variable as AdHocVariableModel | undefined)?.filters,
|
||||
(original as AdHocVariableModel | undefined)?.filters
|
||||
)
|
||||
) {
|
||||
hasVariableValueChanges = true;
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
private _prevScrollPos?: number;
|
||||
|
||||
// TODO: use feature toggle to allow v2 serializer
|
||||
private _serializer: DashboardSceneSerializerLike<Dashboard | DashboardV2Spec> = getDashboardSceneSerializer(true);
|
||||
private _serializer: DashboardSceneSerializerLike<Dashboard | DashboardV2Spec> = getDashboardSceneSerializer();
|
||||
|
||||
public constructor(state: Partial<DashboardSceneState>) {
|
||||
super({
|
||||
|
@ -645,10 +645,10 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
},
|
||||
});
|
||||
|
||||
// Will open a schema v2 editor drawer. Only available with dashboardSchemaV2 feature toggle on.
|
||||
// Will open a schema v2 editor drawer. Only available with useV2DashboardsAPI feature toggle on.
|
||||
toolbarActions.push({
|
||||
group: 'main-buttons',
|
||||
condition: uid && config.featureToggles.dashboardSchemaV2,
|
||||
condition: uid && config.featureToggles.useV2DashboardsAPI,
|
||||
render: () => {
|
||||
return (
|
||||
<ToolbarButton
|
||||
|
@ -7,7 +7,12 @@ import {
|
||||
SceneRefreshPicker,
|
||||
} from '@grafana/scenes';
|
||||
import { Dashboard, VariableModel } from '@grafana/schema';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
|
||||
import {
|
||||
DashboardV2Spec,
|
||||
defaultDashboardV2Spec,
|
||||
defaultPanelSpec,
|
||||
defaultTimeSettingsSpec,
|
||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
|
||||
|
||||
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
@ -15,9 +20,24 @@ import { transformSceneToSaveModel } from '../serialization/transformSceneToSave
|
||||
import { findVizPanelByKey } from '../utils/utils';
|
||||
|
||||
import { V1DashboardSerializer, V2DashboardSerializer } from './DashboardSceneSerializer';
|
||||
import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene';
|
||||
import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: () => {
|
||||
return {
|
||||
getInstanceSettings: jest.fn(),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DashboardSceneSerializer', () => {
|
||||
describe('v1 schema', () => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.useV2DashboardsAPI = false;
|
||||
});
|
||||
|
||||
it('Can detect no changes', () => {
|
||||
const dashboard = setup();
|
||||
const result = dashboard.getDashboardChanges(false);
|
||||
@ -309,18 +329,226 @@ describe('DashboardSceneSerializer', () => {
|
||||
});
|
||||
|
||||
describe('v2 schema', () => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.useV2DashboardsAPI = true;
|
||||
});
|
||||
|
||||
it('Can detect no changes', () => {
|
||||
const dashboard = setupV2();
|
||||
const result = dashboard.getDashboardChanges(false);
|
||||
expect(result.hasChanges).toBe(false);
|
||||
expect(result.diffCount).toBe(0);
|
||||
});
|
||||
|
||||
it('Can detect time changed', () => {
|
||||
const dashboard = setupV2();
|
||||
|
||||
sceneGraph.getTimeRange(dashboard).setState({ from: 'now-10h', 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 = setupV2();
|
||||
|
||||
sceneGraph.getTimeRange(dashboard).setState({ from: 'now-10h', to: 'now' });
|
||||
|
||||
const result = dashboard.getDashboardChanges(true);
|
||||
expect(result.hasChanges).toBe(true);
|
||||
expect(result.diffCount).toBe(1);
|
||||
});
|
||||
|
||||
it('Can detect folder change', () => {
|
||||
const dashboard = setupV2();
|
||||
|
||||
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 = setupV2();
|
||||
|
||||
const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker);
|
||||
if (refreshPicker instanceof SceneRefreshPicker) {
|
||||
refreshPicker.setState({ refresh: '10m' });
|
||||
}
|
||||
|
||||
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 = setupV2();
|
||||
|
||||
const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker);
|
||||
if (refreshPicker instanceof SceneRefreshPicker) {
|
||||
refreshPicker.setState({ refresh: '10m' });
|
||||
}
|
||||
|
||||
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 = setupV2();
|
||||
|
||||
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 = setupV2();
|
||||
|
||||
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 = setupV2({
|
||||
variables: [
|
||||
{
|
||||
kind: 'GroupByVariable',
|
||||
spec: {
|
||||
current: {
|
||||
text: 'Host',
|
||||
value: 'host',
|
||||
},
|
||||
datasource: {
|
||||
type: 'ds',
|
||||
uid: 'ds-uid',
|
||||
},
|
||||
name: 'GroupBy',
|
||||
options: [
|
||||
{
|
||||
text: 'Host',
|
||||
value: 'host',
|
||||
},
|
||||
{
|
||||
text: 'Region',
|
||||
value: 'region',
|
||||
},
|
||||
],
|
||||
multi: false,
|
||||
includeAll: false,
|
||||
hide: 'dontHide',
|
||||
skipUrlSync: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
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 dashboard = setupV2({
|
||||
variables: [
|
||||
{
|
||||
kind: 'AdhocVariable',
|
||||
spec: {
|
||||
name: 'adhoc',
|
||||
label: 'Adhoc Label',
|
||||
description: 'Adhoc Description',
|
||||
datasource: {
|
||||
uid: 'gdev-prometheus',
|
||||
type: 'prometheus',
|
||||
},
|
||||
hide: 'dontHide',
|
||||
skipUrlSync: false,
|
||||
filters: [],
|
||||
baseFilters: [],
|
||||
defaultKeys: [
|
||||
{
|
||||
text: 'Host',
|
||||
value: 'host',
|
||||
},
|
||||
{
|
||||
text: 'Region',
|
||||
value: 'region',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
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 = setupV2();
|
||||
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 DashboardV2Spec).elements['panel-1'].spec;
|
||||
expect(panelSaveModel.title).toBe('changed title');
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw on getTrackingInformation', () => {
|
||||
const serializer = new V2DashboardSerializer();
|
||||
expect(() => serializer.getTrackingInformation()).toThrow('Method not implemented.');
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
@ -335,19 +563,10 @@ describe('DashboardSceneSerializer', () => {
|
||||
})
|
||||
).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 = {}) {
|
||||
function setup() {
|
||||
const dashboard = transformSaveModelToScene({
|
||||
dashboard: {
|
||||
title: 'hello',
|
||||
@ -382,3 +601,89 @@ function setup(options: ScenarioOptions = {}) {
|
||||
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
function setupV2(spec?: Partial<DashboardV2Spec>) {
|
||||
const dashboard = transformSaveModelSchemaV2ToScene({
|
||||
kind: 'DashboardWithAccessInfo',
|
||||
spec: {
|
||||
...defaultDashboardV2Spec(),
|
||||
title: 'hello',
|
||||
schemaVersion: 30,
|
||||
timeSettings: {
|
||||
...defaultTimeSettingsSpec(),
|
||||
autoRefresh: '10s',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
},
|
||||
elements: {
|
||||
'panel-1': {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
...defaultPanelSpec(),
|
||||
id: 1,
|
||||
title: 'Panel 1',
|
||||
},
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
kind: 'GridLayout',
|
||||
spec: {
|
||||
items: [
|
||||
{
|
||||
kind: 'GridLayoutItem',
|
||||
spec: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 12,
|
||||
height: 8,
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: 'panel-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
kind: 'CustomVariable',
|
||||
spec: {
|
||||
name: 'app',
|
||||
label: 'Query Variable',
|
||||
description: 'A query variable',
|
||||
skipUrlSync: false,
|
||||
hide: 'dontHide',
|
||||
options: [],
|
||||
multi: false,
|
||||
current: {
|
||||
text: 'app1',
|
||||
value: 'app1',
|
||||
},
|
||||
query: 'app1',
|
||||
allValue: '',
|
||||
includeAll: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
...spec,
|
||||
},
|
||||
apiVersion: 'v1',
|
||||
metadata: {
|
||||
name: 'dashboard-test',
|
||||
resourceVersion: '1',
|
||||
creationTimestamp: '2023-01-01T00:00:00Z',
|
||||
},
|
||||
access: {
|
||||
canEdit: true,
|
||||
canSave: true,
|
||||
canStar: true,
|
||||
canShare: true,
|
||||
},
|
||||
});
|
||||
|
||||
const initialSaveModel = transformSceneToSaveModelSchemaV2(dashboard);
|
||||
dashboard.setInitialSaveModel(initialSaveModel);
|
||||
|
||||
return dashboard;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDa
|
||||
import { getV1SchemaPanelCounts, getV1SchemaVariables } from 'app/features/dashboard/utils/tracking';
|
||||
import { SaveDashboardResponseDTO } from 'app/types';
|
||||
|
||||
import { getRawDashboardChanges } from '../saving/getDashboardChanges';
|
||||
import { getRawDashboardChanges, getRawDashboardV2Changes } from '../saving/getDashboardChanges';
|
||||
import { DashboardChangeInfo } from '../saving/shared';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
|
||||
@ -126,10 +126,28 @@ export class V2DashboardSerializer implements DashboardSceneSerializerLike<Dashb
|
||||
return {} as DashboardV2Spec;
|
||||
}
|
||||
|
||||
getDashboardChangesFromScene(scene: DashboardScene) {
|
||||
throw new Error('v2 schema: Method not implemented.');
|
||||
// eslint-disable-next-line
|
||||
return {} as DashboardChangeInfo;
|
||||
getDashboardChangesFromScene(
|
||||
scene: DashboardScene,
|
||||
options: { saveTimeRange?: boolean; saveVariables?: boolean; saveRefresh?: boolean }
|
||||
) {
|
||||
const changedSaveModel = this.getSaveModel(scene);
|
||||
const changeInfo = getRawDashboardV2Changes(
|
||||
this.initialSaveModel!,
|
||||
changedSaveModel,
|
||||
options.saveTimeRange,
|
||||
options.saveVariables,
|
||||
options.saveRefresh
|
||||
);
|
||||
|
||||
const hasFolderChanges = scene.getInitialState()?.meta.folderUid !== scene.state.meta.folderUid;
|
||||
const isNew = scene.getInitialState()?.meta.isNew;
|
||||
|
||||
return {
|
||||
...changeInfo,
|
||||
hasFolderChanges,
|
||||
hasChanges: changeInfo.hasChanges || hasFolderChanges,
|
||||
isNew,
|
||||
};
|
||||
}
|
||||
|
||||
onSaveComplete(saveModel: DashboardV2Spec, result: SaveDashboardResponseDTO): void {
|
||||
@ -142,15 +160,8 @@ export class V2DashboardSerializer implements DashboardSceneSerializerLike<Dashb
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
export function getDashboardSceneSerializer(): DashboardSceneSerializerLike<Dashboard | DashboardV2Spec> {
|
||||
if (config.featureToggles.useV2DashboardsAPI) {
|
||||
return new V2DashboardSerializer();
|
||||
}
|
||||
|
||||
|
@ -114,11 +114,13 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
|
||||
// EOF layout
|
||||
};
|
||||
|
||||
if (isDashboardSchemaV2(dashboardSchemaV2)) {
|
||||
return dashboardSchemaV2;
|
||||
try {
|
||||
validateDashboardSchemaV2(dashboardSchemaV2);
|
||||
return dashboardSchemaV2 as DashboardV2Spec;
|
||||
} catch (reason) {
|
||||
console.error('Error transforming dashboard to schema v2: ' + reason, dashboardSchemaV2);
|
||||
throw new Error('Error transforming dashboard to schema v2: ' + reason);
|
||||
}
|
||||
console.error('Error transforming dashboard to schema v2');
|
||||
throw new Error('Error transforming dashboard to schema v2');
|
||||
}
|
||||
|
||||
function getCursorSync(state: DashboardSceneState) {
|
||||
@ -436,7 +438,7 @@ function getDefaultDataSourceRef(): DataSourceRef | undefined {
|
||||
const defaultDatasource = config.bootData.settings.defaultDatasource;
|
||||
|
||||
// get default datasource type
|
||||
const dsList = config.bootData.settings.datasources;
|
||||
const dsList = config.bootData.settings.datasources ?? {};
|
||||
const ds = dsList[defaultDatasource];
|
||||
|
||||
if (ds) {
|
||||
@ -447,98 +449,98 @@ function getDefaultDataSourceRef(): DataSourceRef | undefined {
|
||||
}
|
||||
|
||||
// Function to know if the dashboard transformed is a valid DashboardV2Spec
|
||||
function isDashboardSchemaV2(dash: any): dash is DashboardV2Spec {
|
||||
function validateDashboardSchemaV2(dash: any): dash is DashboardV2Spec {
|
||||
if (typeof dash !== 'object' || dash === null) {
|
||||
return false;
|
||||
throw new Error('Dashboard is not an object or is null');
|
||||
}
|
||||
|
||||
if (typeof dash.title !== 'string') {
|
||||
return false;
|
||||
throw new Error('Title is not a string');
|
||||
}
|
||||
if (typeof dash.description !== 'string') {
|
||||
return false;
|
||||
throw new Error('Description is not a string');
|
||||
}
|
||||
if (typeof dash.cursorSync !== 'string') {
|
||||
return false;
|
||||
throw new Error('CursorSync is not a string');
|
||||
}
|
||||
if (typeof dash.liveNow !== 'boolean') {
|
||||
return false;
|
||||
throw new Error('LiveNow is not a boolean');
|
||||
}
|
||||
if (typeof dash.preload !== 'boolean') {
|
||||
return false;
|
||||
throw new Error('Preload is not a boolean');
|
||||
}
|
||||
if (typeof dash.editable !== 'boolean') {
|
||||
return false;
|
||||
throw new Error('Editable is not a boolean');
|
||||
}
|
||||
if (!Array.isArray(dash.links)) {
|
||||
return false;
|
||||
throw new Error('Links is not an array');
|
||||
}
|
||||
if (!Array.isArray(dash.tags)) {
|
||||
return false;
|
||||
throw new Error('Tags is not an array');
|
||||
}
|
||||
|
||||
if (dash.id !== undefined && typeof dash.id !== 'number') {
|
||||
return false;
|
||||
throw new Error('ID is not a number');
|
||||
}
|
||||
|
||||
// Time settings
|
||||
if (typeof dash.timeSettings !== 'object' || dash.timeSettings === null) {
|
||||
return false;
|
||||
throw new Error('TimeSettings is not an object or is null');
|
||||
}
|
||||
if (typeof dash.timeSettings.timezone !== 'string') {
|
||||
return false;
|
||||
throw new Error('Timezone is not a string');
|
||||
}
|
||||
if (typeof dash.timeSettings.from !== 'string') {
|
||||
return false;
|
||||
throw new Error('From is not a string');
|
||||
}
|
||||
if (typeof dash.timeSettings.to !== 'string') {
|
||||
return false;
|
||||
throw new Error('To is not a string');
|
||||
}
|
||||
if (typeof dash.timeSettings.autoRefresh !== 'string') {
|
||||
return false;
|
||||
throw new Error('AutoRefresh is not a string');
|
||||
}
|
||||
if (!Array.isArray(dash.timeSettings.autoRefreshIntervals)) {
|
||||
return false;
|
||||
throw new Error('AutoRefreshIntervals is not an array');
|
||||
}
|
||||
if (!Array.isArray(dash.timeSettings.quickRanges)) {
|
||||
return false;
|
||||
throw new Error('QuickRanges is not an array');
|
||||
}
|
||||
if (typeof dash.timeSettings.hideTimepicker !== 'boolean') {
|
||||
return false;
|
||||
throw new Error('HideTimepicker is not a boolean');
|
||||
}
|
||||
if (typeof dash.timeSettings.weekStart !== 'string') {
|
||||
return false;
|
||||
throw new Error('WeekStart is not a string');
|
||||
}
|
||||
if (typeof dash.timeSettings.fiscalYearStartMonth !== 'number') {
|
||||
return false;
|
||||
throw new Error('FiscalYearStartMonth is not a number');
|
||||
}
|
||||
if (dash.timeSettings.nowDelay !== undefined && typeof dash.timeSettings.nowDelay !== 'string') {
|
||||
return false;
|
||||
throw new Error('NowDelay is not a string');
|
||||
}
|
||||
|
||||
// Other sections
|
||||
if (!Array.isArray(dash.variables)) {
|
||||
return false;
|
||||
throw new Error('Variables is not an array');
|
||||
}
|
||||
if (typeof dash.elements !== 'object' || dash.elements === null) {
|
||||
return false;
|
||||
throw new Error('Elements is not an object or is null');
|
||||
}
|
||||
if (!Array.isArray(dash.annotations)) {
|
||||
return false;
|
||||
throw new Error('Annotations is not an array');
|
||||
}
|
||||
|
||||
// Layout
|
||||
if (typeof dash.layout !== 'object' || dash.layout === null) {
|
||||
return false;
|
||||
throw new Error('Layout is not an object or is null');
|
||||
}
|
||||
if (dash.layout.kind !== 'GridLayout') {
|
||||
return false;
|
||||
throw new Error('Layout kind is not GridLayout');
|
||||
}
|
||||
if (typeof dash.layout.spec !== 'object' || dash.layout.spec === null) {
|
||||
return false;
|
||||
throw new Error('Layout spec is not an object or is null');
|
||||
}
|
||||
if (!Array.isArray(dash.layout.spec.items)) {
|
||||
return false;
|
||||
throw new Error('Layout spec items is not an array');
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -39,7 +39,7 @@ export function ensureV2Response(
|
||||
const spec: DashboardV2Spec = {
|
||||
title: dashboard.title,
|
||||
description: dashboard.description,
|
||||
tags: dashboard.tags,
|
||||
tags: dashboard.tags ?? [],
|
||||
schemaVersion: dashboard.schemaVersion,
|
||||
cursorSync: transformCursorSynctoEnum(dashboard.graphTooltip),
|
||||
preload: dashboard.preload || dashboardDefaults.preload,
|
||||
|
Loading…
Reference in New Issue
Block a user