DashboardScene: Integrate annotations (#74610)

* WIP Dashboard to Scenes: Annotations

* Bump scenes

* Enable annotations and controls

* Betterer

* Update snapshots

* Test fix
This commit is contained in:
Dominik Prokop 2023-09-20 11:50:35 +02:00 committed by GitHub
parent ae32d43e2b
commit 157859aede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 365 additions and 14 deletions

View File

@ -1794,12 +1794,15 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]

View File

@ -251,7 +251,7 @@
"@grafana/lezer-traceql": "0.0.6",
"@grafana/monaco-logql": "^0.0.7",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^1.3.1",
"@grafana/scenes": "^1.3.3",
"@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*",
"@kusto/monaco-kusto": "^7.4.0",

View File

@ -13,6 +13,16 @@ import { setupLoadDashboardMock } from '../utils/test-utils';
import { DashboardScenePage, Props } from './DashboardScenePage';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: () => {
return {
get: jest.fn().mockResolvedValue({}),
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
};
},
}));
function setup() {
const context = getGrafanaContextMock();
const props: Props = {

View File

@ -1,5 +1,6 @@
import { StateManagerBase } from 'app/core/services/StateManagerBase';
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { buildPanelEditScene, PanelEditor } from '../panel-edit/PanelEditor';
import { DashboardScene } from '../scene/DashboardScene';
@ -66,6 +67,7 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
}
public clearState() {
getDashboardSrv().setCurrent(undefined);
this.setState({ dashboard: undefined, loadError: undefined, isLoading: false, panelEditor: undefined });
}
}

View File

@ -1,13 +1,16 @@
import { Unsubscribable } from 'rxjs';
import { Observable, ReplaySubject, Unsubscribable } from 'rxjs';
import { getDefaultTimeRange } from '@grafana/data';
import {
SceneDataProvider,
SceneDataProviderResult,
SceneDataState,
SceneDataTransformer,
SceneDeactivationHandler,
SceneObject,
SceneObjectBase,
} from '@grafana/scenes';
import { LoadingState } from '@grafana/schema';
import { DashboardQuery } from 'app/plugins/datasource/dashboard/types';
import { getVizPanelKeyForPanelId } from '../utils/utils';
@ -19,6 +22,7 @@ export interface ShareQueryDataProviderState extends SceneDataState {
export class ShareQueryDataProvider extends SceneObjectBase<ShareQueryDataProviderState> implements SceneDataProvider {
private _querySub: Unsubscribable | undefined;
private _sourceDataDeactivationHandler?: SceneDeactivationHandler;
private _results = new ReplaySubject<SceneDataProviderResult>();
constructor(state: ShareQueryDataProviderState) {
super(state);
@ -40,6 +44,10 @@ export class ShareQueryDataProvider extends SceneObjectBase<ShareQueryDataProvid
});
}
public getResultsStream(): Observable<SceneDataProviderResult> {
return this._results;
}
private _subscribeToSource() {
const { query } = this.state;
@ -78,7 +86,18 @@ export class ShareQueryDataProvider extends SceneObjectBase<ShareQueryDataProvid
}
}
this._querySub = sourceData.subscribeToState((state) => this.setState({ data: state.data }));
this._querySub = sourceData.subscribeToState((state) => {
this._results.next({
origin: this,
data: state.data || {
state: LoadingState.Done,
series: [],
timeRange: getDefaultTimeRange(),
},
});
this.setState({ data: state.data });
});
// Copy the initial state
this.setState({ data: sourceData.state.data });

View File

@ -1,7 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transformSceneToSaveModel Annotations should transform annotations to save model 1`] = `
[
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana",
},
"enable": true,
"hide": false,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard",
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": true,
"hide": false,
"iconColor": "red",
"name": "Enabled",
"target": {
"lines": 4,
"refId": "Anno",
"scenarioId": "annotations",
},
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": false,
"hide": false,
"iconColor": "yellow",
"name": "Disabled",
"target": {
"lines": 5,
"refId": "Anno",
"scenarioId": "annotations",
},
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": true,
"hide": true,
"iconColor": "dark-purple",
"name": "Hidden",
"target": {
"lines": 6,
"refId": "Anno",
"scenarioId": "annotations",
},
},
]
`;
exports[`transformSceneToSaveModel Given a scene with rows Should transform back to peristed model 1`] = `
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --",
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard",
},
],
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
@ -120,6 +198,67 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
exports[`transformSceneToSaveModel Given a simple scene Should transform back to peristed model 1`] = `
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana",
},
"enable": true,
"hide": false,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard",
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": true,
"hide": false,
"iconColor": "red",
"name": "Enabled",
"target": {
"lines": 4,
"refId": "Anno",
"scenarioId": "annotations",
},
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": false,
"hide": false,
"iconColor": "yellow",
"name": "Disabled",
"target": {
"lines": 5,
"refId": "Anno",
"scenarioId": "annotations",
},
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": true,
"hide": true,
"iconColor": "dark-purple",
"name": "Hidden",
"target": {
"lines": 6,
"refId": "Anno",
"scenarioId": "annotations",
},
},
],
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,

View File

@ -8,10 +8,53 @@
"uid": "grafana"
},
"enable": true,
"hide": true,
"hide": false,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"enable": true,
"iconColor": "red",
"name": "Enabled",
"target": {
"lines": 4,
"refId": "Anno",
"scenarioId": "annotations"
}
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"enable": false,
"iconColor": "yellow",
"name": "Disabled",
"target": {
"lines": 5,
"refId": "Anno",
"scenarioId": "annotations"
}
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"enable": true,
"hide": true,
"iconColor": "dark-purple",
"name": "Hidden",
"target": {
"lines": 6,
"refId": "Anno",
"scenarioId": "annotations"
}
}
]
},

View File

@ -5,6 +5,8 @@ import {
CustomVariable,
DataSourceVariable,
QueryVariable,
SceneDataLayerControls,
SceneDataLayers,
SceneDataTransformer,
SceneGridItem,
SceneGridLayout,
@ -23,6 +25,7 @@ import { PanelTimeRange } from '../scene/PanelTimeRange';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { ShareQueryDataProvider } from '../scene/ShareQueryDataProvider';
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json';
import {
createDashboardSceneFromDashboardModel,
@ -637,6 +640,33 @@ describe('transformSaveModelToScene', () => {
expect(lastRow.state.isCollapsed).toBe(true);
});
});
describe('Annotation queries', () => {
it('Should build correct scene model', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
expect(scene.state.$data).toBeInstanceOf(SceneDataLayers);
expect(scene.state.controls![0]).toBeInstanceOf(SceneDataLayerControls);
const dataLayers = scene.state.$data as SceneDataLayers;
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);
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.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.layers[3].state.name).toBe('Hidden');
expect(dataLayers.state.layers[3].state.isEnabled).toBe(true);
expect(dataLayers.state.layers[3].state.isHidden).toBe(true);
});
});
});
function buildGridItemForTest(saveModel: Partial<Panel>): { gridItem: SceneGridItem; vizPanel: VizPanel } {

View File

@ -26,7 +26,12 @@ import {
behaviors,
VizPanelState,
SceneGridItemLike,
SceneDataLayers,
dataLayers,
SceneDataLayerProvider,
SceneDataLayerControls,
} from '@grafana/scenes';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { DashboardDTO } from 'app/types';
@ -51,6 +56,9 @@ export function transformSaveModelToScene(rsp: DashboardDTO): DashboardScene {
autoMigrateOldPanels: true,
});
// Setting for built-in annotations query to run
getDashboardSrv().setCurrent(oldModel);
return createDashboardSceneFromDashboardModel(oldModel);
}
@ -148,6 +156,7 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]):
export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) {
let variables: SceneVariableSet | undefined = undefined;
let layers: SceneDataLayerProvider[] = [];
if (oldModel.templating?.list?.length) {
const variableObjects = oldModel.templating.list
@ -168,7 +177,20 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
});
}
if (oldModel.annotations?.list?.length) {
layers = oldModel.annotations?.list.map((a) => {
// Each annotation query is an individual data layer
return new dataLayers.AnnotationsDataLayer({
query: a,
name: a.name,
isEnabled: Boolean(a.enable),
isHidden: Boolean(a.hide),
});
});
}
const controls: SceneObject[] = [
new SceneDataLayerControls(),
new VariableValueSelectors({}),
new SceneControlsSpacer(),
new SceneTimePicker({}),
@ -193,6 +215,12 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
sync: oldModel.graphTooltip,
}),
],
$data:
layers.length > 0
? new SceneDataLayers({
layers,
})
: undefined,
controls: controls,
});
}

View File

@ -1,4 +1,11 @@
import { MultiValueVariable, SceneGridItemLike, SceneGridLayout, SceneGridRow, SceneVariable } from '@grafana/scenes';
import {
MultiValueVariable,
SceneDataLayers,
SceneGridItemLike,
SceneGridLayout,
SceneGridRow,
SceneVariable,
} from '@grafana/scenes';
import { Panel, RowPanel } from '@grafana/schema';
import { PanelModel } from 'app/features/dashboard/state';
@ -94,6 +101,36 @@ describe('transformSceneToSaveModel', () => {
expect(saveModel.gridPos?.h).toBe(8);
});
});
describe('Annotations', () => {
it('should transform annotations to save model', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
const saveModel = transformSceneToSaveModel(scene);
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 SceneDataLayers)?.state.layers;
const enabledLayer = layers[1];
const hiddenLayer = layers[3];
enabledLayer.setState({
isEnabled: false,
});
hiddenLayer.setState({
isHidden: false,
});
const saveModel = transformSceneToSaveModel(scene);
expect(saveModel.annotations?.list?.length).toBe(4);
expect(saveModel.annotations?.list?.[1].enable).toEqual(false);
expect(saveModel.annotations?.list?.[3].hide).toEqual(false);
});
});
});
export function buildGridItemFromPanelSchema(panel: Partial<Panel>): SceneGridItemLike {

View File

@ -1,5 +1,14 @@
import { SceneGridItem, SceneGridItemLike, SceneGridLayout, SceneGridRow, VizPanel } from '@grafana/scenes';
import { Dashboard, defaultDashboard, FieldConfigSource, Panel, RowPanel } from '@grafana/schema';
import {
SceneDataLayers,
SceneGridItem,
SceneGridItemLike,
SceneGridLayout,
SceneGridRow,
VizPanel,
dataLayers,
SceneDataLayerProvider,
} from '@grafana/scenes';
import { AnnotationQuery, Dashboard, defaultDashboard, FieldConfigSource, Panel, RowPanel } from '@grafana/schema';
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
import { DashboardScene } from '../scene/DashboardScene';
@ -11,6 +20,7 @@ import { getPanelIdForVizPanel } from '../utils/utils';
export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
const state = scene.state;
const timeRange = state.$timeRange!.state;
const data = state.$data;
const body = state.body;
const panels: Panel[] = [];
@ -30,6 +40,13 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
}
}
let annotations: AnnotationQuery[] = [];
if (data instanceof SceneDataLayers) {
const layers = data.state.layers;
annotations = dataLayersToAnnotations(layers);
}
const dashboard: Dashboard = {
...defaultDashboard,
title: state.title,
@ -39,6 +56,9 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
to: timeRange.to,
},
panels,
annotations: {
list: annotations,
},
};
return sortedDeepCloneWithoutNulls(dashboard);
@ -141,3 +161,20 @@ export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array<Pan
panelsArray.push(...panelsInsideRow);
}
}
export function dataLayersToAnnotations(layers: SceneDataLayerProvider[]) {
const annotations: AnnotationQuery[] = [];
for (const layer of layers) {
if (!(layer instanceof dataLayers.AnnotationsDataLayer)) {
continue;
}
annotations.push({
...layer.state.query,
enable: Boolean(layer.state.isEnabled),
hide: Boolean(layer.state.isHidden),
});
}
return annotations;
}

View File

@ -24,6 +24,9 @@ export function createPanelDataProvider(panel: PanelModel): SceneDataProvider |
dataProvider = new SceneQueryRunner({
queries: panel.targets,
maxDataPoints: panel.maxDataPoints ?? undefined,
dataLayerFilter: {
panelId: panel.id,
},
});
}

View File

@ -4010,9 +4010,9 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes@npm:^1.3.1":
version: 1.3.1
resolution: "@grafana/scenes@npm:1.3.1"
"@grafana/scenes@npm:^1.3.3":
version: 1.3.3
resolution: "@grafana/scenes@npm:1.3.3"
dependencies:
"@grafana/e2e-selectors": 10.0.2
react-grid-layout: 1.3.4
@ -4024,7 +4024,7 @@ __metadata:
"@grafana/runtime": 10.0.3
"@grafana/schema": 10.0.3
"@grafana/ui": 10.0.3
checksum: fe348e4eaaa3604d0d1ec745b14ae1c0752ce1aa481e5f05a10cdf9392448386e600c2a94f2ae23af83f59daf34cd11fb7a336da863b8624b98c8d3c2732b3c3
checksum: 3c630aaeca7bc240b1db1a6cc7856808d940c354a558e6f1c5c0fac7268c5f57e0b6f8f55a1d9d1dbfbb02011f1beecc5fe1aae1209bc4430599e2660fd76f6d
languageName: node
linkType: hard
@ -19701,7 +19701,7 @@ __metadata:
"@grafana/lezer-traceql": 0.0.6
"@grafana/monaco-logql": ^0.0.7
"@grafana/runtime": "workspace:*"
"@grafana/scenes": ^1.3.1
"@grafana/scenes": ^1.3.3
"@grafana/schema": "workspace:*"
"@grafana/tsconfig": ^1.3.0-rc1
"@grafana/ui": "workspace:*"