Revert "Schema V2: Simplify annotations v1<->v2 conversions" (#107984)

Revert "Schema V2: Simplify annotations v1<->v2 conversions (#107390)"

This reverts commit d5a1781fb6.
pull/107986/head
Stephanie Hingtgen 1 week ago committed by GitHub
parent 15e59a0ca7
commit 2b8c5bea1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      apps/dashboard/kinds/v2alpha1/dashboard_spec.cue
  2. 2
      apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue
  3. 2
      apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go
  4. 2
      apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go
  5. 257
      conf/provisioning/sample/dashboard-v1-annotations.json
  6. 2
      packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts
  7. 2
      pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json
  8. 224
      public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts
  9. 226
      public/app/features/dashboard-scene/serialization/annotations.test.ts
  10. 118
      public/app/features/dashboard-scene/serialization/annotations.ts
  11. 12
      public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts
  12. 38
      public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
  13. 24
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts
  14. 123
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts
  15. 9
      public/app/features/dashboard/api/ResponseTransformers.test.ts
  16. 23
      public/app/features/dashboard/api/ResponseTransformers.ts

@ -394,7 +394,7 @@ AnnotationQuerySpec: {
name: string
builtIn?: bool | *false
filter?: AnnotationPanelFilter
legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling.
legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties
}
AnnotationQueryKind: {

@ -398,7 +398,7 @@ AnnotationQuerySpec: {
name: string
builtIn?: bool | *false
filter?: AnnotationPanelFilter
legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling.
legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties
}
AnnotationQueryKind: {

@ -30,7 +30,7 @@ type DashboardAnnotationQuerySpec struct {
Name string `json:"name"`
BuiltIn *bool `json:"builtIn,omitempty"`
Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"`
// Catch-all field for datasource-specific properties. Should not be available in as code tooling.
// Catch-all field for datasource-specific properties
LegacyOptions map[string]interface{} `json:"legacyOptions,omitempty"`
}

@ -643,7 +643,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref common.
},
"legacyOptions": {
SchemaProps: spec.SchemaProps{
Description: "Catch-all field for datasource-specific properties. Should not be available in as code tooling.",
Description: "Catch-all field for datasource-specific properties",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,

@ -1,257 +0,0 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v1beta1",
"metadata": {
"name": "test-v1-annotations",
"annotations": {
"hello": "world"
},
"labels": {
"region": "west"
}
},
"spec": {
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": false,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
},
{
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "PD8C576611E62080A"
},
"enable": true,
"hide": false,
"iconColor": "blue",
"name": "testdata-annos",
"target": {
"lines": 10,
"refId": "Anno",
"scenarioId": "annotations"
}
},
{
"enable": true,
"hide": false,
"iconColor": "blue",
"name": "no-ds-testdata-annos",
"target": {
"lines": 10,
"refId": "Anno",
"scenarioId": "annotations"
}
},
{
"datasource": {
"type": "prometheus",
"uid": "gdev-prometheus"
},
"enable": true,
"hide": false,
"iconColor": "yellow",
"name": "prom-annos",
"target": {
"expr": "{action=\"add_client\"}",
"interval": "",
"lines": 10,
"refId": "Anno",
"scenarioId": "annotations"
}
},
{
"enable": true,
"hide": false,
"iconColor": "yellow",
"name": "no-ds-prom-annos",
"target": {
"expr": "{action=\"add_client\"}",
"interval": "",
"lines": 10,
"refId": "Anno",
"scenarioId": "annotations"
}
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "PBBCEC2D313BC06C3"
},
"enable": true,
"hide": false,
"iconColor": "red",
"name": "postgress-annos",
"target": {
"editorMode": "builder",
"format": "table",
"lines": 10,
"rawSql": "",
"refId": "Anno",
"scenarioId": "annotations",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
},
{
"datasource": {
"type": "elasticsearch",
"uid": "gdev-elasticsearch"
},
"enable": true,
"hide": false,
"iconColor": "red",
"name": "elastic - annos",
"tagsField": "asd",
"target": {
"lines": 10,
"query": "test query",
"refId": "Anno",
"scenarioId": "annotations"
},
"textField": "asd",
"timeEndField": "asdas",
"timeField": "asd"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"refId": "A"
}
],
"title": "New panel",
"type": "timeseries"
}
],
"preload": false,
"schemaVersion": 41,
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Test: V1 dashboard with annotations",
"version": 8
}
}

@ -18,7 +18,7 @@ export interface AnnotationQuerySpec {
name: string;
builtIn?: boolean;
filter?: AnnotationPanelFilter;
// Catch-all field for datasource-specific properties. Should not be available in as code tooling.
// Catch-all field for datasource-specific properties
legacyOptions?: Record<string, any>;
}

@ -1276,7 +1276,7 @@
"default": ""
},
"legacyOptions": {
"description": "Catch-all field for datasource-specific properties. Should not be available in as code tooling.",
"description": "Catch-all field for datasource-specific properties",
"type": "object",
"additionalProperties": {
"type": "object"

@ -14,9 +14,7 @@ import {
defaultPanelSpec,
defaultTimeSettingsSpec,
GridLayoutKind,
PanelKind,
PanelSpec,
QueryVariableKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
import { AnnoKeyDashboardSnapshotOriginalUrl } from 'app/features/apiserver/types';
@ -50,13 +48,11 @@ jest.mock('@grafana/runtime', () => ({
name: 'Grafana',
meta: { id: 'grafana' },
type: 'datasource',
uid: 'grafana',
},
prometheus: {
name: 'prometheus',
meta: { id: 'prometheus' },
type: 'datasource',
uid: 'prometheus-uid',
},
},
},
@ -894,226 +890,6 @@ describe('DashboardSceneSerializer', () => {
},
});
});
describe('data source references persistence', () => {
it('should not fill data source references for annotations when input did not contain it', () => {
const dashboard = setupV2({
annotations: [
{
kind: 'AnnotationQuery',
spec: {
builtIn: false,
enable: true,
hide: false,
iconColor: 'blue',
name: 'prom-annotations',
query: {
group: 'prometheus',
kind: 'DataQuery',
spec: {
refId: 'Anno',
},
version: 'v0',
},
},
},
],
});
const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions);
// referencing index 1 as transformation adds built in annotation query
expect(saveAsModel.annotations[1].spec.query.datasource).toBeUndefined();
});
it('should fill data source references for annotations when input did contain it', () => {
const dashboard = setupV2({
annotations: [
{
kind: 'AnnotationQuery',
spec: {
builtIn: false,
enable: true,
hide: false,
iconColor: 'blue',
name: 'prom-annotations',
query: {
group: 'prometheus',
kind: 'DataQuery',
datasource: {
name: 'prometheus-uid',
},
spec: {
refId: 'Anno',
},
version: 'v0',
},
},
},
],
});
const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions);
// referencing index 1 as transformation adds built in annotation query
expect(saveAsModel.annotations[1].spec.query.datasource).toEqual({
name: 'prometheus-uid',
});
});
it('should not fill data source references for panel queries when input did not contain it', () => {
const dashboard = setupV2({
elements: {
'panel-1': {
kind: 'Panel',
spec: {
...defaultPanelSpec(),
data: {
kind: 'QueryGroup',
spec: {
transformations: [],
queryOptions: {},
queries: [
{
kind: 'PanelQuery',
spec: {
refId: 'A',
hidden: false,
query: {
kind: 'DataQuery',
group: 'prometheus',
version: 'v0',
spec: {},
},
},
},
],
},
},
},
},
},
});
const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions);
expect(
(saveAsModel.elements['panel-1'] as PanelKind).spec.data.spec.queries[0].spec.query.datasource
).toBeUndefined();
});
it('should fill data source references for panel queries when input did contain it', () => {
const dashboard = setupV2({
elements: {
'panel-1': {
kind: 'Panel',
spec: {
...defaultPanelSpec(),
data: {
kind: 'QueryGroup',
spec: {
transformations: [],
queryOptions: {},
queries: [
{
kind: 'PanelQuery',
spec: {
refId: 'A',
hidden: false,
query: {
kind: 'DataQuery',
group: 'prometheus',
version: 'v0',
datasource: {
name: 'prometheus-uid',
},
spec: {},
},
},
},
],
},
},
},
},
},
});
const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions);
expect(
(saveAsModel.elements['panel-1'] as PanelKind).spec.data.spec.queries[0].spec.query.datasource
).toEqual({
name: 'prometheus-uid',
});
});
it('should not fill data source references for query variables when input did contain it', () => {
const queryVariable: QueryVariableKind = {
kind: 'QueryVariable',
spec: {
name: 'app',
current: {
text: 'app1',
value: 'app1',
},
hide: 'dontHide',
includeAll: false,
label: 'Query Variable',
skipUrlSync: false,
regex: '',
definition: '',
options: [],
refresh: 'never',
sort: 'alphabeticalAsc',
multi: false,
allowCustomValue: true,
query: {
kind: 'DataQuery',
group: 'prometheus',
version: 'v0',
spec: {},
},
},
};
const dashboard = setupV2({
variables: [queryVariable],
});
const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions);
expect((saveAsModel.variables[0] as QueryVariableKind).spec.query.datasource).toBeUndefined();
});
it('should fill data source references for query variables when input did contain it', () => {
const queryVariable: QueryVariableKind = {
kind: 'QueryVariable',
spec: {
name: 'app',
current: {
text: 'app1',
value: 'app1',
},
hide: 'dontHide',
includeAll: false,
label: 'Query Variable',
skipUrlSync: false,
regex: '',
definition: '',
options: [],
refresh: 'never',
sort: 'alphabeticalAsc',
multi: false,
allowCustomValue: true,
query: {
kind: 'DataQuery',
group: 'prometheus',
version: 'v0',
datasource: {
name: 'prometheus-uid',
},
spec: {},
},
},
};
const dashboard = setupV2({
variables: [queryVariable],
});
const saveAsModel = serializer.getSaveAsModel(dashboard, baseOptions);
expect((saveAsModel.variables[0] as QueryVariableKind).spec.query.datasource).toEqual({
name: 'prometheus-uid',
});
});
});
});
describe('panel mapping methods', () => {

@ -1,226 +0,0 @@
import { AnnotationQuery } from '@grafana/data';
import { AnnotationQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { transformV1ToV2AnnotationQuery, transformV2ToV1AnnotationQuery } from './annotations';
describe('V1<->V2 annotation convertions', () => {
test('given grafana-built in annotations', () => {
// test case
const annotationDefinition: AnnotationQuery = {
builtIn: 1,
datasource: {
type: 'grafana',
uid: 'grafana',
},
enable: true,
hide: false,
iconColor: 'yellow',
name: 'Annotations \u0026 Alerts',
target: {
// @ts-expect-error
limit: 100,
matchAny: false,
tags: [],
type: 'dashboard',
},
type: 'dashboard',
};
const expectedV2: AnnotationQueryKind = {
kind: 'AnnotationQuery',
spec: {
builtIn: true,
enable: true,
hide: false,
iconColor: 'yellow',
name: 'Annotations \u0026 Alerts',
query: {
kind: 'DataQuery',
group: 'grafana',
version: 'v0',
datasource: {
name: 'grafana',
},
spec: {
limit: 100,
matchAny: false,
tags: [],
type: 'dashboard',
},
},
},
};
const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'grafana', 'grafana');
expect(resultV2).toEqual(expectedV2);
const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2);
expect(resultV1).toEqual(annotationDefinition);
});
test('given annotations with datasource', () => {
const annotationDefinition = {
datasource: {
type: 'grafana-testdata-datasource',
uid: 'uid',
},
enable: true,
hide: false,
iconColor: 'blue',
name: 'testdata-annos',
target: {
lines: 10,
refId: 'Anno',
scenarioId: 'annotations',
},
};
const expectedV2: AnnotationQueryKind = {
kind: 'AnnotationQuery',
spec: {
enable: true,
hide: false,
iconColor: 'blue',
name: 'testdata-annos',
builtIn: false,
query: {
kind: 'DataQuery',
group: 'grafana-testdata-datasource',
version: 'v0',
datasource: {
name: 'uid',
},
spec: {
lines: 10,
refId: 'Anno',
scenarioId: 'annotations',
},
},
},
};
const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(
annotationDefinition,
'grafana-testdata-datasource',
'uid'
);
expect(resultV2).toEqual(expectedV2);
const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2);
expect(resultV1).toEqual(annotationDefinition);
});
test('given annotations with target', () => {
const annotationDefinition = {
datasource: {
type: 'prometheus',
uid: 'uid',
},
enable: true,
hide: false,
iconColor: 'yellow',
name: 'prom-annos',
target: {
expr: '{action="add_client"}',
interval: '',
lines: 10,
refId: 'Anno',
scenarioId: 'annotations',
},
};
const expectedV2: AnnotationQueryKind = {
kind: 'AnnotationQuery',
spec: {
enable: true,
hide: false,
iconColor: 'yellow',
name: 'prom-annos',
builtIn: false,
query: {
kind: 'DataQuery',
group: 'prometheus',
version: 'v0',
datasource: {
name: 'uid',
},
spec: {
expr: '{action="add_client"}',
interval: '',
lines: 10,
refId: 'Anno',
scenarioId: 'annotations',
},
},
},
};
const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'prometheus', 'uid');
expect(resultV2).toEqual(expectedV2);
const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2);
expect(resultV1).toEqual(annotationDefinition);
});
test('given annotations with non-schematised options / legacyOptions', () => {
const annotationDefinition = {
datasource: {
type: 'elasticsearch',
uid: 'uid',
},
enable: true,
hide: false,
iconColor: 'red',
name: 'elastic - annos',
tagsField: 'asd',
target: {
lines: 10,
query: 'test query',
refId: 'Anno',
scenarioId: 'annotations',
},
textField: 'asd',
timeEndField: 'asdas',
timeField: 'asd',
};
const expectedV2: AnnotationQueryKind = {
kind: 'AnnotationQuery',
spec: {
enable: true,
hide: false,
iconColor: 'red',
name: 'elastic - annos',
builtIn: false,
query: {
kind: 'DataQuery',
group: 'elasticsearch',
version: 'v0',
datasource: {
name: 'uid',
},
spec: {
lines: 10,
query: 'test query',
refId: 'Anno',
scenarioId: 'annotations',
},
},
legacyOptions: {
tagsField: 'asd',
textField: 'asd',
timeEndField: 'asdas',
timeField: 'asd',
},
},
};
const resultV2: AnnotationQueryKind = transformV1ToV2AnnotationQuery(annotationDefinition, 'elasticsearch', 'uid');
expect(resultV2).toEqual(expectedV2);
const resultV1: AnnotationQuery = transformV2ToV1AnnotationQuery(expectedV2);
expect(resultV1).toEqual(annotationDefinition);
});
});

@ -1,118 +0,0 @@
import { AnnotationQuery } from '@grafana/data';
import {
AnnotationQueryKind,
defaultDataQueryKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { getRuntimePanelDataSource } from './layoutSerializers/utils';
export function transformV1ToV2AnnotationQuery(
annotation: AnnotationQuery,
dsType: string,
dsUID?: string,
// Overrides are used to provide properties based on scene's annotations data layer object state
override?: Partial<AnnotationQuery>
): AnnotationQueryKind {
const group = annotation.builtIn ? 'grafana' : dsType;
const {
// known properties documented in v1 schema
enable,
hide,
iconColor,
name,
builtIn,
filter,
mappings,
datasource,
target,
snapshotData,
type,
// unknown properties that are still available for configuration through API
...legacyOptions
} = annotation;
const result: AnnotationQueryKind = {
kind: 'AnnotationQuery',
spec: {
builtIn: Boolean(annotation.builtIn),
name: annotation.name,
enable: Boolean(override?.enable) || Boolean(annotation.enable),
hide: Boolean(override?.hide) || Boolean(annotation.hide),
iconColor: annotation.iconColor,
query: {
kind: 'DataQuery',
version: defaultDataQueryKind().version,
group, // Annotation layer has a datasource type provided in runtime.
spec: target || {},
},
},
};
if (dsUID) {
result.spec.query.datasource = {
name: dsUID,
};
}
// if legacy options is not an empty object, add it to the result
if (Object.keys(legacyOptions).length > 0) {
result.spec.legacyOptions = legacyOptions;
}
if (annotation.filter?.ids?.length) {
result.spec.filter = annotation.filter;
}
// TODO: add mappings
return result;
}
export function transformV2ToV1AnnotationQuery(annotation: AnnotationQueryKind): AnnotationQuery {
let { query: dataQuery, ...annotationQuery } = annotation.spec;
// Mapping from AnnotationQueryKind to AnnotationQuery used by scenes.
let annoQuerySpec: AnnotationQuery = {
enable: annotation.spec.enable,
hide: annotation.spec.hide,
iconColor: annotation.spec.iconColor,
name: annotation.spec.name,
// TOOO: mappings
};
if (Object.keys(dataQuery.spec).length > 0) {
// @ts-expect-error DataQueryKind spec should be typed as DataQuery interface
annoQuerySpec.target = {
...dataQuery?.spec,
};
}
if (annotation.spec.builtIn) {
annoQuerySpec.type = 'dashboard';
annoQuerySpec.builtIn = 1;
}
if (annotation.spec.filter) {
annoQuerySpec.filter = annotation.spec.filter;
}
// some annotations will contain in the legacyOptions properties that need to be
// added to the root level AnnotationQuery
if (annotationQuery.legacyOptions) {
annoQuerySpec = {
...annoQuerySpec,
...annotationQuery.legacyOptions,
};
}
// get data source from annotation query
const datasource = getRuntimePanelDataSource(dataQuery);
annoQuerySpec.datasource = datasource;
return annoQuerySpec;
}

@ -781,8 +781,8 @@ describe('transformSaveModelSchemaV2ToScene', () => {
enable: true,
iconColor: 'rgba(0, 211, 255, 1)',
name: 'Annotations & Alerts',
filter: undefined,
hide: true,
type: 'dashboard',
});
const annotationLayer = dataLayerSet.state.annotationLayers[1] as DashboardAnnotationsDataLayer;
@ -794,6 +794,7 @@ describe('transformSaveModelSchemaV2ToScene', () => {
type: 'prometheus',
},
name: 'Annotation with legacy options',
builtIn: 0,
enable: true,
hide: false,
iconColor: 'purple',
@ -803,6 +804,15 @@ describe('transformSaveModelSchemaV2ToScene', () => {
useValueAsTime: true,
step: '1m',
});
// Verify the original legacyOptions object is also preserved
expect(annotationLayer.state.query.legacyOptions).toMatchObject({
expr: 'rate(http_requests_total[5m])',
queryType: 'range',
legendFormat: '{{method}} {{endpoint}}',
useValueAsTime: true,
step: '1m',
});
});
});
});

@ -1,5 +1,6 @@
import { uniqueId } from 'lodash';
import { AnnotationQuery } from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
import {
AdHocFiltersVariable,
@ -63,10 +64,9 @@ import { DashboardScene } from '../scene/DashboardScene';
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
import { getIntervalsFromQueryString } from '../utils/utils';
import { transformV2ToV1AnnotationQuery } from './annotations';
import { SnapshotVariable } from './custom-variables/SnapshotVariable';
import { layoutDeserializerRegistry } from './layoutSerializers/layoutSerializerRegistry';
import { getRuntimeVariableDataSource } from './layoutSerializers/utils';
import { getRuntimePanelDataSource, getRuntimeVariableDataSource } from './layoutSerializers/utils';
import { registerPanelInteractionsReporter } from './transformSaveModelToScene';
import {
transformCursorSyncV2ToV1,
@ -92,17 +92,47 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
const { spec: dashboard, metadata, apiVersion } = dto;
// annotations might not come with the builtIn Grafana annotation, we need to add it
const grafanaBuiltAnnotation = getGrafanaBuiltInAnnotationDataLayer(dashboard);
if (grafanaBuiltAnnotation) {
dashboard.annotations.unshift(grafanaBuiltAnnotation);
}
const annotationLayers = dashboard.annotations.map((annotation) => {
const annotationQuerySpec = transformV2ToV1AnnotationQuery(annotation);
let { query: dataQuery, ...annotationQuery } = annotation.spec;
// Mapping from AnnotationQueryKind to AnnotationQuery used by scenes.
let annoQuerySpec: AnnotationQuery = {
builtIn: annotation.spec.builtIn ? 1 : 0,
enable: annotation.spec.enable,
iconColor: annotation.spec.iconColor,
name: annotation.spec.name,
filter: annotation.spec.filter,
hide: annotation.spec.hide,
...dataQuery?.spec,
};
// some annotations will contain in the legacyOptions properties that need to be
// added to the root level annotation spec
if (annotationQuery.legacyOptions) {
annoQuerySpec = {
...annoQuerySpec,
...annotationQuery.legacyOptions,
legacyOptions: {
...annotationQuery.legacyOptions,
},
};
}
// get data source from annotation query
const datasource = getRuntimePanelDataSource(dataQuery);
const layerState = {
key: uniqueId('annotations-'),
query: annotationQuerySpec,
query: {
...annoQuerySpec,
datasource,
},
name: annotation.spec.name,
isEnabled: Boolean(annotation.spec.enable),
isHidden: Boolean(annotation.spec.hide),

@ -561,8 +561,16 @@ describe('transformSceneToSaveModelSchemaV2', () => {
name: 'annotation-with-options',
enable: true,
iconColor: 'red',
customProp1: true,
customProp2: 'test',
legacyOptions: {
expr: 'rate(http_requests_total[5m])',
queryType: 'range',
legendFormat: '{{method}} {{endpoint}}',
useValueAsTime: true,
},
// Some other properties that aren't in the annotation spec
// and should be moved to options
customProp1: 'value1',
customProp2: 'value2',
},
name: 'layerWithOptions',
isEnabled: true,
@ -584,11 +592,19 @@ describe('transformSceneToSaveModelSchemaV2', () => {
expect(result.annotations.length).toBe(1);
expect(result.annotations[0].spec.legacyOptions).toBeDefined();
expect(result.annotations[0].spec.legacyOptions).toEqual({
customProp1: true,
customProp2: 'test',
expr: 'rate(http_requests_total[5m])',
queryType: 'range',
legendFormat: '{{method}} {{endpoint}}',
useValueAsTime: true,
customProp1: 'value1',
customProp2: 'value2',
});
// Ensure these properties are not at the root level
expect(result).not.toHaveProperty('annotations[0].spec.expr');
expect(result).not.toHaveProperty('annotations[0].spec.queryType');
expect(result).not.toHaveProperty('annotations[0].spec.legendFormat');
expect(result).not.toHaveProperty('annotations[0].spec.useValueAsTime');
expect(result).not.toHaveProperty('annotations[0].spec.customProp1');
expect(result).not.toHaveProperty('annotations[0].spec.customProp2');
});

@ -52,7 +52,6 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
import { DSReferencesMapping } from './DashboardSceneSerializer';
import { transformV1ToV2AnnotationQuery } from './annotations';
import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables';
import { colorIdEnumToColorIdV2, transformCursorSynctoEnum } from './transformToV2TypesUtils';
@ -419,46 +418,114 @@ function getAnnotations(state: DashboardSceneState, dsReferencesMapping?: DSRefe
if (!(layer instanceof dataLayers.AnnotationsDataLayer)) {
continue;
}
const datasource = getElementDatasource(layer, layer.state.query, 'annotation', undefined, dsReferencesMapping);
let layerDs = layer.state.query.datasource;
const layerDs = layer.state.query.datasource;
if (!layerDs) {
// This can happen only if we are transforming a scene that was created
// from a v1 spec. In v1 annotation layer can contain no datasource ref, which is guaranteed
// for layers created for v2 schema. See transform transformSaveModelSchemaV2ToScene.ts.
// In this case we will resolve default data source
layerDs = getDefaultDataSourceRef();
console.error(
'Misconfigured AnnotationsDataLayer: Data source is required for annotations. Resolving default data source',
layer,
layerDs
);
throw new Error('Misconfigured AnnotationsDataLayer: Datasource is required for annotations');
}
const result = transformV1ToV2AnnotationQuery(layer.state.query, layerDs.type!, layerDs.uid!, {
enable: layer.state.isEnabled,
hide: layer.state.isHidden,
});
const result: AnnotationQueryKind = {
kind: 'AnnotationQuery',
spec: {
builtIn: Boolean(layer.state.query.builtIn),
name: layer.state.query.name,
enable: Boolean(layer.state.isEnabled),
hide: Boolean(layer.state.isHidden),
iconColor: layer.state.query.iconColor,
query: {
kind: 'DataQuery',
version: defaultDataQueryKind().version,
group: layerDs.type!, // Annotation layer has a datasource type provided in runtime.
spec: {},
},
},
};
const annotationQuery = layer.state.query;
if (datasource) {
result.spec.query!.datasource = {
name: datasource.uid,
};
}
// If filter is an empty array, don't save it
if (annotationQuery.filter?.ids?.length) {
result.spec.filter = annotationQuery.filter;
// Transform v1 dashboard (using target) to v2 structure
// adds extra condition to prioritize query over target
// if query is defined, use it
if (layer.state.query.target && !layer.state.query.query) {
// Handle built-in annotations
if (layer.state.query.builtIn) {
result.spec.query = {
kind: 'DataQuery',
version: defaultDataQueryKind().version,
group: 'grafana', // built-in annotations are always of type grafana
spec: {
...layer.state.query.target,
},
};
} else {
result.spec.query = {
kind: 'DataQuery',
version: defaultDataQueryKind().version,
group: datasource?.type!,
spec: {
...layer.state.query.target,
},
};
if (layer.state.query.datasource?.uid) {
result.spec.query.datasource = {
name: layer.state.query.datasource?.uid,
};
}
}
}
// For annotations without query.query defined (e.g., grafana annotations without tags)
else if (layer.state.query.query?.kind) {
result.spec.query = {
kind: 'DataQuery',
version: defaultDataQueryKind().version,
group: layer.state.query.query.group || getAnnotationQueryKind(layer.state.query),
datasource: layer.state.query.query.datasource,
spec: {
...layer.state.query.query.spec,
},
};
}
// Collect datasource-specific properties not in standard annotation spec
let otherProps = omit(
layer.state.query,
'type',
'target',
'builtIn',
'name',
'datasource',
'iconColor',
'enable',
'hide',
'filter',
'query'
);
// Store extra properties in the legacyOptions field instead of directly in the spec
if (Object.keys(otherProps).length > 0) {
// // Extract options property and get the rest of the properties
const { legacyOptions, ...restProps } = otherProps;
if (legacyOptions) {
// Merge options with the rest of the properties
result.spec.legacyOptions = { ...legacyOptions, ...restProps };
}
result.spec.query!.spec = {
...otherProps,
};
}
// Finally, if the datasource references mapping did not containt data source ref,
// this means that the original model that was fetched did not contain it. In such scenario we don't want to save
// the explicit data source reference, so lets remove it from the save model.
if (!datasource) {
delete result.spec.query.datasource;
// If filter is an empty array, don't save it
if (layer.state.query.filter?.ids?.length) {
result.spec.filter = layer.state.query.filter;
}
annotations.push(result);
}
return annotations;
}

@ -1016,14 +1016,15 @@ describe('ResponseTransformers', () => {
function validateAnnotation(v1: AnnotationQuery, v2: DashboardV2Spec['annotations'][0]) {
const { spec: v2Spec } = v2;
expect(v1.name).toBe(v2Spec.name);
expect(v1.datasource?.type).toBe(v2Spec.query.group);
expect(v1.datasource?.uid).toBe(v2Spec.query.datasource?.name);
expect(v1.datasource?.type).toBe(v2Spec.query?.spec.group);
expect(v1.datasource?.uid).toBe(v2Spec.query?.spec.datasource?.name);
expect(v1.enable).toBe(v2Spec.enable);
expect(v1.hide).toBe(v2Spec.hide);
expect(v1.iconColor).toBe(v2Spec.iconColor);
expect(v1.builtIn).toBe(v2Spec.builtIn ? 1 : undefined);
expect(v1.target).toEqual(v2Spec.query.spec);
expect(v1.builtIn).toBe(v2Spec.builtIn ? 1 : 0);
expect(v1.target).toBe(v2Spec.query?.spec);
expect(v1.filter).toEqual(v2Spec.filter);
}

@ -56,7 +56,6 @@ import {
DeprecatedInternalId,
ObjectMeta,
} from 'app/features/apiserver/types';
import { transformV2ToV1AnnotationQuery } from 'app/features/dashboard-scene/serialization/annotations';
import { GRID_ROW_HEIGHT } from 'app/features/dashboard-scene/serialization/const';
import { validateFiltersOrigin } from 'app/features/dashboard-scene/serialization/sceneVariablesSetToVariables';
import { TypedVariableModelV2 } from 'app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene';
@ -893,6 +892,25 @@ function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] {
return variables;
}
function getAnnotationsV1(annotations: DashboardV2Spec['annotations']): AnnotationQuery[] {
// @ts-expect-error - target v2 query is not compatible with v1 target
return annotations.map((a) => {
return {
name: a.spec.name,
datasource: {
type: a.spec.query?.spec.group,
uid: a.spec.query?.spec.datasource?.name,
},
enable: a.spec.enable,
hide: a.spec.hide,
iconColor: a.spec.iconColor,
builtIn: a.spec.builtIn ? 1 : 0,
target: a.spec.query?.spec,
filter: a.spec.filter,
};
});
}
interface LibraryPanelDTO extends Pick<Panel, 'libraryPanel' | 'id' | 'title' | 'gridPos' | 'type'> {}
function getPanelsV1(
@ -1140,8 +1158,7 @@ function transformToV1VariableTypes(variable: TypedVariableModelV2): VariableTyp
}
export function transformDashboardV2SpecToV1(spec: DashboardV2Spec, metadata: ObjectMeta): DashboardDataDTO {
const annotations = spec.annotations.map(transformV2ToV1AnnotationQuery);
const annotations = getAnnotationsV1(spec.annotations);
const variables = getVariablesV1(spec.variables);
const panels = getPanelsV1(spec.elements, spec.layout);
return {

Loading…
Cancel
Save