From 4f18ad30c9058f0d290f2bf5d1f46a3432a24320 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Mon, 26 May 2025 14:33:30 +0200 Subject: [PATCH] SchemaV2: Remove legacy rows from schema v2. (#105238) * save transparent setting * make sure we test both transparent and non transparent * no more legacy rows * schema changes * Add testing, fix first row offset * Remove legacy row from transform test * Remove panel that's not present in layout * Remove expects after removing the row, fix lint issues * Remove unused commit * update codegen * update openapi snapshot * Fix snapshot * add missing default prop * Fix repeating, fix first row, fix not flushing last row * Use correct repeater * fix lint, remove unused empty check * update codegen * update openapi test snapshot --- .../kinds/v2alpha1/dashboard_spec.cue | 15 +- .../dashboard/v2alpha1/dashboard_spec.cue | 15 +- .../dashboard/v2alpha1/dashboard_spec_gen.go | 114 +----- .../v2alpha1/zz_generated.openapi.go | 120 +----- ...enerated.openapi_violation_exceptions.list | 3 - .../src/schema/dashboard/v2_examples.ts | 78 ---- .../dashboard/v2alpha0/dashboard.schema.cue | 15 +- .../schema/dashboard/v2alpha0/types.gen.ts | 48 +-- .../dashboard/v2alpha1/types.spec.gen.ts | 48 +-- .../dashboard.grafana.app-v2alpha1.json | 81 +--- .../dashboard-scene/scene/export/utils.ts | 21 +- ...sformSceneToSaveModelSchemaV2.test.ts.snap | 65 --- .../DefaultGridLayoutSerializer.ts | 79 +--- .../testfiles/rows_after_free_panels.json | 374 ++++++++++++++++++ .../transformSaveModelSchemaV2ToScene.test.ts | 14 +- .../transformSaveModelToScene.test.ts | 66 ++++ .../transformSaveModelToScene.ts | 91 ++++- .../transformSceneToSaveModelSchemaV2.test.ts | 26 -- .../api/ResponseTransformers.test.ts | 369 +++++++++++------ .../dashboard/api/ResponseTransformers.ts | 128 +++--- 20 files changed, 914 insertions(+), 856 deletions(-) create mode 100644 public/app/features/dashboard-scene/serialization/testfiles/rows_after_free_panels.json diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 04273371f01..3928838bd33 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -518,21 +518,8 @@ GridLayoutItemKind: { spec: GridLayoutItemSpec } -GridLayoutRowKind: { - kind: "GridLayoutRow" - spec: GridLayoutRowSpec -} - -GridLayoutRowSpec: { - y: int - collapsed: bool - title: string - elements: [...GridLayoutItemKind] // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard. - repeat?: RowRepeatOptions -} - GridLayoutSpec: { - items: [...GridLayoutItemKind | GridLayoutRowKind] + items: [...GridLayoutItemKind] } GridLayoutKind: { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 38ec4bf46ce..a08f1f6eff6 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -522,21 +522,8 @@ GridLayoutItemKind: { spec: GridLayoutItemSpec } -GridLayoutRowKind: { - kind: "GridLayoutRow" - spec: GridLayoutRowSpec -} - -GridLayoutRowSpec: { - y: int - collapsed: bool - title: string - elements: [...GridLayoutItemKind] // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard. - repeat?: RowRepeatOptions -} - GridLayoutSpec: { - items: [...GridLayoutItemKind | GridLayoutRowKind] + items: [...GridLayoutItemKind] } GridLayoutKind: { 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 45db16e7490..616ec66456b 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -688,7 +688,7 @@ func NewDashboardGridLayoutKind() *DashboardGridLayoutKind { // +k8s:openapi-gen=true type DashboardGridLayoutSpec struct { - Items []DashboardGridLayoutItemKindOrGridLayoutRowKind `json:"items"` + Items []DashboardGridLayoutItemKind `json:"items"` } // NewDashboardGridLayoutSpec creates a new DashboardGridLayoutSpec object. @@ -758,46 +758,6 @@ func NewDashboardRepeatOptions() *DashboardRepeatOptions { // +k8s:openapi-gen=true const DashboardRepeatMode = "variable" -// +k8s:openapi-gen=true -type DashboardGridLayoutRowKind struct { - Kind string `json:"kind"` - Spec DashboardGridLayoutRowSpec `json:"spec"` -} - -// NewDashboardGridLayoutRowKind creates a new DashboardGridLayoutRowKind object. -func NewDashboardGridLayoutRowKind() *DashboardGridLayoutRowKind { - return &DashboardGridLayoutRowKind{ - Kind: "GridLayoutRow", - Spec: *NewDashboardGridLayoutRowSpec(), - } -} - -// +k8s:openapi-gen=true -type DashboardGridLayoutRowSpec struct { - Y int64 `json:"y"` - Collapsed bool `json:"collapsed"` - Title string `json:"title"` - // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard. - Elements []DashboardGridLayoutItemKind `json:"elements"` - Repeat *DashboardRowRepeatOptions `json:"repeat,omitempty"` -} - -// NewDashboardGridLayoutRowSpec creates a new DashboardGridLayoutRowSpec object. -func NewDashboardGridLayoutRowSpec() *DashboardGridLayoutRowSpec { - return &DashboardGridLayoutRowSpec{} -} - -// +k8s:openapi-gen=true -type DashboardRowRepeatOptions struct { - Mode string `json:"mode"` - Value string `json:"value"` -} - -// NewDashboardRowRepeatOptions creates a new DashboardRowRepeatOptions object. -func NewDashboardRowRepeatOptions() *DashboardRowRepeatOptions { - return &DashboardRowRepeatOptions{} -} - // +k8s:openapi-gen=true type DashboardRowsLayoutKind struct { Kind string `json:"kind"` @@ -954,6 +914,17 @@ func NewDashboardConditionalRenderingTimeRangeSizeSpec() *DashboardConditionalRe return &DashboardConditionalRenderingTimeRangeSizeSpec{} } +// +k8s:openapi-gen=true +type DashboardRowRepeatOptions struct { + Mode string `json:"mode"` + Value string `json:"value"` +} + +// NewDashboardRowRepeatOptions creates a new DashboardRowRepeatOptions object. +func NewDashboardRowRepeatOptions() *DashboardRowRepeatOptions { + return &DashboardRowRepeatOptions{} +} + // +k8s:openapi-gen=true type DashboardAutoGridLayoutKind struct { Kind string `json:"kind"` @@ -2027,67 +1998,6 @@ func (resource *DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap) Unmarsha return fmt.Errorf("could not unmarshal resource with `type = %v`", discriminator) } -// +k8s:openapi-gen=true -type DashboardGridLayoutItemKindOrGridLayoutRowKind struct { - GridLayoutItemKind *DashboardGridLayoutItemKind `json:"GridLayoutItemKind,omitempty"` - GridLayoutRowKind *DashboardGridLayoutRowKind `json:"GridLayoutRowKind,omitempty"` -} - -// NewDashboardGridLayoutItemKindOrGridLayoutRowKind creates a new DashboardGridLayoutItemKindOrGridLayoutRowKind object. -func NewDashboardGridLayoutItemKindOrGridLayoutRowKind() *DashboardGridLayoutItemKindOrGridLayoutRowKind { - return &DashboardGridLayoutItemKindOrGridLayoutRowKind{} -} - -// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardGridLayoutItemKindOrGridLayoutRowKind` as JSON. -func (resource DashboardGridLayoutItemKindOrGridLayoutRowKind) MarshalJSON() ([]byte, error) { - if resource.GridLayoutItemKind != nil { - return json.Marshal(resource.GridLayoutItemKind) - } - if resource.GridLayoutRowKind != nil { - return json.Marshal(resource.GridLayoutRowKind) - } - return []byte("null"), nil -} - -// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardGridLayoutItemKindOrGridLayoutRowKind` from JSON. -func (resource *DashboardGridLayoutItemKindOrGridLayoutRowKind) UnmarshalJSON(raw []byte) error { - if raw == nil { - return nil - } - - // FIXME: this is wasteful, we need to find a more efficient way to unmarshal this. - parsedAsMap := make(map[string]interface{}) - if err := json.Unmarshal(raw, &parsedAsMap); err != nil { - return err - } - - discriminator, found := parsedAsMap["kind"] - if !found { - return errors.New("discriminator field 'kind' not found in payload") - } - - switch discriminator { - case "GridLayoutItem": - var dashboardGridLayoutItemKind DashboardGridLayoutItemKind - if err := json.Unmarshal(raw, &dashboardGridLayoutItemKind); err != nil { - return err - } - - resource.GridLayoutItemKind = &dashboardGridLayoutItemKind - return nil - case "GridLayoutRow": - var dashboardGridLayoutRowKind DashboardGridLayoutRowKind - if err := json.Unmarshal(raw, &dashboardGridLayoutRowKind); err != nil { - return err - } - - resource.GridLayoutRowKind = &dashboardGridLayoutRowKind - return nil - } - - return fmt.Errorf("could not unmarshal resource with `kind = %v`", discriminator) -} - // +k8s:openapi-gen=true type DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind struct { GridLayoutKind *DashboardGridLayoutKind `json:"GridLayoutKind,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 20fd5a637cc..85b5a6f65cb 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -56,13 +56,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfig(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfigSource": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfigSource(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKindOrGridLayoutRowKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemKindOrGridLayoutRowKind(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemSpec(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKind(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutRowKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutRowKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutRowSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutRowSpec(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutSpec(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableKind(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableSpec(ref), @@ -2057,30 +2054,6 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemKind(ref common.R } } -func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemKindOrGridLayoutRowKind(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "GridLayoutItemKind": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind"), - }, - }, - "GridLayoutRowKind": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutRowKind"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutRowKind"}, - } -} - func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -2232,89 +2205,6 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrRowsLayoutKindO } } -func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutRowKind(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutRowSpec"), - }, - }, - }, - Required: []string{"kind", "spec"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutRowSpec"}, - } -} - -func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutRowSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "y": { - SchemaProps: spec.SchemaProps{ - Default: 0, - Type: []string{"integer"}, - Format: "int64", - }, - }, - "collapsed": { - SchemaProps: spec.SchemaProps{ - Default: false, - Type: []string{"boolean"}, - Format: "", - }, - }, - "title": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "elements": { - SchemaProps: spec.SchemaProps{ - Description: "Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind"), - }, - }, - }, - }, - }, - "repeat": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowRepeatOptions"), - }, - }, - }, - Required: []string{"y", "collapsed", "title", "elements"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowRepeatOptions"}, - } -} - func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -2327,7 +2217,8 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutSpec(ref common.Refer Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKindOrGridLayoutRowKind"), + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind"), }, }, }, @@ -2338,7 +2229,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutSpec(ref common.Refer }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKindOrGridLayoutRowKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind"}, } } @@ -2390,6 +2281,11 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableSpec(ref common. Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataSourceRef"), }, }, + "defaultValue": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVariableOption"), + }, + }, "current": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list index 889a4fcdfed..991f1b20450 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list @@ -12,7 +12,6 @@ API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/ API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardFieldConfig,Links API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardFieldConfig,Mappings API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardFieldConfigSource,Overrides -API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutRowSpec,Elements API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutSpec,Items API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGroupByVariableSpec,Options API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardIntervalVariableSpec,Options @@ -36,8 +35,6 @@ API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/ API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingDataKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingTimeRangeSizeKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutItemKindOrGridLayoutRowKind,GridLayoutItemKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutItemKindOrGridLayoutRowKind,GridLayoutRowKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,AutoGridLayoutKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,GridLayoutKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,RowsLayoutKind diff --git a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts index 446936890db..4bf078783cb 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts @@ -202,60 +202,6 @@ export const handyTestingSchema: Spec = { }, }, }, - 'panel-3': { - kind: 'Panel', - spec: { - data: { - kind: 'QueryGroup', - spec: { - queries: [ - { - kind: 'PanelQuery', - spec: { - refId: 'A', - datasource: { - type: 'prometheus', - uid: 'datasource1', - }, - query: { - kind: 'prometheus', - spec: { - expr: 'test-query', - }, - }, - hidden: false, - }, - }, - ], - queryOptions: { - timeFrom: '1h', - maxDataPoints: 100, - timeShift: '1h', - queryCachingTTL: 60, - interval: '1m', - cacheTimeout: '1m', - hideTimeOverride: false, - }, - transformations: [], - }, - }, - description: 'Test Description', - links: [], - title: 'Test Panel 3', - id: 3, - vizConfig: { - kind: 'timeseries', - spec: { - fieldConfig: { - defaults: {}, - overrides: [], - }, - options: {}, - pluginVersion: '7.0.0', - }, - }, - }, - }, }, layout: { kind: 'GridLayout', @@ -292,30 +238,6 @@ export const handyTestingSchema: Spec = { y: 2, }, }, - { - kind: 'GridLayoutRow', - spec: { - y: 20, - collapsed: false, - title: 'Row 1', - repeat: { value: 'customVar', mode: 'variable' }, - elements: [ - { - kind: 'GridLayoutItem', - spec: { - element: { - kind: 'ElementReference', - name: 'panel-3', - }, - height: 10, - width: 10, - x: 0, - y: 0, - }, - }, - ], - }, - }, ], }, }, diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 73faa5df734..3f1f40287cc 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -518,21 +518,8 @@ GridLayoutItemKind: { spec: GridLayoutItemSpec } -GridLayoutRowKind: { - kind: "GridLayoutRow" - spec: GridLayoutRowSpec -} - -GridLayoutRowSpec: { - y: int - collapsed: bool - title: string - elements: [...GridLayoutItemKind] // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard. - repeat?: RowRepeatOptions -} - GridLayoutSpec: { - items: [...GridLayoutItemKind | GridLayoutRowKind] + items: [...GridLayoutItemKind] } GridLayoutKind: { diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index a4d966c654a..d38a6693ac9 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -609,7 +609,7 @@ export const defaultGridLayoutKind = (): GridLayoutKind => ({ }); export interface GridLayoutSpec { - items: (GridLayoutItemKind | GridLayoutRowKind)[]; + items: GridLayoutItemKind[]; } export const defaultGridLayoutSpec = (): GridLayoutSpec => ({ @@ -669,42 +669,6 @@ export const defaultRepeatOptions = (): RepeatOptions => ({ // other repeat modes will be added in the future: label, frame export const RepeatMode = "variable"; -export interface GridLayoutRowKind { - kind: "GridLayoutRow"; - spec: GridLayoutRowSpec; -} - -export const defaultGridLayoutRowKind = (): GridLayoutRowKind => ({ - kind: "GridLayoutRow", - spec: defaultGridLayoutRowSpec(), -}); - -export interface GridLayoutRowSpec { - y: number; - collapsed: boolean; - title: string; - // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard. - elements: GridLayoutItemKind[]; - repeat?: RowRepeatOptions; -} - -export const defaultGridLayoutRowSpec = (): GridLayoutRowSpec => ({ - y: 0, - collapsed: false, - title: "", - elements: [], -}); - -export interface RowRepeatOptions { - mode: "variable"; - value: string; -} - -export const defaultRowRepeatOptions = (): RowRepeatOptions => ({ - mode: RepeatMode, - value: "", -}); - export interface RowsLayoutKind { kind: "RowsLayout"; spec: RowsLayoutSpec; @@ -747,6 +711,16 @@ export const defaultRowsLayoutRowSpec = (): RowsLayoutRowSpec => ({ layout: defaultGridLayoutKind(), }); +export interface RowRepeatOptions { + mode: "variable"; + value: string; +} + +export const defaultRowRepeatOptions = (): RowRepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + export interface ConditionalRenderingGroupKind { kind: "ConditionalRenderingGroup"; spec: ConditionalRenderingGroupSpec; 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 33e7f2ed0b8..5719f2da797 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 @@ -564,7 +564,7 @@ export const defaultGridLayoutKind = (): GridLayoutKind => ({ }); export interface GridLayoutSpec { - items: (GridLayoutItemKind | GridLayoutRowKind)[]; + items: GridLayoutItemKind[]; } export const defaultGridLayoutSpec = (): GridLayoutSpec => ({ @@ -624,42 +624,6 @@ export const defaultRepeatOptions = (): RepeatOptions => ({ // other repeat modes will be added in the future: label, frame export const RepeatMode = "variable"; -export interface GridLayoutRowKind { - kind: "GridLayoutRow"; - spec: GridLayoutRowSpec; -} - -export const defaultGridLayoutRowKind = (): GridLayoutRowKind => ({ - kind: "GridLayoutRow", - spec: defaultGridLayoutRowSpec(), -}); - -export interface GridLayoutRowSpec { - y: number; - collapsed: boolean; - title: string; - // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard. - elements: GridLayoutItemKind[]; - repeat?: RowRepeatOptions; -} - -export const defaultGridLayoutRowSpec = (): GridLayoutRowSpec => ({ - y: 0, - collapsed: false, - title: "", - elements: [], -}); - -export interface RowRepeatOptions { - mode: "variable"; - value: string; -} - -export const defaultRowRepeatOptions = (): RowRepeatOptions => ({ - mode: RepeatMode, - value: "", -}); - export interface RowsLayoutKind { kind: "RowsLayout"; spec: RowsLayoutSpec; @@ -782,6 +746,16 @@ export const defaultConditionalRenderingTimeRangeSizeSpec = (): ConditionalRende value: "", }); +export interface RowRepeatOptions { + mode: "variable"; + value: string; +} + +export const defaultRowRepeatOptions = (): RowRepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + export interface AutoGridLayoutKind { kind: "AutoGridLayout"; spec: AutoGridLayoutSpec; 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 de9b158d7c7..2f4dafbaa16 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -2230,17 +2230,6 @@ } } }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKindOrGridLayoutRowKind": { - "type": "object", - "properties": { - "GridLayoutItemKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKind" - }, - "GridLayoutRowKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutRowKind" - } - } - }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemSpec": { "type": "object", "required": [ @@ -2340,66 +2329,6 @@ } } }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutRowKind": { - "type": "object", - "required": [ - "kind", - "spec" - ], - "properties": { - "kind": { - "type": "string", - "default": "" - }, - "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutRowSpec" - } - ] - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutRowSpec": { - "type": "object", - "required": [ - "y", - "collapsed", - "title", - "elements" - ], - "properties": { - "collapsed": { - "type": "boolean", - "default": false - }, - "elements": { - "description": "Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard.", - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKind" - } - ] - } - }, - "repeat": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowRepeatOptions" - }, - "title": { - "type": "string", - "default": "" - }, - "y": { - "type": "integer", - "format": "int64", - "default": 0 - } - } - }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutSpec": { "type": "object", "required": [ @@ -2409,7 +2338,12 @@ "items": { "type": "array", "items": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKindOrGridLayoutRowKind" + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKind" + } + ] } } } @@ -2459,6 +2393,9 @@ "datasource": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataSourceRef" }, + "defaultValue": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" + }, "description": { "type": "string" }, diff --git a/public/app/features/dashboard-scene/scene/export/utils.ts b/public/app/features/dashboard-scene/scene/export/utils.ts index 3cc85834662..ef754171363 100644 --- a/public/app/features/dashboard-scene/scene/export/utils.ts +++ b/public/app/features/dashboard-scene/scene/export/utils.ts @@ -12,14 +12,7 @@ export function removePanelRefFromLayout(layout: DashboardV2Spec['layout'], elem case 'GridLayout': { const items = layout.spec.items || []; layout.spec.items = items.filter((item) => { - if (item.kind === 'GridLayoutItem') { - return item.spec.element.name !== elementName; - } else if (item.kind === 'GridLayoutRow') { - item.spec.elements = item.spec.elements.filter((el) => el.spec.element.name !== elementName); - // Keep the row if it still has elements left - return item.spec.elements.length > 0; - } - return true; + return item.spec.element.name !== elementName; }); break; } @@ -60,17 +53,7 @@ function isLayoutEmpty(layout: DashboardV2Spec['layout']) { switch (layout.kind) { case 'GridLayout': { const items = layout.spec.items || []; - return ( - items.length === 0 || - items.every((item) => { - if (item.kind === 'GridLayoutItem') { - return false; - } else if (item.kind === 'GridLayoutRow') { - return item.spec.elements.length === 0; - } - return false; - }) - ); + return items.length === 0; } case 'AutoGridLayout': { diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap index 8933d1adde3..cc3a40896c7 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap @@ -85,44 +85,6 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model }, }, }, - "panel-2": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [], - "queryOptions": {}, - "transformations": [], - }, - }, - "description": "Test Description 2", - "id": 2, - "links": [ - { - "targetBlank": true, - "title": "Test Link 1", - "url": "http://test1.com", - }, - { - "title": "Test Link 2", - "url": "http://test2.com", - }, - ], - "title": "Test Panel 2", - "vizConfig": { - "kind": "graph", - "spec": { - "fieldConfig": { - "defaults": {}, - "overrides": [], - }, - "options": {}, - "pluginVersion": "7.0.0", - }, - }, - }, - }, }, "layout": { "kind": "GridLayout", @@ -141,33 +103,6 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model "y": 0, }, }, - { - "kind": "GridLayoutRow", - "spec": { - "collapsed": false, - "elements": [ - { - "kind": "GridLayoutItem", - "spec": { - "element": { - "kind": "ElementReference", - "name": "panel-2", - }, - "height": 0, - "width": 0, - "x": 0, - "y": 0, - }, - }, - ], - "repeat": { - "mode": "variable", - "value": "customVar", - }, - "title": "Test Row", - "y": 10, - }, - }, ], }, }, diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts index 5baba89b4ea..a1eea785230 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts @@ -1,9 +1,8 @@ -import { SceneGridItemLike, SceneGridLayout, SceneGridRow, SceneObject, VizPanel } from '@grafana/scenes'; +import { SceneGridItemLike, SceneGridLayout, VizPanel } from '@grafana/scenes'; import { Spec as DashboardV2Spec, GridLayoutItemKind, GridLayoutKind, - GridLayoutRowKind, RepeatOptions, Element, GridLayoutItemSpec, @@ -14,12 +13,8 @@ import { contextSrv } from 'app/core/core'; import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; -import { RowRepeaterBehavior } from '../../scene/layout-default/RowRepeaterBehavior'; -import { RowActions } from '../../scene/layout-default/row-actions/RowActions'; -import { getOriginalKey, isClonedKey } from '../../utils/clone'; import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; import { calculateGridItemDimensions, isLibraryPanel } from '../../utils/utils'; -import { GRID_ROW_HEIGHT } from '../const'; import { buildLibraryPanel, buildVizPanel } from './utils'; @@ -52,11 +47,8 @@ export function deserializeDefaultGridLayout( }); } -function getGridLayoutItems( - body: DefaultGridLayoutManager, - isSnapshot?: boolean -): Array { - let items: Array = []; +function getGridLayoutItems(body: DefaultGridLayoutManager, isSnapshot?: boolean): GridLayoutItemKind[] { + let items: GridLayoutItemKind[] = []; for (const child of body.state.grid.state.children) { if (child instanceof DashboardGridItem) { // TODO: handle panel repeater scenario @@ -65,50 +57,12 @@ function getGridLayoutItems( } else { items.push(gridItemToGridLayoutItemKind(child)); } - } else if (child instanceof SceneGridRow) { - if (isClonedKey(child.state.key!) && !isSnapshot) { - // Skip repeat rows - continue; - } - items.push(gridRowToLayoutRowKind(child, isSnapshot)); } } return items; } -function getRowRepeat(row: SceneGridRow): RepeatOptions | undefined { - if (row.state.$behaviors) { - for (const behavior of row.state.$behaviors) { - if (behavior instanceof RowRepeaterBehavior) { - return { value: behavior.state.variableName, mode: 'variable' }; - } - } - } - return undefined; -} - -function gridRowToLayoutRowKind(row: SceneGridRow, isSnapshot = false): GridLayoutRowKind { - const children = row.state.children.map((child) => { - if (!(child instanceof DashboardGridItem)) { - throw new Error('Unsupported row child type'); - } - const y = (child.state.y ?? 0) - (row.state.y ?? 0) - GRID_ROW_HEIGHT; - return gridItemToGridLayoutItemKind(child, y); - }); - - return { - kind: 'GridLayoutRow', - spec: { - title: row.state.title, - y: row.state.y ?? 0, - collapsed: Boolean(row.state.isCollapsed), - elements: children, - repeat: getRowRepeat(row), - }, - }; -} - export function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, yOverride?: number): GridLayoutItemKind { let elementGridItem: GridLayoutItemKind | undefined; let x = 0, @@ -234,34 +188,7 @@ function createSceneGridLayoutForItems( return gridItems.map((item) => { if (item.kind === 'GridLayoutItem') { return deserializeGridItem(item, elements, panelIdGenerator); - } else if (item.kind === 'GridLayoutRow') { - const children = item.spec.elements.map((gridElement) => { - const panel = elements[getOriginalKey(gridElement.spec.element.name)]; - if (panel.kind === 'Panel' || panel.kind === 'LibraryPanel') { - let id: number | undefined; - if (panelIdGenerator) { - id = panelIdGenerator(); - } - return buildGridItem(gridElement.spec, panel, item.spec.y + GRID_ROW_HEIGHT + gridElement.spec.y, id); - } else { - throw new Error(`Unknown element kind: ${gridElement.kind}`); - } - }); - let behaviors: SceneObject[] | undefined; - if (item.spec.repeat) { - behaviors = [new RowRepeaterBehavior({ variableName: item.spec.repeat.value })]; - } - return new SceneGridRow({ - y: item.spec.y, - isCollapsed: item.spec.collapsed, - title: item.spec.title, - $behaviors: behaviors, - actions: new RowActions({}), - children, - }); } else { - // If this has been validated by the schema we should never reach this point, which is why TS is telling us this is an error. - //@ts-expect-error throw new Error(`Unknown layout element kind: ${item.kind}`); } }); diff --git a/public/app/features/dashboard-scene/serialization/testfiles/rows_after_free_panels.json b/public/app/features/dashboard-scene/serialization/testfiles/rows_after_free_panels.json new file mode 100644 index 00000000000..5d745793cbd --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/testfiles/rows_after_free_panels.json @@ -0,0 +1,374 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "gridPos": { + "h": 2, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 15, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "First panel", + "mode": "markdown" + }, + "pluginVersion": "10.2.0-pre", + "type": "text", + "title": "First panel" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 20, + "panels": [], + "title": "Row at the top - not repeated - saved expanded", + "type": "row" + }, + { + "gridPos": { + "h": 2, + "w": 24, + "x": 0, + "y": 4 + }, + "id": 15, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "
\n Repeated row below. The row has \n a panel that is also repeated horizontally based\n on values in the $pod variable. \n
", + "mode": "markdown" + }, + "pluginVersion": "10.2.0-pre", + "type": "text" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 16, + "panels": [], + "repeat": "server", + "repeatDirection": "h", + "title": "Row for server $server", + "type": "row" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisBorderShow": false, + "barAlignment": 0, + "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": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 2, + "maxPerRow": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "repeat": "pod", + "repeatDirection": "h", + "targets": [ + { + "alias": "server = $server, pod id = $pod ", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 1 + } + ], + "title": "server = $server, pod = $pod", + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 25, + "panels": [ + { + "gridPos": { + "h": 2, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 30, + "title": "Text panel in collapsed row", + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "
\n Just a panel\n
", + "mode": "markdown" + }, + "pluginVersion": "10.2.0-pre", + "type": "text" + } + ], + "title": "Row at the bottom - not repeated - saved collapsed ", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": ["templating", "gdev"], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": ["A", "B"], + "value": ["A", "B"] + }, + "hide": 0, + "includeAll": true, + "multi": true, + "name": "server", + "options": [ + { + "selected": false, + "text": "All", + "value": "$__all" + }, + { + "selected": true, + "text": "A", + "value": "A" + }, + { + "selected": true, + "text": "B", + "value": "B" + }, + { + "selected": false, + "text": "C", + "value": "C" + }, + { + "selected": false, + "text": "D", + "value": "D" + }, + { + "selected": false, + "text": "E", + "value": "E" + }, + { + "selected": false, + "text": "F", + "value": "F" + }, + { + "selected": false, + "text": "E", + "value": "E" + }, + { + "selected": false, + "text": "G", + "value": "G" + }, + { + "selected": false, + "text": "H", + "value": "H" + }, + { + "selected": false, + "text": "I", + "value": "I" + }, + { + "selected": false, + "text": "J", + "value": "J" + }, + { + "selected": false, + "text": "K", + "value": "K" + }, + { + "selected": false, + "text": "L", + "value": "L" + } + ], + "query": "A,B,C,D,E,F,E,G,H,I,J,K,L", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": true, + "text": ["Bob", "Rob"], + "value": ["1", "2"] + }, + "hide": 0, + "includeAll": true, + "multi": true, + "name": "pod", + "options": [ + { + "selected": false, + "text": "All", + "value": "$__all" + }, + { + "selected": true, + "text": "Bob", + "value": "1" + }, + { + "selected": true, + "text": "Rob", + "value": "2" + }, + { + "selected": false, + "text": "Sod", + "value": "3" + }, + { + "selected": false, + "text": "Hod", + "value": "4" + }, + { + "selected": false, + "text": "Cod", + "value": "5" + } + ], + "query": "Bob : 1, Rob : 2,Sod : 3, Hod : 4, Cod : 5", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Repeating rows", + "uid": "Repeating-rows-uid", + "version": 1 +} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 824d2ee4ce4..ce31ae57d0e 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -13,7 +13,6 @@ import { GroupByVariable, AdHocFiltersVariable, SceneDataTransformer, - SceneGridRow, SceneGridItem, } from '@grafana/scenes'; import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples'; @@ -239,14 +238,14 @@ describe('transformSaveModelSchemaV2ToScene', () => { // VizPanel const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels).toHaveLength(3); + expect(vizPanels).toHaveLength(2); // Layout const layout = scene.state.body as DefaultGridLayoutManager; // Panel const panel = getPanelElement(dash, 'panel-1')!; - expect(layout.state.grid.state.children.length).toBe(3); + expect(layout.state.grid.state.children.length).toBe(2); expect(layout.state.grid.state.children[0].state.key).toBe(`grid-item-${panel.spec.id}`); const gridLayoutItemSpec = (dash.layout.spec as GridLayoutSpec).items[0].spec as GridLayoutItemSpec; expect(layout.state.grid.state.children[0].state.width).toBe(gridLayoutItemSpec.width); @@ -267,9 +266,6 @@ describe('transformSaveModelSchemaV2ToScene', () => { const vizLibraryPanel = vizPanels.find((p) => p.state.key === 'panel-2')!; validateVizPanel(vizLibraryPanel, dash); - expect((layout.state.grid.state.children[2] as SceneGridRow).state.isCollapsed).toBe(false); - expect((layout.state.grid.state.children[2] as SceneGridRow).state.y).toBe(20); - // Transformations const panelWithTransformations = vizPanels.find((p) => p.state.key === 'panel-1')!; expect((panelWithTransformations.state.$data as SceneDataTransformer)?.state.transformations[0]).toEqual( @@ -300,7 +296,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { const scene = transformSaveModelSchemaV2ToScene(dashboard); const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels.length).toBe(3); + expect(vizPanels.length).toBe(2); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.type).toBe('mixed'); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME); }); @@ -328,7 +324,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { const scene = transformSaveModelSchemaV2ToScene(dashboard); const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels.length).toBe(3); + expect(vizPanels.length).toBe(2); expect(getQueryRunnerFor(vizPanels[0])?.state.queries[0].datasource).toEqual({ type: 'prometheus', uid: 'datasource1', @@ -355,7 +351,7 @@ describe('transformSaveModelSchemaV2ToScene', () => { const scene = transformSaveModelSchemaV2ToScene(dashboard); const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels(); - expect(vizPanels.length).toBe(3); + expect(vizPanels.length).toBe(2); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.type).toBe('mixed'); expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 0e96bc019bd..84900146d50 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -6,6 +6,7 @@ import { behaviors, ConstantVariable, SceneDataTransformer, + SceneGridItem, SceneGridLayout, SceneGridRow, SceneQueryRunner, @@ -32,6 +33,8 @@ import { PanelTimeRange } from '../scene/PanelTimeRange'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; +import { RowItemRepeaterBehavior } from '../scene/layout-rows/RowItemRepeaterBehavior'; +import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager'; import { NEW_LINK } from '../settings/links/utils'; import { getQueryRunnerFor } from '../utils/utils'; @@ -40,6 +43,7 @@ import { GRAFANA_DATASOURCE_REF } from './const'; import { SnapshotVariable } from './custom-variables/SnapshotVariable'; import dashboard_to_load1 from './testfiles/dashboard_to_load1.json'; import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json'; +import rowsAfterFreePanels from './testfiles/rows_after_free_panels.json'; import { createDashboardSceneFromDashboardModel, buildGridItemForPanel, @@ -809,6 +813,68 @@ describe('transformSaveModelToScene', () => { }); }); + describe('Convert to new rows', () => { + beforeEach(() => { + // set feature flag to true + config.featureToggles.dashboardNewLayouts = true; + }); + afterEach(() => { + config.featureToggles.dashboardNewLayouts = false; + }); + + it('Should convert legacy rows to new rows', () => { + const scene = transformSaveModelToScene({ + dashboard: repeatingRowsAndPanelsDashboardJson as DashboardDataDTO, + meta: {}, + }); + + const layout = scene.state.body as RowsLayoutManager; + const row1 = layout.state.rows[0]; + + expect(row1.state.title).toBe('Row at the top - not repeated - saved expanded'); + const row1Layout = row1.state.layout as DefaultGridLayoutManager; + expect(row1Layout.state.grid.state.children).toHaveLength(1); + const row1gridItem = row1Layout.state.grid.state.children[0] as SceneGridItem; + expect(row1gridItem.state.body).toBeInstanceOf(VizPanel); + const row1Panel = row1gridItem.state.body as VizPanel; + expect(row1Panel.state.pluginId).toBe('text'); + const row1PanelOptions = row1Panel.state.options as { content: string }; + expect(row1PanelOptions.content).toBe( + '
\n Repeated row below. The row has \n a panel that is also repeated horizontally based\n on values in the $pod variable. \n
' + ); + + const row2 = layout.state.rows[1]; + + expect(row2.state.$behaviors?.[0]).toBeInstanceOf(RowItemRepeaterBehavior); + + const repeatBehavior = row2.state.$behaviors?.[0] as RowItemRepeaterBehavior; + expect(repeatBehavior.state.variableName).toBe('server'); + + const lastRow = layout.state.rows[layout.state.rows.length - 1]; + expect(lastRow.state.title).toBe('Row at the bottom - not repeated - saved collapsed '); + const lastRowLayout = lastRow.state.layout as DefaultGridLayoutManager; + expect(lastRowLayout.state.grid.state.children).toHaveLength(1); + const lastRowgridItem = lastRowLayout.state.grid.state.children[0] as SceneGridItem; + expect(lastRowgridItem.state.body).toBeInstanceOf(VizPanel); + const lastRowPanel = lastRowgridItem.state.body as VizPanel; + expect(lastRowPanel.state.pluginId).toBe('text'); + }); + + it('Should convert legacy rows to new rows with free panels before first row', () => { + const scene = transformSaveModelToScene({ + dashboard: rowsAfterFreePanels as DashboardDataDTO, + meta: {}, + }); + + const layout = scene.state.body as RowsLayoutManager; + const row1 = layout.state.rows[0]; + expect(row1.state.title).toBe(''); + expect(row1.state.hideHeader).toBe(true); + const row1Layout = row1.state.layout as DefaultGridLayoutManager; + expect(row1Layout.state.grid.state.children).toHaveLength(1); + }); + }); + describe('Repeating rows', () => { it('Should build correct scene model', () => { const scene = transformSaveModelToScene({ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index c832d48709d..f7dbc4a6700 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -44,7 +44,11 @@ import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/Dash import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; import { RowActions } from '../scene/layout-default/row-actions/RowActions'; +import { RowItem } from '../scene/layout-rows/RowItem'; +import { RowItemRepeaterBehavior } from '../scene/layout-rows/RowItemRepeaterBehavior'; +import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; +import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; import { DashboardInteractions } from '../utils/interactions'; import { getVizPanelKeyForPanelId } from '../utils/utils'; @@ -79,6 +83,56 @@ export function transformSaveModelToScene(rsp: DashboardDTO): DashboardScene { return scene; } +export function createRowsFromPanels(oldPanels: PanelModel[]): RowsLayoutManager { + const rowItems: RowItem[] = []; + + let currentLegacyRow: PanelModel | null = null; + let currentRowPanels: DashboardGridItem[] = []; + + for (const panel of oldPanels) { + if (panel.type === 'row') { + if (!currentLegacyRow && currentRowPanels.length === 0) { + // This is the first row, and we have no panels before it. We set currentLegacyRow to the first row. + currentLegacyRow = panel; + } else if (!currentLegacyRow) { + // This is the first row but we have panels before the first row. We should flush the current panels into a row item with header hidden. + rowItems.push( + new RowItem({ + title: '', + collapse: panel.collapsed, + layout: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: currentRowPanels, + }), + }), + hideHeader: true, + $behaviors: [], + }) + ); + currentRowPanels = []; + + currentLegacyRow = panel; + } else { + // This is a new row. We should flush the current panels into a row item. + rowItems.push(createRowItemFromLegacyRow(currentLegacyRow, currentRowPanels)); + currentRowPanels = []; + currentLegacyRow = panel; + } + } else { + currentRowPanels.push(buildGridItemForPanel(panel)); + } + } + + if (currentLegacyRow) { + // If there is a row left to process, we should flush it into a row item. + rowItems.push(createRowItemFromLegacyRow(currentLegacyRow, currentRowPanels)); + } + + return new RowsLayoutManager({ + rows: rowItems, + }); +} + export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridItemLike[] { // collects all panels and rows const panels: SceneGridItemLike[] = []; @@ -174,6 +228,22 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]): }); } +function createRowItemFromLegacyRow(row: PanelModel, panels: DashboardGridItem[]): RowItem { + const rowItem = new RowItem({ + key: getVizPanelKeyForPanelId(row.id), + title: row.title, + collapse: row.collapsed, + layout: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + // If the row is collapsed it will have panels within the row model. + children: (row.panels?.map((p) => buildGridItemForPanel(p)) ?? []).concat(panels), + }), + }), + $behaviors: row.repeat ? [new RowItemRepeaterBehavior({ variableName: row.repeat })] : undefined, + }); + return rowItem; +} + export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, dto: DashboardDataDTO) { let variables: SceneVariableSet | undefined; let annotationLayers: SceneDataLayerProvider[] = []; @@ -241,6 +311,20 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, version: oldModel.version, }), ]; + + let body: DashboardLayoutManager; + + if (config.featureToggles.dashboardNewLayouts && oldModel.panels.some((p) => p.type === 'row')) { + body = createRowsFromPanels(oldModel.panels); + } else { + body = new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + isLazy: !(dto.preload || contextSrv.user.authenticatedBy === 'render'), + children: createSceneObjectsForPanels(oldModel.panels), + }), + }); + } + const dashboardScene = new DashboardScene( { uid, @@ -255,12 +339,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, title: oldModel.title, version: oldModel.version, scopeMeta, - body: new DefaultGridLayoutManager({ - grid: new SceneGridLayout({ - isLazy: !(dto.preload || contextSrv.user.authenticatedBy === 'render'), - children: createSceneObjectsForPanels(oldModel.panels), - }), - }), + body, $timeRange: new SceneTimeRange({ from: oldModel.time.from, to: oldModel.time.to, diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index e88afda7e4e..0bc40f8d9d6 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -10,7 +10,6 @@ import { IntervalVariable, QueryVariable, SceneGridLayout, - SceneGridRow, SceneRefreshPicker, SceneTimePicker, SceneTimeRange, @@ -45,7 +44,6 @@ import { AutoGridLayout } from '../scene/layout-auto-grid/AutoGridLayout'; import { AutoGridLayoutManager } from '../scene/layout-auto-grid/AutoGridLayoutManager'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; import { RowItem } from '../scene/layout-rows/RowItem'; import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager'; import { TabItem } from '../scene/layout-tabs/TabItem'; @@ -254,30 +252,6 @@ describe('transformSceneToSaveModelSchemaV2', () => { // repeatDirection?: RepeatDirection, // maxPerRow?: number, }), - new SceneGridRow({ - key: 'panel-4', - title: 'Test Row', - y: 10, - $behaviors: [new RowRepeaterBehavior({ variableName: 'customVar' })], - children: [ - new DashboardGridItem({ - y: 11, - body: new VizPanel({ - key: 'panel-2', - pluginId: 'graph', - title: 'Test Panel 2', - description: 'Test Description 2', - fieldConfig: { defaults: {}, overrides: [] }, - pluginVersion: '7.0.0', - $timeRange: new SceneTimeRange({ - timeZone: 'UTC', - from: 'now-3h', - to: 'now', - }), - }), - }), - ], - }), ], }), }), diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index 90b29fbb4fe..eda142c38e5 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -3,10 +3,10 @@ import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2 import { Spec as DashboardV2Spec, GridLayoutItemKind, - GridLayoutItemSpec, GridLayoutKind, - GridLayoutRowSpec, PanelKind, + RowsLayoutKind, + RowsLayoutRowKind, VariableKind, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { @@ -352,76 +352,6 @@ describe('ResponseTransformers', () => { }, gridPos: { x: 0, y: 8, w: 12, h: 8 }, }, - { - id: 3, - type: 'row', - title: 'Row test title', - gridPos: { x: 0, y: 16, w: 12, h: 1 }, - panels: [], - collapsed: false, - }, - { - id: 4, - type: 'timeseries', - title: 'Panel in row', - gridPos: { x: 0, y: 17, w: 16, h: 8 }, - targets: [ - { - refId: 'A', - datasource: 'datasource1', - expr: 'test-query', - hide: false, - }, - ], - datasource: { - type: 'prometheus', - uid: 'datasource1', - }, - fieldConfig: { defaults: {}, overrides: [] }, - options: {}, - transparent: false, - links: [], - transformations: [], - }, - { - id: 5, - type: 'row', - title: 'Collapsed row title', - gridPos: { x: 0, y: 25, w: 12, h: 1 }, - panels: [ - { - id: 5, - type: 'timeseries', - title: 'Panel in collapsed row', - gridPos: { x: 0, y: 26, w: 16, h: 8 }, - targets: [ - { - refId: 'A', - datasource: 'datasource1', - expr: 'test-query', - hide: false, - }, - ], - datasource: { - type: 'prometheus', - uid: 'datasource1', - }, - fieldConfig: { defaults: {}, overrides: [] }, - options: {}, - transparent: false, - links: [], - transformations: [], - }, - ], - collapsed: true, - }, - { - id: 6, - type: 'row', - title: 'Row with no panel property', - gridPos: { x: 0, y: 25, w: 12, h: 1 }, - collapsed: true, - }, ], }; @@ -499,7 +429,7 @@ describe('ResponseTransformers', () => { // Panel expect(spec.layout.kind).toBe('GridLayout'); const layout = spec.layout as GridLayoutKind; - expect(layout.spec.items).toHaveLength(5); + expect(layout.spec.items).toHaveLength(2); expect(layout.spec.items[0].spec).toEqual({ element: { kind: 'ElementReference', @@ -585,48 +515,6 @@ describe('ResponseTransformers', () => { }, }); - const rowSpec = layout.spec.items[2].spec as GridLayoutRowSpec; - - expect(rowSpec.collapsed).toBe(false); - expect(rowSpec.title).toBe('Row test title'); - expect(rowSpec.repeat).toBeUndefined(); - - const panelInRow = rowSpec.elements[0].spec as GridLayoutItemSpec; - - expect(panelInRow).toEqual({ - element: { - kind: 'ElementReference', - name: 'panel-4', - }, - x: 0, - y: 0, - width: 16, - height: 8, - }); - - const collapsedRowSpec = layout.spec.items[3].spec as GridLayoutRowSpec; - expect(collapsedRowSpec.collapsed).toBe(true); - expect(collapsedRowSpec.title).toBe('Collapsed row title'); - expect(collapsedRowSpec.repeat).toBeUndefined(); - - const panelInCollapsedRow = collapsedRowSpec.elements[0].spec as GridLayoutItemSpec; - - expect(panelInCollapsedRow).toEqual({ - element: { - kind: 'ElementReference', - name: 'panel-5', - }, - x: 0, - y: 0, - width: 16, - height: 8, - }); - - const rowWithNoPanelProperty = layout.spec.items[4].spec as GridLayoutRowSpec; - expect(rowWithNoPanelProperty.collapsed).toBe(true); - expect(rowWithNoPanelProperty.title).toBe('Row with no panel property'); - expect(rowWithNoPanelProperty.elements).toHaveLength(0); - // Variables validateVariablesV1ToV2(spec.variables[0], dashboardV1.templating?.list?.[0]); validateVariablesV1ToV2(spec.variables[1], dashboardV1.templating?.list?.[1]); @@ -640,6 +528,254 @@ describe('ResponseTransformers', () => { }); }); + describe('v1 -> v2 transformation with rows', () => { + it('should transform DashboardDTO to DashboardWithAccessInfo', () => { + const dashboardV1: DashboardDataDTO = { + uid: 'dashboard-uid', + id: 123, + title: 'Dashboard Title', + description: 'Dashboard Description', + tags: ['tag1', 'tag2'], + schemaVersion: 1, + graphTooltip: 0, + preload: true, + liveNow: false, + editable: true, + time: { from: 'now-6h', to: 'now' }, + timezone: 'browser', + refresh: '5m', + timepicker: { + refresh_intervals: ['5s', '10s', '30s'], + hidden: false, + nowDelay: '1m', + quick_ranges: [ + { + display: 'Last 6 hours', + from: 'now-6h', + to: 'now', + }, + { + display: 'Last 7 days', + from: 'now-7d', + to: 'now', + }, + ], + }, + fiscalYearStartMonth: 1, + weekStart: 'monday', + version: 1, + gnetId: 'something-like-a-uid', + revision: 225, + links: [], + annotations: { + list: [], + }, + templating: { + list: [], + }, + panels: [ + { + id: 1, + type: 'timeseries', + title: 'Panel Title', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + targets: [ + { + refId: 'A', + datasource: 'datasource1', + expr: 'test-query', + hide: false, + }, + ], + datasource: { + type: 'prometheus', + uid: 'datasource1', + }, + fieldConfig: { defaults: {}, overrides: [] }, + options: {}, + transparent: false, + links: [], + transformations: [], + repeat: 'var1', + repeatDirection: 'h', + }, + { + id: 2, + type: 'table', + title: 'Just a shared table', + libraryPanel: { + uid: 'library-panel-table', + name: 'Table Panel as Library Panel', + }, + gridPos: { x: 0, y: 8, w: 12, h: 8 }, + }, + { + id: 3, + type: 'row', + title: 'Row test title', + gridPos: { x: 0, y: 16, w: 12, h: 1 }, + repeat: 'var1', + repeatDirection: 'v', + panels: [], + collapsed: false, + }, + { + id: 4, + type: 'timeseries', + title: 'Panel in row', + gridPos: { x: 0, y: 17, w: 16, h: 8 }, + targets: [ + { + refId: 'A', + datasource: 'datasource1', + expr: 'test-query', + hide: false, + }, + ], + datasource: { + type: 'prometheus', + uid: 'datasource1', + }, + fieldConfig: { defaults: {}, overrides: [] }, + options: {}, + transparent: false, + links: [], + transformations: [], + }, + { + id: 5, + type: 'row', + title: 'Collapsed row title', + gridPos: { x: 0, y: 25, w: 12, h: 1 }, + panels: [ + { + id: 6, + type: 'timeseries', + title: 'Panel in collapsed row', + gridPos: { x: 0, y: 26, w: 16, h: 8 }, + targets: [ + { + refId: 'A', + datasource: 'datasource1', + expr: 'test-query', + hide: false, + }, + ], + datasource: { + type: 'prometheus', + uid: 'datasource1', + }, + fieldConfig: { defaults: {}, overrides: [] }, + options: {}, + transparent: false, + links: [], + transformations: [], + }, + ], + collapsed: true, + }, + { + id: 7, + type: 'row', + title: 'collapsed row with no panel property', + gridPos: { x: 0, y: 26, w: 12, h: 1 }, + collapsed: true, + }, + { + id: 8, + type: 'row', + title: 'empty row', + gridPos: { x: 0, y: 27, w: 12, h: 1 }, + collapsed: false, + }, + ], + }; + + const dto: DashboardWithAccessInfo = { + spec: dashboardV1, + access: { + slug: 'dashboard-slug', + url: '/d/dashboard-slug', + canAdmin: true, + canDelete: true, + canEdit: true, + canSave: true, + canShare: true, + canStar: true, + annotationsPermissions: { + dashboard: { canAdd: true, canEdit: true, canDelete: true }, + organization: { canAdd: true, canEdit: true, canDelete: true }, + }, + }, + apiVersion: 'v1', + kind: 'DashboardWithAccessInfo', + metadata: { + name: 'dashboard-uid', + resourceVersion: '1', + creationTimestamp: '2023-01-01T00:00:00Z', + annotations: { + [AnnoKeyCreatedBy]: 'user1', + [AnnoKeyUpdatedBy]: 'user2', + [AnnoKeyUpdatedTimestamp]: '2023-01-02T00:00:00Z', + [AnnoKeyFolder]: 'folder1', + [AnnoKeySlug]: 'dashboard-slug', + }, + labels: { + [DeprecatedInternalId]: '123', + }, + }, + }; + + const transformed = ResponseTransformers.ensureV2Response(dto); + + const spec = transformed.spec; + + // Panel + expect(spec.layout.kind).toBe('RowsLayout'); + const layout = spec.layout as RowsLayoutKind; + expect(layout.spec.rows).toHaveLength(5); + + const row0grid = layout.spec.rows[0].spec.layout as GridLayoutKind; + expect(row0grid.kind).toBe('GridLayout'); + expect(row0grid.spec.items).toHaveLength(2); + expect(row0grid.spec.items[0].spec.element.name).toBe('panel-1'); + expect(row0grid.spec.items[0].spec.y).toBe(0); + expect(row0grid.spec.items[1].spec.element.name).toBe('panel-2'); + expect(row0grid.spec.items[1].spec.y).toBe(8); + + const row1 = layout.spec.rows[1] as RowsLayoutRowKind; + expect(row1.kind).toBe('RowsLayoutRow'); + expect(row1.spec.repeat?.value).toBe('var1'); + expect(row1.spec.repeat?.mode).toBe('variable'); + + const row1grid = layout.spec.rows[1].spec.layout as GridLayoutKind; + expect(row1grid.kind).toBe('GridLayout'); + expect(row1grid.spec.items).toHaveLength(1); + expect(row1grid.spec.items[0].spec.element.name).toBe('panel-4'); + + const row2grid = layout.spec.rows[2].spec.layout as GridLayoutKind; + expect(row2grid.kind).toBe('GridLayout'); + expect(row2grid.spec.items).toHaveLength(1); + expect(row2grid.spec.items[0].spec.element.name).toBe('panel-6'); + + const row3 = layout.spec.rows[3] as RowsLayoutRowKind; + expect(row3.kind).toBe('RowsLayoutRow'); + expect(row3.spec.collapse).toBe(true); + expect(row3.spec.layout.kind).toBe('GridLayout'); + const row3grid = row3.spec.layout as GridLayoutKind; + expect(row3grid.kind).toBe('GridLayout'); + expect(row3grid.spec.items).toHaveLength(0); + + const row4 = layout.spec.rows[4] as RowsLayoutRowKind; + expect(row4.kind).toBe('RowsLayoutRow'); + expect(row4.spec.collapse).toBe(false); + expect(row4.spec.layout.kind).toBe('GridLayout'); + const row4grid = row4.spec.layout as GridLayoutKind; + expect(row4grid.kind).toBe('GridLayout'); + expect(row4grid.spec.items).toHaveLength(0); + }); + }); + describe('v2 -> v1 transformation', () => { it('should return the same object if it is already a DashboardDTO', () => { const dashboard: DashboardDTO = { @@ -804,9 +940,6 @@ describe('ResponseTransformers', () => { uid: 'uid-for-library-panel', name: 'Library Panel', }); - expect(dashboard.panels![2].type).toBe('row'); - expect(dashboard.panels![2].id).toBe(4); // Row id should be assigned to unique number following the highest id of panels. - expect(dashboard.panels![3].type).toBe('timeseries'); }); describe('getPanelQueries', () => { diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index 0d358d1a890..70baed5dce7 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -37,8 +37,9 @@ import { GroupByVariableKind, LibraryPanelKind, PanelKind, - GridLayoutRowKind, GridLayoutItemKind, + RowsLayoutRowKind, + GridLayoutKind, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { DashboardLink, DataTransformerConfig } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen'; import { isWeekStart, WeekStart } from '@grafana/ui'; @@ -259,22 +260,49 @@ function getElementsFromPanels( return [elements, layout]; } - let currentRow: GridLayoutRowKind | null = null; + if (panels.some(isRowPanel)) { + return convertToRowsLayout(panels); + } // iterate over panels + for (const p of panels) { + const [element, elementName] = buildElement(p); + + elements[elementName] = element; + + layout.spec.items.push(buildGridItemKind(p, elementName)); + } + + return [elements, layout]; +} + +function convertToRowsLayout( + panels: Array +): [DashboardV2Spec['elements'], DashboardV2Spec['layout']] { + let currentRow: RowsLayoutRowKind | null = null; + let legacyRowY = 0; + const elements: DashboardV2Spec['elements'] = {}; + const layout: DashboardV2Spec['layout'] = { + kind: 'RowsLayout', + spec: { + rows: [], + }, + }; + for (const p of panels) { if (isRowPanel(p)) { + legacyRowY = p.gridPos!.y; if (currentRow) { // Flush current row to layout before we create a new one - layout.spec.items.push(currentRow); + layout.spec.rows.push(currentRow); } + // If the row is collapsed it will have panels const rowElements = []; - for (const panel of p.panels || []) { const [element, name] = buildElement(panel); elements[name] = element; - rowElements.push(buildGridItemKind(panel, name, yOffsetInRows(panel, p.gridPos!.y))); + rowElements.push(buildGridItemKind(panel, name, yOffsetInRows(panel, legacyRowY))); } currentRow = buildRowKind(p, rowElements); @@ -285,18 +313,41 @@ function getElementsFromPanels( if (currentRow) { // Collect panels to current layout row - currentRow.spec.elements.push(buildGridItemKind(p, elementName, yOffsetInRows(p, currentRow.spec.y))); + if (currentRow.spec.layout.kind === 'GridLayout') { + currentRow.spec.layout.spec.items.push(buildGridItemKind(p, elementName, yOffsetInRows(p, legacyRowY))); + } else { + throw new Error('RowsLayoutRow from legacy row must have a GridLayout'); + } } else { - layout.spec.items.push(buildGridItemKind(p, elementName)); + // This is the first row. In V1 these items could live outside of a row. In V2 they will be in a row with header hidden so that it will look similar to V1. + const grid: GridLayoutKind = { + kind: 'GridLayout', + spec: { + items: [buildGridItemKind(p, elementName)], + }, + }; + + // Since this row does not exist in V1, we simulate it being outside of the grid above the first panel + // The Y position does not matter for the rows layout, but it's used to calculate the position of the panels in the grid layout in the row. + legacyRowY = -1; + + currentRow = { + kind: 'RowsLayoutRow', + spec: { + collapse: false, + title: '', + hideHeader: true, + layout: grid, + }, + }; } } } if (currentRow) { // Flush last row to layout - layout.spec.items.push(currentRow); + layout.spec.rows.push(currentRow); } - return [elements, layout]; } @@ -311,15 +362,19 @@ function getWeekStart(weekStart?: string, defaultWeekStart?: WeekStart): WeekSta return weekStart; } -function buildRowKind(p: RowPanel, elements: GridLayoutItemKind[]): GridLayoutRowKind { +function buildRowKind(p: RowPanel, elements: GridLayoutItemKind[]): RowsLayoutRowKind { return { - kind: 'GridLayoutRow', + kind: 'RowsLayoutRow', spec: { - collapsed: p.collapsed, + collapse: p.collapsed, title: p.title ?? '', repeat: p.repeat ? { value: p.repeat, mode: 'variable' } : undefined, - y: p.gridPos?.y ?? 0, - elements, + layout: { + kind: 'GridLayout', + spec: { + items: elements, + }, + }, }, }; } @@ -849,46 +904,11 @@ function getPanelsV1( } for (const item of layout.spec.items) { - if (item.kind === 'GridLayoutItem') { - const panel = panels[item.spec.element.name]; - const v1Panel = transformV2PanelToV1Panel(panel, item); - panelsV1.push(v1Panel); - if (v1Panel.id ?? 0 > maxPanelId) { - maxPanelId = v1Panel.id ?? 0; - } - } else if (item.kind === 'GridLayoutRow') { - const row: RowPanel = { - id: -1, // Temporarily set to -1, updated later to be unique - type: 'row', - title: item.spec.title, - collapsed: item.spec.collapsed, - repeat: item.spec.repeat ? item.spec.repeat.value : undefined, - gridPos: { - x: 0, - y: item.spec.y, - w: 24, - h: GRID_ROW_HEIGHT, - }, - panels: [], - }; - - const rowPanels = []; - for (const panel of item.spec.elements) { - const panelElement = panels[panel.spec.element.name]; - const v1Panel = transformV2PanelToV1Panel(panelElement, panel, item.spec.y + GRID_ROW_HEIGHT + panel.spec.y); - rowPanels.push(v1Panel); - if (v1Panel.id ?? 0 > maxPanelId) { - maxPanelId = v1Panel.id ?? 0; - } - } - if (item.spec.collapsed) { - // When a row is collapsed, panels inside it are stored in the panels property. - row.panels = rowPanels; - panelsV1.push(row); - } else { - panelsV1.push(row); - panelsV1.push(...rowPanels); - } + const panel = panels[item.spec.element.name]; + const v1Panel = transformV2PanelToV1Panel(panel, item); + panelsV1.push(v1Panel); + if (v1Panel.id ?? 0 > maxPanelId) { + maxPanelId = v1Panel.id ?? 0; } }