mirror of https://github.com/grafana/grafana.git
Revert "Schema V2: Simplify annotations v1<->v2 conversions" (#107984)
Revert "Schema V2: Simplify annotations v1<->v2 conversions (#107390)"
This reverts commit d5a1781fb6
.
This commit is contained in:
parent
15e59a0ca7
commit
2b8c5bea1a
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
// If filter is an empty array, don't save it
|
||||
if (annotationQuery.filter?.ids?.length) {
|
||||
result.spec.filter = annotationQuery.filter;
|
||||
}
|
||||
|
||||
// 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 (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…
Reference in New Issue