Dashboards: Schema V2 - Rename Annotations 'options' to 'legacyOptions' and Fix stale props (#105266)
* Rename Annotations 'Options' to 'legacyOptions' * use new legacyOptions in StandardAnnotationQueryEditor * use new legacyOptions in transformSaveModelSchemaToScene * use legacyOptions in transformSceneToSaveModelSchemaV2 * Fix bug with ds query editors not taking the latest state * update snapshot
This commit is contained in:
@@ -393,7 +393,7 @@ AnnotationQuerySpec: {
|
||||
name: string
|
||||
builtIn?: bool | *false
|
||||
filter?: AnnotationPanelFilter
|
||||
options?: [string]: _ //Catch-all field for datasource-specific properties
|
||||
legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties
|
||||
}
|
||||
|
||||
AnnotationQueryKind: {
|
||||
|
||||
@@ -294,6 +294,8 @@ var _ resource.ListObject = &DashboardList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
|
||||
|
||||
// DeepCopy creates a full deep copy of DashboardStatus
|
||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||
cpy := &DashboardStatus{}
|
||||
|
||||
@@ -294,6 +294,8 @@ var _ resource.ListObject = &DashboardList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
|
||||
|
||||
// DeepCopy creates a full deep copy of DashboardStatus
|
||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||
cpy := &DashboardStatus{}
|
||||
|
||||
@@ -397,7 +397,7 @@ AnnotationQuerySpec: {
|
||||
name: string
|
||||
builtIn?: bool | *false
|
||||
filter?: AnnotationPanelFilter
|
||||
options?: [string]: _ //Catch-all field for datasource-specific properties
|
||||
legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties
|
||||
}
|
||||
|
||||
AnnotationQueryKind: {
|
||||
|
||||
@@ -33,7 +33,7 @@ type DashboardAnnotationQuerySpec struct {
|
||||
BuiltIn *bool `json:"builtIn,omitempty"`
|
||||
Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"`
|
||||
// Catch-all field for datasource-specific properties
|
||||
Options map[string]interface{} `json:"options,omitempty"`
|
||||
LegacyOptions map[string]interface{} `json:"legacyOptions,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardAnnotationQuerySpec creates a new DashboardAnnotationQuerySpec object.
|
||||
|
||||
@@ -647,7 +647,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref common.
|
||||
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter"),
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"legacyOptions": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Catch-all field for datasource-specific properties",
|
||||
Type: []string{"object"},
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
)
|
||||
|
||||
var ()
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
AppName: "dashboard",
|
||||
Group: "dashboard.grafana.app",
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
)
|
||||
|
||||
var ()
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
AppName: "folder",
|
||||
Group: "folder.grafana.app",
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface AnnotationQuerySpec {
|
||||
builtIn?: boolean;
|
||||
filter?: AnnotationPanelFilter;
|
||||
// Catch-all field for datasource-specific properties
|
||||
options?: Record<string, any>;
|
||||
legacyOptions?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultAnnotationQuerySpec = (): AnnotationQuerySpec => ({
|
||||
|
||||
@@ -1277,17 +1277,17 @@
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"options": {
|
||||
"legacyOptions": {
|
||||
"description": "Catch-all field for datasource-specific properties",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"query": {
|
||||
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind"
|
||||
}
|
||||
|
||||
@@ -68,14 +68,14 @@ describe('StandardAnnotationQueryEditor', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('v2 dashboard - should preserve options field when changing target', () => {
|
||||
// Setup with annotation that has options
|
||||
it('v2 dashboard - should preserve legacyOptions field when changing target', () => {
|
||||
// Setup with annotation that has legacyOptions
|
||||
const mockOnChange = jest.fn();
|
||||
const { props } = setup({
|
||||
annotation: {
|
||||
name: 'annotationWithOptions',
|
||||
name: 'annotationWithLegacyOptions',
|
||||
target: { refId: 'refId1' },
|
||||
options: {
|
||||
legacyOptions: {
|
||||
expr: 'rate(http_requests_total[5m])',
|
||||
queryType: 'range',
|
||||
},
|
||||
@@ -98,11 +98,11 @@ describe('StandardAnnotationQueryEditor', () => {
|
||||
// Simulate changing the target
|
||||
componentInstance.onChange({ refId: 'refId2', newField: 'value' });
|
||||
|
||||
// Check that options are preserved
|
||||
// Check that legacyOptions are preserved
|
||||
expect(mockOnChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
target: { refId: 'refId2', newField: 'value' },
|
||||
options: {
|
||||
legacyOptions: {
|
||||
expr: 'rate(http_requests_total[5m])',
|
||||
queryType: 'range',
|
||||
},
|
||||
@@ -110,13 +110,13 @@ describe('StandardAnnotationQueryEditor', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve options field when using onAnnotationChange', () => {
|
||||
// Setup with annotation that has options
|
||||
it('should preserve legacyOptions field when using onAnnotationChange', () => {
|
||||
// Setup with annotation that has legacyOptions
|
||||
const mockOnChange = jest.fn();
|
||||
const { props } = setup({
|
||||
annotation: {
|
||||
name: 'annotationWithOptions',
|
||||
options: {
|
||||
name: 'annotationWithLegacyOptions',
|
||||
legacyOptions: {
|
||||
expr: 'rate(http_requests_total[5m])',
|
||||
queryType: 'range',
|
||||
},
|
||||
@@ -142,12 +142,12 @@ describe('StandardAnnotationQueryEditor', () => {
|
||||
iconColor: 'red',
|
||||
});
|
||||
|
||||
// Check that options are preserved
|
||||
// Check that legacyOptions are preserved
|
||||
expect(mockOnChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'newName',
|
||||
iconColor: 'red',
|
||||
options: {
|
||||
legacyOptions: {
|
||||
expr: 'rate(http_requests_total[5m])',
|
||||
queryType: 'range',
|
||||
},
|
||||
@@ -187,17 +187,17 @@ describe('StandardAnnotationQueryEditor', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should propagate options to root level for v2 dashboards', () => {
|
||||
it('should propagate legacyOptions to root level for v2 dashboards', () => {
|
||||
const { props } = setup({
|
||||
annotation: {
|
||||
name: 'v2annotationWithOptions',
|
||||
name: 'v2annotationWithLegacyOptions',
|
||||
query: {
|
||||
kind: 'prometheus',
|
||||
spec: {
|
||||
refId: 'A',
|
||||
},
|
||||
},
|
||||
options: {
|
||||
legacyOptions: {
|
||||
expr: 'rate(http_requests_total[5m])',
|
||||
legendFormat: '{{method}} {{endpoint}}',
|
||||
},
|
||||
@@ -210,13 +210,13 @@ describe('StandardAnnotationQueryEditor', () => {
|
||||
} as unknown as DataSourceApi,
|
||||
});
|
||||
|
||||
// Check that options are propagated to root level for the editor
|
||||
// Check that legacyOptions are propagated to root level for the editor
|
||||
expect(props.datasource?.annotations?.QueryEditor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
annotation: expect.objectContaining({
|
||||
name: 'v2annotationWithOptions',
|
||||
name: 'v2annotationWithLegacyOptions',
|
||||
query: expect.anything(),
|
||||
options: expect.anything(),
|
||||
legacyOptions: expect.anything(),
|
||||
expr: 'rate(http_requests_total[5m])',
|
||||
legendFormat: '{{method}} {{endpoint}}',
|
||||
}),
|
||||
@@ -235,7 +235,7 @@ describe('StandardAnnotationQueryEditor', () => {
|
||||
type: 'prometheus',
|
||||
uid: 'abc123',
|
||||
},
|
||||
// v1 dashboards don't have options field
|
||||
// v1 dashboards don't have legacyOptions field
|
||||
enable: true,
|
||||
iconColor: 'red',
|
||||
hide: false,
|
||||
|
||||
@@ -101,11 +101,27 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
|
||||
};
|
||||
|
||||
onQueryChange = (target: DataQuery) => {
|
||||
// if dealing with v2 dashboards
|
||||
if (this.props.annotation.query && this.props.annotation.query.spec) {
|
||||
target = {
|
||||
...this.props.annotation.query.spec,
|
||||
...target,
|
||||
};
|
||||
}
|
||||
//target property is what ds query editor are using, but for v2 we also need to keep query in sync
|
||||
this.props.onChange({
|
||||
...this.props.annotation,
|
||||
// the query editor uses target, but the annotation in v2 uses query
|
||||
// therefore we need to keep the target and query in sync
|
||||
target,
|
||||
// Keep options from the original annotation if they exist
|
||||
...(this.props.annotation.options ? { options: this.props.annotation.options } : {}),
|
||||
...(this.props.annotation.query && {
|
||||
query: {
|
||||
kind: this.props.annotation.query.kind,
|
||||
spec: { ...target },
|
||||
},
|
||||
}),
|
||||
// Keep legacyOptions from the original annotation if they exist
|
||||
...(this.props.annotation.legacyOptions ? { legacyOptions: this.props.annotation.legacyOptions } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -209,11 +225,11 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
|
||||
}
|
||||
|
||||
onAnnotationChange = (annotation: AnnotationQuery) => {
|
||||
// Also preserve any options field that might exist when migrating from V2 to V1
|
||||
// Also preserve any legacyOptions field that might exist when migrating from V2 to V1
|
||||
this.props.onChange({
|
||||
...annotation,
|
||||
// Keep options from the original annotation if they exist
|
||||
...(this.props.annotation.options ? { options: this.props.annotation.options } : {}),
|
||||
// Keep legacyOptions from the original annotation if they exist
|
||||
...(this.props.annotation.legacyOptions ? { legacyOptions: this.props.annotation.legacyOptions } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -251,10 +267,9 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
|
||||
// Create annotation object that respects annotations API
|
||||
let editorAnnotation = annotation;
|
||||
|
||||
// For v2 dashboards: propagate options to root level for datasource compatibility
|
||||
if (annotation.query && annotation.options) {
|
||||
editorAnnotation = { ...annotation };
|
||||
Object.assign(editorAnnotation, annotation.options);
|
||||
// For v2 dashboards: propagate legacyOptions to root level for datasource compatibility
|
||||
if (annotation.query && annotation.legacyOptions) {
|
||||
editorAnnotation = { ...annotation.legacyOptions, ...annotation };
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+7
-7
@@ -679,7 +679,7 @@ describe('transformSaveModelSchemaV2ToScene', () => {
|
||||
});
|
||||
|
||||
describe('annotations', () => {
|
||||
it('should transform annotation with options field', () => {
|
||||
it('should transform annotation with legacyOptions field', () => {
|
||||
// Create a dashboard with an annotation that has options
|
||||
const dashboardWithAnnotationOptions: DashboardWithAccessInfo<DashboardV2Spec> = {
|
||||
kind: 'DashboardWithAccessInfo',
|
||||
@@ -721,7 +721,7 @@ describe('transformSaveModelSchemaV2ToScene', () => {
|
||||
{
|
||||
kind: 'AnnotationQuery',
|
||||
spec: {
|
||||
name: 'Annotation with options',
|
||||
name: 'Annotation with legacy options',
|
||||
builtIn: false,
|
||||
enable: true,
|
||||
hide: false,
|
||||
@@ -730,7 +730,7 @@ describe('transformSaveModelSchemaV2ToScene', () => {
|
||||
type: 'prometheus',
|
||||
uid: 'abc123',
|
||||
},
|
||||
options: {
|
||||
legacyOptions: {
|
||||
expr: 'rate(http_requests_total[5m])',
|
||||
queryType: 'range',
|
||||
legendFormat: '{{method}} {{endpoint}}',
|
||||
@@ -773,9 +773,9 @@ describe('transformSaveModelSchemaV2ToScene', () => {
|
||||
|
||||
const annotationLayer = dataLayerSet.state.annotationLayers[1] as DashboardAnnotationsDataLayer;
|
||||
|
||||
// Verify that the options have been merged into the query object
|
||||
// Verify that the legacyOptions have been merged into the query object
|
||||
expect(annotationLayer.state.query).toMatchObject({
|
||||
name: 'Annotation with options',
|
||||
name: 'Annotation with legacy options',
|
||||
expr: 'rate(http_requests_total[5m])',
|
||||
queryType: 'range',
|
||||
legendFormat: '{{method}} {{endpoint}}',
|
||||
@@ -783,8 +783,8 @@ describe('transformSaveModelSchemaV2ToScene', () => {
|
||||
step: '1m',
|
||||
});
|
||||
|
||||
// Verify the original options object is also preserved
|
||||
expect(annotationLayer.state.query.options).toMatchObject({
|
||||
// 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}}',
|
||||
|
||||
+3
-3
@@ -98,12 +98,12 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
||||
|
||||
const annotationLayers = dashboard.annotations.map((annotation) => {
|
||||
let annoQuerySpec = annotation.spec;
|
||||
// some annotations will contain in the options properties that need to be
|
||||
// some annotations will contain in the legacyOptions properties that need to be
|
||||
// added to the root level annotation spec
|
||||
if (annoQuerySpec?.options) {
|
||||
if (annoQuerySpec?.legacyOptions) {
|
||||
annoQuerySpec = {
|
||||
...annoQuerySpec,
|
||||
...annoQuerySpec.options,
|
||||
...annoQuerySpec.legacyOptions,
|
||||
};
|
||||
}
|
||||
return new DashboardAnnotationsDataLayer({
|
||||
|
||||
+5
-5
@@ -576,10 +576,10 @@ describe('transformSceneToSaveModelSchemaV2', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should test annotation with options field', () => {
|
||||
it('should test annotation with legacyOptions field', () => {
|
||||
// Create a scene with an annotation layer that has options
|
||||
const annotationWithOptions = new DashboardAnnotationsDataLayer({
|
||||
key: 'layerWithOptions',
|
||||
key: 'layerWithLegacyOptions',
|
||||
query: {
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
@@ -588,7 +588,7 @@ describe('transformSceneToSaveModelSchemaV2', () => {
|
||||
name: 'annotation-with-options',
|
||||
enable: true,
|
||||
iconColor: 'red',
|
||||
options: {
|
||||
legacyOptions: {
|
||||
expr: 'rate(http_requests_total[5m])',
|
||||
queryType: 'range',
|
||||
legendFormat: '{{method}} {{endpoint}}',
|
||||
@@ -617,8 +617,8 @@ describe('transformSceneToSaveModelSchemaV2', () => {
|
||||
|
||||
// Verify the annotation options are properly serialized
|
||||
expect(result.annotations.length).toBe(1);
|
||||
expect(result.annotations[0].spec.options).toBeDefined();
|
||||
expect(result.annotations[0].spec.options).toEqual({
|
||||
expect(result.annotations[0].spec.legacyOptions).toBeDefined();
|
||||
expect(result.annotations[0].spec.legacyOptions).toEqual({
|
||||
expr: 'rate(http_requests_total[5m])',
|
||||
queryType: 'range',
|
||||
legendFormat: '{{method}} {{endpoint}}',
|
||||
|
||||
+6
-5
@@ -420,7 +420,9 @@ function getAnnotations(state: DashboardSceneState, dsReferencesMapping?: DSRefe
|
||||
};
|
||||
|
||||
// Transform v1 dashboard (using target) to v2 structure
|
||||
if (layer.state.query.target) {
|
||||
// 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 = {
|
||||
@@ -462,13 +464,12 @@ function getAnnotations(state: DashboardSceneState, dsReferencesMapping?: DSRefe
|
||||
'query'
|
||||
);
|
||||
|
||||
// Store extra properties in the options field instead of directly in the spec
|
||||
// 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 { options, ...restProps } = otherProps;
|
||||
|
||||
const { legacyOptions, ...restProps } = otherProps;
|
||||
// Merge options with the rest of the properties
|
||||
result.spec.options = { ...options, ...restProps };
|
||||
result.spec.legacyOptions = { ...legacyOptions, ...restProps };
|
||||
}
|
||||
|
||||
// If filter is an empty array, don't save it
|
||||
|
||||
Reference in New Issue
Block a user