diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 95da187b1e5..180cc985669 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -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: { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 8cc188b0909..50306a06aa0 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -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: { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index f1acc3f273b..79d77c4de74 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -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"` } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index 302f539a9ec..4e55caae3c6 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -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, diff --git a/conf/provisioning/sample/dashboard-v1-annotations.json b/conf/provisioning/sample/dashboard-v1-annotations.json deleted file mode 100644 index 7e90452a2a5..00000000000 --- a/conf/provisioning/sample/dashboard-v1-annotations.json +++ /dev/null @@ -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 - } -} diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 61b29e2ee2e..0075a197ffa 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -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; } diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index f59c4667769..a5262d3e078 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -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" diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index 4157352dd3a..ab99200417d 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -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', () => { diff --git a/public/app/features/dashboard-scene/serialization/annotations.test.ts b/public/app/features/dashboard-scene/serialization/annotations.test.ts deleted file mode 100644 index f054c7a22a3..00000000000 --- a/public/app/features/dashboard-scene/serialization/annotations.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/public/app/features/dashboard-scene/serialization/annotations.ts b/public/app/features/dashboard-scene/serialization/annotations.ts deleted file mode 100644 index 7fba058df19..00000000000 --- a/public/app/features/dashboard-scene/serialization/annotations.ts +++ /dev/null @@ -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 -): 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; -} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 07f1359bc7a..c78cbd4c480 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -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', + }); }); }); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 12f14fee511..f0ed00957f2 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -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 { - 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), diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index 94c7aae659a..ad6137da1e5 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -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'); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 0dae10574b6..a7682c4e559 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -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; } diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index 51a0cb00f36..5111fc74e5c 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -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); } diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index aba26fbb7e4..0fe72aaf365 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -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 {} 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 {