DashboardScene: Support time region migrations (#84147)

* DashboardScene: Support time region migrations

* Update

* Update

* Update

* fix

* Fix lock

* fix tests

* Fix migrations test

---------

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Torkel Ödegaard 2024-03-28 07:39:26 +01:00 committed by GitHub
parent e5d1cd8ea5
commit d290aaff46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 287 additions and 164 deletions

View File

@ -0,0 +1,73 @@
import React from 'react';
import {
SceneDataLayerProviderState,
SceneDataLayerProvider,
SceneDataLayerSetBase,
SceneComponentProps,
} from '@grafana/scenes';
import { AlertStatesDataLayer } from './AlertStatesDataLayer';
export interface DashboardDataLayerSetState extends SceneDataLayerProviderState {
alertStatesLayer?: AlertStatesDataLayer;
annotationLayers: SceneDataLayerProvider[];
}
export class DashboardDataLayerSet
extends SceneDataLayerSetBase<DashboardDataLayerSetState>
implements SceneDataLayerProvider
{
public constructor(state: Partial<DashboardDataLayerSetState>) {
super({
...state,
name: state.name ?? 'Data layers',
annotationLayers: state.annotationLayers ?? [],
});
this.addActivationHandler(() => this._onActivate());
}
private _onActivate() {
this._subs.add(
this.subscribeToState((newState, oldState) => {
if (newState.annotationLayers !== oldState.annotationLayers) {
this.querySub?.unsubscribe();
this.subscribeToAllLayers(this.getAllLayers());
}
})
);
this.subscribeToAllLayers(this.getAllLayers());
return () => {
this.querySub?.unsubscribe();
};
}
public addAnnotationLayer(layer: SceneDataLayerProvider) {
this.setState({ annotationLayers: [...this.state.annotationLayers, layer] });
}
private getAllLayers() {
const layers = [...this.state.annotationLayers];
if (this.state.alertStatesLayer) {
layers.push(this.state.alertStatesLayer);
}
return layers;
}
public static Component = ({ model }: SceneComponentProps<DashboardDataLayerSet>) => {
const { annotationLayers } = model.useState();
return (
<>
{annotationLayers.map((layer) => (
<layer.Component model={layer} key={layer.state.key} />
))}
</>
);
};
}

View File

@ -1,9 +1,10 @@
import { AnnotationChangeEvent, AnnotationEventUIModel, CoreApp, DataFrame } from '@grafana/data';
import { AdHocFiltersVariable, dataLayers, SceneDataLayerSet, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes';
import { AdHocFiltersVariable, dataLayers, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema';
import { AdHocFilterItem, PanelContext } from '@grafana/ui';
import { deleteAnnotation, saveAnnotation, updateAnnotation } from 'app/features/annotations/api';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
import { DashboardScene } from './DashboardScene';
@ -128,13 +129,13 @@ export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelConte
}
function getBuiltInAnnotationsLayer(scene: DashboardScene): dataLayers.AnnotationsDataLayer | undefined {
const set = dashboardSceneGraph.getDataLayers(scene);
// When there is no builtin annotations query we disable the ability to add annotations
if (scene.state.$data instanceof SceneDataLayerSet) {
for (const layer of scene.state.$data.state.layers) {
if (layer instanceof dataLayers.AnnotationsDataLayer) {
if (layer.state.isEnabled && layer.state.query.builtIn) {
return layer;
}
for (const layer of set.state.annotationLayers) {
if (layer instanceof dataLayers.AnnotationsDataLayer) {
if (layer.state.isEnabled && layer.state.query.builtIn) {
return layer;
}
}
}

View File

@ -20,7 +20,6 @@ import {
GroupByVariable,
QueryVariable,
SceneDataLayerControls,
SceneDataLayerSet,
SceneDataTransformer,
SceneGridLayout,
SceneGridRow,
@ -42,6 +41,7 @@ import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard
import { DashboardDataDTO } from 'app/types';
import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { PanelTimeRange } from '../scene/PanelTimeRange';
@ -1230,26 +1230,26 @@ describe('transformSaveModelToScene', () => {
it('Should build correct scene model', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
expect(scene.state.$data).toBeInstanceOf(SceneDataLayerSet);
expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet);
expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls);
const dataLayers = scene.state.$data as SceneDataLayerSet;
expect(dataLayers.state.layers).toHaveLength(4);
expect(dataLayers.state.layers[0].state.name).toBe('Annotations & Alerts');
expect(dataLayers.state.layers[0].state.isEnabled).toBe(true);
expect(dataLayers.state.layers[0].state.isHidden).toBe(false);
const dataLayers = scene.state.$data as DashboardDataLayerSet;
expect(dataLayers.state.annotationLayers).toHaveLength(4);
expect(dataLayers.state.annotationLayers[0].state.name).toBe('Annotations & Alerts');
expect(dataLayers.state.annotationLayers[0].state.isEnabled).toBe(true);
expect(dataLayers.state.annotationLayers[0].state.isHidden).toBe(false);
expect(dataLayers.state.layers[1].state.name).toBe('Enabled');
expect(dataLayers.state.layers[1].state.isEnabled).toBe(true);
expect(dataLayers.state.layers[1].state.isHidden).toBe(false);
expect(dataLayers.state.annotationLayers[1].state.name).toBe('Enabled');
expect(dataLayers.state.annotationLayers[1].state.isEnabled).toBe(true);
expect(dataLayers.state.annotationLayers[1].state.isHidden).toBe(false);
expect(dataLayers.state.layers[2].state.name).toBe('Disabled');
expect(dataLayers.state.layers[2].state.isEnabled).toBe(false);
expect(dataLayers.state.layers[2].state.isHidden).toBe(false);
expect(dataLayers.state.annotationLayers[2].state.name).toBe('Disabled');
expect(dataLayers.state.annotationLayers[2].state.isEnabled).toBe(false);
expect(dataLayers.state.annotationLayers[2].state.isHidden).toBe(false);
expect(dataLayers.state.layers[3].state.name).toBe('Hidden');
expect(dataLayers.state.layers[3].state.isEnabled).toBe(true);
expect(dataLayers.state.layers[3].state.isHidden).toBe(true);
expect(dataLayers.state.annotationLayers[3].state.name).toBe('Hidden');
expect(dataLayers.state.annotationLayers[3].state.isEnabled).toBe(true);
expect(dataLayers.state.annotationLayers[3].state.isHidden).toBe(true);
});
});
@ -1258,12 +1258,11 @@ describe('transformSaveModelToScene', () => {
config.unifiedAlertingEnabled = true;
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
expect(scene.state.$data).toBeInstanceOf(SceneDataLayerSet);
expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet);
expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls);
const dataLayers = scene.state.$data as SceneDataLayerSet;
expect(dataLayers.state.layers).toHaveLength(5);
expect(dataLayers.state.layers[4].state.name).toBe('Alert States');
const dataLayers = scene.state.$data as DashboardDataLayerSet;
expect(dataLayers.state.alertStatesLayer).toBeDefined();
});
it('Should add alert states data layer if any panel has a legacy alert defined', () => {
@ -1272,12 +1271,11 @@ describe('transformSaveModelToScene', () => {
dashboard.panels![0].alert = {};
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
expect(scene.state.$data).toBeInstanceOf(SceneDataLayerSet);
expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet);
expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls);
const dataLayers = scene.state.$data as SceneDataLayerSet;
expect(dataLayers.state.layers).toHaveLength(5);
expect(dataLayers.state.layers[4].state.name).toBe('Alert States');
const dataLayers = scene.state.$data as DashboardDataLayerSet;
expect(dataLayers.state.alertStatesLayer).toBeDefined();
});
});

View File

@ -20,7 +20,6 @@ import {
behaviors,
VizPanelState,
SceneGridItemLike,
SceneDataLayerSet,
SceneDataLayerProvider,
SceneDataLayerControls,
TextBoxVariable,
@ -37,6 +36,7 @@ import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget';
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
import { registerDashboardMacro } from '../scene/DashboardMacro';
import { DashboardScene } from '../scene/DashboardScene';
@ -216,8 +216,9 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]):
}
export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) {
let variables: SceneVariableSet | undefined = undefined;
let layers: SceneDataLayerProvider[] = [];
let variables: SceneVariableSet | undefined;
let annotationLayers: SceneDataLayerProvider[] = [];
let alertStatesLayer: AlertStatesDataLayer | undefined;
if (oldModel.templating?.list?.length) {
const variableObjects = oldModel.templating.list
@ -244,7 +245,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
}
if (oldModel.annotations?.list?.length && !oldModel.isSnapshot()) {
layers = oldModel.annotations?.list.map((a) => {
annotationLayers = oldModel.annotations?.list.map((a) => {
// Each annotation query is an individual data layer
return new DashboardAnnotationsDataLayer({
key: `annotations-${a.name}`,
@ -264,12 +265,10 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
}
if (shouldUseAlertStatesLayer) {
layers.push(
new AlertStatesDataLayer({
key: 'alert-states',
name: 'Alert States',
})
);
alertStatesLayer = new AlertStatesDataLayer({
key: 'alert-states',
name: 'Alert States',
});
}
const dashboardScene = new DashboardScene({
@ -307,12 +306,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
trackIfIsEmpty,
new behaviors.LiveNowTimer(oldModel.liveNow),
],
$data:
layers.length > 0
? new SceneDataLayerSet({
layers,
})
: undefined,
$data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }),
controls: new DashboardControls({
variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()],
timePicker: new SceneTimePicker({}),

View File

@ -16,13 +16,14 @@ import {
} from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime';
import { MultiValueVariable, SceneDataLayerSet, SceneGridLayout, SceneGridRow, VizPanel } from '@grafana/scenes';
import { MultiValueVariable, SceneGridLayout, SceneGridRow, VizPanel } from '@grafana/scenes';
import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema';
import { PanelModel } from 'app/features/dashboard/state';
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
import { reduceTransformRegistryItem } from 'app/features/transformers/editors/ReduceTransformerEditor';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
@ -402,10 +403,11 @@ describe('transformSceneToSaveModel', () => {
expect(saveModel.annotations?.list?.length).toBe(4);
expect(saveModel.annotations?.list).toMatchSnapshot();
});
it('should transform annotations to save model after state changes', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
const layers = (scene.state.$data as SceneDataLayerSet)?.state.layers;
const layers = (scene.state.$data as DashboardDataLayerSet)?.state.annotationLayers;
const enabledLayer = layers[1];
const hiddenLayer = layers[3];

View File

@ -10,7 +10,6 @@ import {
SceneDataTransformer,
SceneVariableSet,
LocalValueVariable,
SceneDataLayerSet,
} from '@grafana/scenes';
import {
AnnotationQuery,
@ -33,6 +32,7 @@ import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/Dashboard
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
@ -77,10 +77,9 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
}
let annotations: AnnotationQuery[] = [];
if (data instanceof SceneDataLayerSet) {
const layers = data.state.layers;
annotations = dataLayersToAnnotations(layers);
if (data instanceof DashboardDataLayerSet) {
annotations = dataLayersToAnnotations(data.state.annotationLayers);
}
if (variablesSet instanceof SceneVariableSet) {

View File

@ -1,10 +1,10 @@
import { map, of } from 'rxjs';
import { AnnotationQuery, DataQueryRequest, DataSourceApi, LoadingState, PanelData } from '@grafana/data';
import { SceneDataLayerSet, SceneGridLayout, SceneTimeRange, dataLayers } from '@grafana/scenes';
import { SceneGridLayout, SceneTimeRange, dataLayers } from '@grafana/scenes';
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardScene } from '../scene/DashboardScene';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { activateFullSceneTree } from '../utils/test-utils';
@ -48,40 +48,26 @@ jest.mock('@grafana/runtime', () => ({
describe('AnnotationsEditView', () => {
describe('Dashboard annotations state', () => {
let annotationsView: AnnotationsEditView;
let dashboardScene: DashboardScene;
beforeEach(async () => {
const result = await buildTestScene();
annotationsView = result.annotationsView;
dashboardScene = result.dashboard;
});
it('should return the correct urlKey', () => {
expect(annotationsView.getUrlKey()).toBe('annotations');
});
it('should return the annotations length', () => {
expect(annotationsView.getAnnotationsLength()).toBe(1);
});
it('should return 0 if no annotations', () => {
dashboardScene.setState({
$data: new SceneDataLayerSet({ layers: [] }),
});
expect(annotationsView.getAnnotationsLength()).toBe(0);
});
it('should add a new annotation and group it with the other annotations', () => {
const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard());
expect(dataLayers?.state.layers.length).toBe(2);
expect(dataLayers?.state.annotationLayers.length).toBe(1);
annotationsView.onNew();
expect(dataLayers?.state.layers.length).toBe(3);
expect(dataLayers?.state.layers[1].state.name).toBe(newAnnotationName);
expect(dataLayers?.state.layers[1].isActive).toBe(true);
expect(dataLayers?.state.annotationLayers.length).toBe(2);
expect(dataLayers?.state.annotationLayers[1].state.name).toBe(newAnnotationName);
expect(dataLayers?.state.annotationLayers[1].isActive).toBe(true);
});
it('should move an annotation up one position', () => {
@ -89,13 +75,13 @@ describe('AnnotationsEditView', () => {
annotationsView.onNew();
expect(dataLayers?.state.layers.length).toBe(3);
expect(dataLayers?.state.layers[0].state.name).toBe('test');
expect(dataLayers?.state.annotationLayers.length).toBe(2);
expect(dataLayers?.state.annotationLayers[0].state.name).toBe('test');
annotationsView.onMove(1, MoveDirection.UP);
expect(dataLayers?.state.layers.length).toBe(3);
expect(dataLayers?.state.layers[0].state.name).toBe(newAnnotationName);
expect(dataLayers?.state.annotationLayers.length).toBe(2);
expect(dataLayers?.state.annotationLayers[0].state.name).toBe(newAnnotationName);
});
it('should move an annotation down one position', () => {
@ -103,33 +89,32 @@ describe('AnnotationsEditView', () => {
annotationsView.onNew();
expect(dataLayers?.state.layers.length).toBe(3);
expect(dataLayers?.state.layers[0].state.name).toBe('test');
expect(dataLayers?.state.annotationLayers.length).toBe(2);
expect(dataLayers?.state.annotationLayers[0].state.name).toBe('test');
annotationsView.onMove(0, MoveDirection.DOWN);
expect(dataLayers?.state.layers.length).toBe(3);
expect(dataLayers?.state.layers[0].state.name).toBe(newAnnotationName);
expect(dataLayers?.state.annotationLayers.length).toBe(2);
expect(dataLayers?.state.annotationLayers[0].state.name).toBe(newAnnotationName);
});
it('should delete annotation at index', () => {
const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard());
expect(dataLayers?.state.layers.length).toBe(2);
expect(dataLayers?.state.annotationLayers.length).toBe(1);
annotationsView.onDelete(0);
expect(dataLayers?.state.layers.length).toBe(1);
expect(dataLayers?.state.layers[0].state.name).toBe('Alert States');
expect(dataLayers?.state.annotationLayers.length).toBe(0);
});
it('should update an annotation at index', () => {
const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard());
expect(dataLayers?.state.layers[0].state.name).toBe('test');
expect(dataLayers?.state.annotationLayers[0].state.name).toBe('test');
const annotation: AnnotationQuery = {
...(dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query,
...(dataLayers?.state.annotationLayers[0] as dataLayers.AnnotationsDataLayer).state.query,
};
annotation.name = 'new name';
@ -138,12 +123,16 @@ describe('AnnotationsEditView', () => {
annotation.iconColor = 'blue';
annotationsView.onUpdate(annotation, 0);
expect(dataLayers?.state.layers.length).toBe(2);
expect(dataLayers?.state.layers[0].state.name).toBe('new name');
expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.name).toBe('new name');
expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.hide).toBe(true);
expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.enable).toBe(false);
expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.iconColor).toBe('blue');
expect(dataLayers?.state.annotationLayers.length).toBe(1);
expect(dataLayers?.state.annotationLayers[0].state.name).toBe('new name');
expect((dataLayers?.state.annotationLayers[0] as dataLayers.AnnotationsDataLayer).state.query.name).toBe(
'new name'
);
expect((dataLayers?.state.annotationLayers[0] as dataLayers.AnnotationsDataLayer).state.query.hide).toBe(true);
expect((dataLayers?.state.annotationLayers[0] as dataLayers.AnnotationsDataLayer).state.query.enable).toBe(false);
expect((dataLayers?.state.annotationLayers[0] as dataLayers.AnnotationsDataLayer).state.query.iconColor).toBe(
'blue'
);
});
});
});
@ -158,8 +147,8 @@ async function buildTestScene() {
meta: {
canEdit: true,
},
$data: new SceneDataLayerSet({
layers: [
$data: new DashboardDataLayerSet({
annotationLayers: [
new DashboardAnnotationsDataLayer({
key: `annotations-test`,
query: {
@ -175,10 +164,6 @@ async function buildTestScene() {
isEnabled: true,
isHidden: false,
}),
new AlertStatesDataLayer({
key: 'alert-states',
name: 'Alert States',
}),
],
}),
body: new SceneGridLayout({

View File

@ -40,7 +40,7 @@ export class AnnotationsEditView extends SceneObjectBase<AnnotationsEditViewStat
public getDataLayer(editIndex: number): dataLayers.AnnotationsDataLayer {
const data = dashboardSceneGraph.getDataLayers(this._dashboard);
const layer = data.state.layers[editIndex];
const layer = data.state.annotationLayers[editIndex];
if (!(layer instanceof dataLayers.AnnotationsDataLayer)) {
throw new Error('AnnotationsDataLayer not found at index ' + editIndex);
@ -49,12 +49,6 @@ export class AnnotationsEditView extends SceneObjectBase<AnnotationsEditViewStat
return layer;
}
public getAnnotationsLength(): number {
return dashboardSceneGraph
.getDataLayers(this._dashboard)
.state.layers.filter((layer) => layer instanceof DashboardAnnotationsDataLayer).length;
}
public getDashboard(): DashboardScene {
return this._dashboard;
}
@ -76,18 +70,9 @@ export class AnnotationsEditView extends SceneObjectBase<AnnotationsEditViewStat
const data = dashboardSceneGraph.getDataLayers(this._dashboard);
const layers = [...data.state.layers];
data.addAnnotationLayer(newAnnotation);
//keep annotation layers together
layers.splice(this.getAnnotationsLength(), 0, newAnnotation);
data.setState({
layers,
});
newAnnotation.activate();
this.setState({ editIndex: this.getAnnotationsLength() - 1 });
this.setState({ editIndex: data.state.annotationLayers.length - 1 });
};
public onEdit = (idx: number) => {
@ -100,25 +85,21 @@ export class AnnotationsEditView extends SceneObjectBase<AnnotationsEditViewStat
public onMove = (idx: number, direction: MoveDirection) => {
const data = dashboardSceneGraph.getDataLayers(this._dashboard);
const layers = [...data.state.annotationLayers];
const layers = [...data.state.layers];
const [layer] = layers.splice(idx, 1);
layers.splice(idx + direction, 0, layer);
data.setState({
layers,
});
data.setState({ annotationLayers: layers });
};
public onDelete = (idx: number) => {
const data = dashboardSceneGraph.getDataLayers(this._dashboard);
const layers = [...data.state.annotationLayers];
const layers = [...data.state.layers];
layers.splice(idx, 1);
data.setState({
layers,
});
data.setState({ annotationLayers: layers });
};
public onUpdate = (annotation: AnnotationQuery, editIndex: number) => {
@ -139,14 +120,14 @@ export class AnnotationsEditView extends SceneObjectBase<AnnotationsEditViewStat
function AnnotationsSettingsView({ model }: SceneComponentProps<AnnotationsEditView>) {
const dashboard = model.getDashboard();
const { layers } = dashboardSceneGraph.getDataLayers(dashboard).useState();
const { annotationLayers } = dashboardSceneGraph.getDataLayers(dashboard).useState();
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
const { editIndex } = model.useState();
const panels = dashboardSceneGraph.getVizPanels(dashboard);
const annotations: AnnotationQuery[] = dataLayersToAnnotations(layers);
const annotations: AnnotationQuery[] = dataLayersToAnnotations(annotationLayers);
if (editIndex != null && editIndex < model.getAnnotationsLength()) {
if (editIndex != null && editIndex < annotationLayers.length) {
return (
<AnnotationsSettingsEditView
annotationLayer={model.getDataLayer(editIndex)}

View File

@ -134,6 +134,6 @@ export const AnnotationSettingsList = ({ annotations, onNew, onEdit, onMove, onD
const getStyles = () => ({
table: css({
width: '100%',
overflowX: 'scroll',
overflowX: 'auto',
}),
});

View File

@ -1,17 +1,9 @@
import {
SceneDataLayerSet,
SceneGridLayout,
SceneGridRow,
SceneQueryRunner,
SceneTimeRange,
VizPanel,
behaviors,
} from '@grafana/scenes';
import { SceneGridLayout, SceneGridRow, SceneQueryRunner, SceneTimeRange, VizPanel, behaviors } from '@grafana/scenes';
import { DashboardCursorSync } from '@grafana/schema';
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
@ -75,8 +67,8 @@ describe('dashboardSceneGraph', () => {
it('should return the scene data layers', () => {
const dataLayers = dashboardSceneGraph.getDataLayers(scene);
expect(dataLayers).toBeInstanceOf(SceneDataLayerSet);
expect(dataLayers?.state.layers.length).toBe(2);
expect(dataLayers).toBeInstanceOf(DashboardDataLayerSet);
expect(dataLayers?.state.annotationLayers.length).toBe(1);
});
it('should throw if there are no scene data layers', () => {
@ -84,7 +76,7 @@ describe('dashboardSceneGraph', () => {
$data: undefined,
});
expect(() => dashboardSceneGraph.getDataLayers(scene)).toThrow('SceneDataLayerSet not found');
expect(() => dashboardSceneGraph.getDataLayers(scene)).toThrow('DashboardDataLayerSet not found');
});
});
@ -241,8 +233,8 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
sync: DashboardCursorSync.Crosshair,
}),
],
$data: new SceneDataLayerSet({
layers: [
$data: new DashboardDataLayerSet({
annotationLayers: [
new DashboardAnnotationsDataLayer({
key: `annotation`,
query: {
@ -255,10 +247,6 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
isEnabled: true,
isHidden: false,
}),
new AlertStatesDataLayer({
key: 'alert-states',
name: 'Alert States',
}),
],
}),
body: new SceneGridLayout({

View File

@ -1,5 +1,6 @@
import { VizPanel, SceneGridRow, SceneDataLayerSet, sceneGraph, SceneGridLayout, behaviors } from '@grafana/scenes';
import { VizPanel, SceneGridRow, sceneGraph, SceneGridLayout, behaviors } from '@grafana/scenes';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
@ -53,11 +54,11 @@ function getVizPanels(scene: DashboardScene): VizPanel[] {
return panels;
}
function getDataLayers(scene: DashboardScene): SceneDataLayerSet {
function getDataLayers(scene: DashboardScene): DashboardDataLayerSet {
const data = sceneGraph.getData(scene);
if (!(data instanceof SceneDataLayerSet)) {
throw new Error('SceneDataLayerSet not found');
if (!(data instanceof DashboardDataLayerSet)) {
throw new Error('DashboardDataLayerSet not found');
}
return data;

View File

@ -552,6 +552,37 @@ exports[`Graph Migrations time regions should migrate 1`] = `
}
`;
exports[`Graph Migrations time regions should migrate in scenes dashboard 1`] = `
{
"alert": undefined,
"datasource": {
"type": "datasource",
"uid": "gdev-testdata",
},
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "points",
"spanNulls": false,
},
},
"overrides": [],
},
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true,
},
"tooltip": {
"mode": "single",
"sort": "none",
},
},
}
`;
exports[`Graph Migrations transforms should preserve "constant" transform 1`] = `
{
"defaults": {

View File

@ -5,6 +5,9 @@ import { TooltipDisplayMode, SortOrder } from '@grafana/schema';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardModel, PanelModel as PanelModelState } from 'app/features/dashboard/state';
import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
import { dataLayersToAnnotations } from 'app/features/dashboard-scene/serialization/dataLayersToAnnotations';
import { transformSaveModelToScene } from 'app/features/dashboard-scene/serialization/transformSaveModelToScene';
import { dashboardSceneGraph } from 'app/features/dashboard-scene/utils/dashboardSceneGraph';
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { graphPanelChangedHandler } from './migrations';
@ -125,6 +128,42 @@ describe('Graph Migrations', () => {
).toHaveLength(1);
expect(panel).toMatchSnapshot();
});
test('should migrate in scenes dashboard', () => {
const old = {
angular: {
timeRegions: [
{
colorMode: 'red',
fill: true,
fillColor: 'rgba(234, 112, 112, 0.12)',
fromDayOfWeek: 1,
line: true,
lineColor: 'rgba(237, 46, 24, 0.60)',
op: 'time',
},
],
},
};
const panel = { datasource: { type: 'datasource', uid: 'gdev-testdata' } } as PanelModel;
dashboard.panels.push(new PanelModelState(panel));
const scene = transformSaveModelToScene({ dashboard, meta: {} });
window.__grafanaSceneContext = scene;
panel.options = graphPanelChangedHandler(panel, 'graph', old, prevFieldConfig);
const layers = dashboardSceneGraph.getDataLayers(scene).state.annotationLayers;
const annotations = dataLayersToAnnotations(layers);
expect(annotations).toHaveLength(2); // built-in + time region
expect(
annotations.filter((annotation) => annotation.target?.queryType === GrafanaQueryType.TimeRegions)
).toHaveLength(1);
expect(panel).toMatchSnapshot();
});
});
describe('legend', () => {

View File

@ -37,6 +37,9 @@ import {
import { TimeRegionConfig } from 'app/core/utils/timeRegions';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DashboardAnnotationsDataLayer } from 'app/features/dashboard-scene/scene/DashboardAnnotationsDataLayer';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { dashboardSceneGraph } from 'app/features/dashboard-scene/utils/dashboardSceneGraph';
import { GrafanaQuery, GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { defaultGraphConfig } from './config';
@ -61,17 +64,8 @@ export const graphPanelChangedHandler: PanelTypeChangedHandler = (
panel: panel,
});
const dashboard = getDashboardSrv().getCurrent();
if (dashboard && annotations?.length > 0) {
dashboard.annotations.list = [...dashboard.annotations.list, ...annotations];
// Trigger a full dashboard refresh when annotations change
if (dashboardRefreshDebouncer == null) {
dashboardRefreshDebouncer = setTimeout(() => {
dashboardRefreshDebouncer = null;
getTimeSrv().refreshTimeModel();
});
}
if (annotations?.length > 0) {
addAnnotationsToDashboard(annotations);
}
panel.fieldConfig = fieldConfig; // Mutates the incoming panel
@ -400,7 +394,7 @@ export function graphToTimeseriesOptions(angular: any): {
// timeRegions migration
if (angular.timeRegions?.length) {
let regions = angular.timeRegions.map((old: GraphTimeRegionConfig, idx: number) => ({
name: `T${idx + 1}`,
name: `T${idx}`,
color: old.colorMode !== 'custom' ? old.colorMode : old.fillColor,
line: old.line,
fill: old.fill,
@ -423,7 +417,7 @@ export function graphToTimeseriesOptions(angular: any): {
ids: [angular.panel.id],
},
iconColor: region.fillColor ?? (region as any).color,
name: `T${idx + 1}`,
name: `Time region for panel ${angular.panel.title}${idx > 0 ? ` ${idx}` : ''}`,
target: {
queryType: GrafanaQueryType.TimeRegions,
refId: 'Anno',
@ -751,3 +745,40 @@ function getStackingFromOverrides(value: Boolean | string) {
group: isString(value) ? value : defaultGroupName,
};
}
function addAnnotationsToDashboard(annotations: AnnotationQuery[]) {
const scene = window.__grafanaSceneContext;
if (scene instanceof DashboardScene) {
const dataLayers = dashboardSceneGraph.getDataLayers(scene);
const annotationLayers = [...dataLayers.state.annotationLayers];
for (let annotation of annotations) {
const newAnnotation = new DashboardAnnotationsDataLayer({
key: `annotations-${annotation.name}`,
query: annotation,
name: annotation.name,
isEnabled: annotation.enable,
isHidden: annotation.hide,
});
annotationLayers.push(newAnnotation);
}
dataLayers.setState({ annotationLayers });
return;
}
const dashboard = getDashboardSrv().getCurrent();
if (dashboard) {
dashboard.annotations.list = [...dashboard.annotations.list, ...annotations];
// Trigger a full dashboard refresh when annotations change
if (dashboardRefreshDebouncer == null) {
dashboardRefreshDebouncer = setTimeout(() => {
dashboardRefreshDebouncer = null;
getTimeSrv().refreshTimeModel();
});
}
}
}