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
pull/106009/head^2
Oscar Kilhed 1 month ago committed by GitHub
parent 970dceab8c
commit 4f18ad30c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 15
      apps/dashboard/kinds/v2alpha1/dashboard_spec.cue
  2. 15
      apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue
  3. 114
      apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go
  4. 120
      apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go
  5. 3
      apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list
  6. 78
      packages/grafana-schema/src/schema/dashboard/v2_examples.ts
  7. 15
      packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue
  8. 48
      packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts
  9. 48
      packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts
  10. 75
      pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json
  11. 21
      public/app/features/dashboard-scene/scene/export/utils.ts
  12. 65
      public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap
  13. 79
      public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts
  14. 374
      public/app/features/dashboard-scene/serialization/testfiles/rows_after_free_panels.json
  15. 14
      public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts
  16. 66
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts
  17. 91
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
  18. 26
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts
  19. 369
      public/app/features/dashboard/api/ResponseTransformers.test.ts
  20. 128
      public/app/features/dashboard/api/ResponseTransformers.ts

@ -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: {

@ -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: {

@ -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"`

@ -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{}{},

@ -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

@ -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,
},
},
],
},
},
],
},
},

@ -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: {

@ -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;

@ -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;

@ -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,42 +2329,13 @@
}
}
},
"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": {
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutSpec": {
"type": "object",
"required": [
"y",
"collapsed",
"title",
"elements"
"items"
],
"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.",
"items": {
"type": "array",
"items": {
"default": {},
@ -2385,32 +2345,6 @@
}
]
}
},
"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": [
"items"
],
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKindOrGridLayoutRowKind"
}
}
}
},
@ -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"
},

@ -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': {

@ -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,
},
},
],
},
},

@ -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<GridLayoutItemKind | GridLayoutRowKind> {
let items: Array<GridLayoutItemKind | GridLayoutRowKind> = [];
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}`);
}
});

@ -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": "<div class=\"center-vh\">\n Repeated row below. The row has \n a panel that is also repeated horizontally based\n on values in the $pod variable. \n</div>",
"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": "<div class=\"center-vh\">\n Just a panel\n</div>",
"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
}

@ -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);
});

@ -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(
'<div class=\"center-vh\">\n Repeated row below. The row has \n a panel that is also repeated horizontally based\n on values in the $pod variable. \n</div>'
);
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({

@ -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,

@ -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',
}),
}),
}),
],
}),
],
}),
}),

@ -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<DashboardV2Spec>', () => {
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<DashboardDataDTO> = {
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', () => {

@ -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<Panel | RowPanel>
): [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;
}
}

Loading…
Cancel
Save