DashbboardScene: RowRepeaterBehavior (#74505)

* Repeating rows start

* working

* Progress

* Progress

* Update

* up scenes lib

* Update

* Progress

* restore url sync

* Progress

* Fixes and tests

* Update

* Adds tests and code to remove repeats from save model

* Update

* Fix test
pull/74650/head^2
Torkel Ödegaard 2 years ago committed by GitHub
parent fb367bf91d
commit 97d568e60a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      .betterer.results
  2. 334
      devenv/dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel-and-vertical-repeating-panel.json
  3. 257
      devenv/dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel.json
  4. 327
      devenv/dev-dashboards/feature-templating/templating-repeating-rows.json
  5. 28
      devenv/jsonnet/dev-dashboards.libsonnet
  6. 41
      e2e/dashboards-suite/Repeating_a_row_with_a_non_repeating_panel.spec.ts
  7. 2
      package.json
  8. 24
      public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx
  9. 144
      public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx
  10. 215
      public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts
  11. 177
      public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap
  12. 353
      public/app/features/dashboard-scene/serialization/testfiles/repeating_rows_and_panels.json
  13. 24
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts
  14. 57
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
  15. 43
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts
  16. 47
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts
  17. 20
      public/app/features/dashboard-scene/utils/utils.ts
  18. 3
      public/app/features/scenes/scenes/index.tsx
  19. 127
      public/app/features/scenes/scenes/repeatingPanels.tsx
  20. 10
      yarn.lock

@ -1899,9 +1899,17 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts:5381": [
"public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],

@ -1,334 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"iteration": 1640181176989,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 2,
"panels": [],
"repeat": "row",
"title": "Row title $row",
"type": "row"
},
{
"datasource": {
"type": "testdata"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": 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"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 1
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"title": "Panel Title",
"type": "timeseries"
},
{
"datasource": {
"type": "testdata"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": 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"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 1
},
"id": 9,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"repeat": "vertical",
"repeatDirection": "v",
"title": "Vertical repeating $vertical",
"type": "timeseries"
}
],
"schemaVersion": 34,
"tags": [],
"templating": {
"list": [
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"hide": 0,
"includeAll": true,
"multi": true,
"name": "vertical",
"options": [
{
"selected": true,
"text": "All",
"value": "$__all"
},
{
"selected": false,
"text": "1",
"value": "1"
},
{
"selected": false,
"text": "2",
"value": "2"
},
{
"selected": false,
"text": "3",
"value": "3"
}
],
"query": "1,2,3",
"queryValue": "",
"skipUrlSync": false,
"type": "custom"
},
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"hide": 0,
"includeAll": true,
"multi": true,
"name": "horizontal",
"options": [
{
"selected": true,
"text": "All",
"value": "$__all"
},
{
"selected": false,
"text": "1",
"value": "1"
},
{
"selected": false,
"text": "2",
"value": "2"
},
{
"selected": false,
"text": "3",
"value": "3"
}
],
"query": "1,2,3",
"queryValue": "",
"skipUrlSync": false,
"type": "custom"
},
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"hide": 0,
"includeAll": true,
"multi": true,
"name": "row",
"options": [
{
"selected": true,
"text": "All",
"value": "$__all"
},
{
"selected": false,
"text": "1",
"value": "1"
},
{
"selected": false,
"text": "2",
"value": "2"
},
{
"selected": false,
"text": "3",
"value": "3"
}
],
"query": "1,2,3",
"queryValue": "",
"skipUrlSync": false,
"type": "custom"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "utc",
"title": "Repeating a row with a non-repeating panel and vertical repeating panel",
"uid": "7lS-ojt7z",
"version": 2,
"weekStart": ""
}

@ -1,257 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"iteration": 1640181195825,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 2,
"panels": [],
"repeat": "row",
"title": "Row title $row",
"type": "row"
},
{
"datasource": {
"type": "testdata"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": 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"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 1
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"title": "Panel Title",
"type": "timeseries"
}
],
"schemaVersion": 34,
"tags": [],
"templating": {
"list": [
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"hide": 0,
"includeAll": true,
"multi": true,
"name": "vertical",
"options": [
{
"selected": true,
"text": "All",
"value": "$__all"
},
{
"selected": false,
"text": "1",
"value": "1"
},
{
"selected": false,
"text": "2",
"value": "2"
},
{
"selected": false,
"text": "3",
"value": "3"
}
],
"query": "1,2,3",
"queryValue": "",
"skipUrlSync": false,
"type": "custom"
},
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"hide": 0,
"includeAll": true,
"multi": true,
"name": "horizontal",
"options": [
{
"selected": true,
"text": "All",
"value": "$__all"
},
{
"selected": false,
"text": "1",
"value": "1"
},
{
"selected": false,
"text": "2",
"value": "2"
},
{
"selected": false,
"text": "3",
"value": "3"
}
],
"query": "1,2,3",
"queryValue": "",
"skipUrlSync": false,
"type": "custom"
},
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"hide": 0,
"includeAll": true,
"multi": true,
"name": "row",
"options": [
{
"selected": true,
"text": "All",
"value": "$__all"
},
{
"selected": false,
"text": "1",
"value": "1"
},
{
"selected": false,
"text": "2",
"value": "2"
},
{
"selected": false,
"text": "3",
"value": "3"
}
],
"query": "1,2,3",
"queryValue": "",
"skipUrlSync": false,
"type": "custom"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "utc",
"title": "Repeating a row with a non-repeating panel",
"uid": "ZzyTojpnz",
"version": 3,
"weekStart": ""
}

@ -4,19 +4,13 @@
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
@ -24,116 +18,72 @@
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 85,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 2,
"id": 20,
"panels": [],
"repeat": "row",
"title": "Row $row",
"title": "Row at the top - not repeated - saved expanded",
"type": "row"
},
{
"datasource": {
"type": "testdata"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": 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"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"h": 2,
"w": 24,
"x": 0,
"y": 1
},
"id": 4,
"id": 15,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
"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"
},
"title": "Row $row non-repeating panel",
"type": "timeseries"
"pluginVersion": "10.2.0-pre",
"type": "text"
},
{
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 3
},
"id": 16,
"panels": [],
"repeat": "server",
"repeatDirection": "h",
"title": "Row for server $server",
"type": "row"
},
{
"datasource": {
"type": "testdata"
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"axisShow": false,
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
@ -143,6 +93,7 @@
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
@ -164,7 +115,8 @@
"mode": "absolute",
"steps": [
{
"color": "green"
"color": "green",
"value": null
},
{
"color": "red",
@ -176,111 +128,174 @@
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"h": 6,
"w": 12,
"x": 0,
"y": 9
"y": 4
},
"id": 9,
"id": 2,
"maxPerRow": 3,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"repeat": "horizontal",
"repeat": "pod",
"repeatDirection": "h",
"title": "Row $row repeating panel $horizontal",
"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": 21
},
"id": 25,
"panels": [
{
"gridPos": {
"h": 2,
"w": 24,
"x": 0,
"y": 22
},
"id": 30,
"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"
}
],
"schemaVersion": 36,
"tags": [],
"refresh": "",
"schemaVersion": 38,
"tags": [
"templating",
"gdev"
],
"templating": {
"list": [
{
"current": {
"selected": true,
"text": [
"All"
"A",
"B"
],
"value": [
"$__all"
"A",
"B"
]
},
"hide": 0,
"includeAll": true,
"multi": true,
"name": "vertical",
"name": "server",
"options": [
{
"selected": true,
"selected": false,
"text": "All",
"value": "$__all"
},
{
"selected": true,
"text": "A",
"value": "A"
},
{
"selected": true,
"text": "B",
"value": "B"
},
{
"selected": false,
"text": "1",
"value": "1"
"text": "C",
"value": "C"
},
{
"selected": false,
"text": "2",
"value": "2"
"text": "D",
"value": "D"
},
{
"selected": false,
"text": "3",
"value": "3"
}
],
"query": "1,2,3",
"queryValue": "",
"skipUrlSync": false,
"type": "custom"
},
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"hide": 0,
"includeAll": true,
"multi": true,
"name": "horizontal",
"options": [
"text": "E",
"value": "E"
},
{
"selected": true,
"text": "All",
"value": "$__all"
"selected": false,
"text": "F",
"value": "F"
},
{
"selected": false,
"text": "1",
"value": "1"
"text": "E",
"value": "E"
},
{
"selected": false,
"text": "2",
"value": "2"
"text": "G",
"value": "G"
},
{
"selected": false,
"text": "3",
"value": "3"
"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": "1,2,3",
"query": "A,B,C,D,E,F,E,G,H,I,J,K,L",
"queryValue": "",
"skipUrlSync": false,
"type": "custom"
@ -289,39 +304,51 @@
"current": {
"selected": true,
"text": [
"All"
"Bob",
"Rob"
],
"value": [
"$__all"
"1",
"2"
]
},
"hide": 0,
"includeAll": true,
"multi": true,
"name": "row",
"name": "pod",
"options": [
{
"selected": true,
"selected": false,
"text": "All",
"value": "$__all"
},
{
"selected": false,
"text": "1",
"selected": true,
"text": "Bob",
"value": "1"
},
{
"selected": false,
"text": "2",
"selected": true,
"text": "Rob",
"value": "2"
},
{
"selected": false,
"text": "3",
"text": "Sod",
"value": "3"
},
{
"selected": false,
"text": "Hod",
"value": "4"
},
{
"selected": false,
"text": "Cod",
"value": "5"
}
],
"query": "1,2,3",
"query": "Bob : 1, Rob : 2,Sod : 3, Hod : 4, Cod : 5",
"queryValue": "",
"skipUrlSync": false,
"type": "custom"
@ -333,9 +360,9 @@
"to": "now"
},
"timepicker": {},
"timezone": "utc",
"title": "Repeating a row with a non-repeating panel and horizontal repeating panel",
"uid": "k3PEoCpnk",
"timezone": "",
"title": "Repeating rows",
"uid": "Repeating-rows-uid",
"version": 1,
"weekStart": ""
}
}

@ -30,27 +30,6 @@ local dashboard = grafana.dashboard;
id: 0,
}
},
dashboard.new('Repeating-a-row-with-a-non-repeating-pan', import '../dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel-and-horizontal-repeating-panel.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{
spec+: {
id: 0,
}
},
dashboard.new('Repeating-a-row-with-a-non-repeating-pan', import '../dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel-and-vertical-repeating-panel.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{
spec+: {
id: 0,
}
},
dashboard.new('Repeating-a-row-with-a-non-repeating-pan', import '../dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{
spec+: {
id: 0,
}
},
dashboard.new('Repeating-a-row-with-a-repeating-horizon', import '../dev-dashboards/e2e-repeats/Repeating-a-row-with-a-repeating-horizontal-panel.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{
@ -625,6 +604,13 @@ local dashboard = grafana.dashboard;
id: 0,
}
},
dashboard.new('templating-repeating-rows', import '../dev-dashboards/feature-templating/templating-repeating-rows.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{
spec+: {
id: 0,
}
},
dashboard.new('templating-textbox-e2e-scenarios', import '../dev-dashboards/feature-templating/templating-textbox-e2e-scenarios.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{

@ -1,41 +0,0 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'k3PEoCpnk/repeating-a-row-with-a-non-repeating-panel-and-horizontal-repeating-panel';
const DASHBOARD_NAME = 'Repeating a row with a non-repeating panel and horizontal repeating panel';
describe('Repeating a row with repeated panels and a non-repeating panel', () => {
beforeEach(() => {
e2e.flows.login('admin', 'admin');
});
it('should be able to collapse and expand a repeated row without losing panels', () => {
e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST });
e2e().contains(DASHBOARD_NAME).should('be.visible');
const panelsToCheck = [
'Row 2 non-repeating panel',
'Row 2 repeating panel 1',
'Row 2 repeating panel 2',
'Row 2 repeating panel 3',
];
// Collapse Row 1 first so the Row 2 panels all fit on the screen
e2e.components.DashboardRow.title('Row 1').click();
// Rows are expanded by default, so check that all panels are visible
panelsToCheck.forEach((title) => {
e2e.components.Panels.Panel.title(title).should('be.visible');
});
// Collapse the row and check panels are no longer visible
e2e.components.DashboardRow.title('Row 2').click();
panelsToCheck.forEach((title) => {
e2e.components.Panels.Panel.title(title).should('not.exist');
});
// Expand the row and check all panels are visible again
e2e.components.DashboardRow.title('Row 2').click();
panelsToCheck.forEach((title) => {
e2e.components.Panels.Panel.title(title).should('be.visible');
});
});
});

@ -253,7 +253,7 @@
"@grafana/lezer-traceql": "0.0.5",
"@grafana/monaco-logql": "^0.0.7",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^0.29.0",
"@grafana/scenes": "^1.1.1",
"@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*",
"@kusto/monaco-kusto": "^7.4.0",

@ -14,11 +14,12 @@ import {
SceneGridItemLike,
sceneGraph,
MultiValueVariable,
VariableValueSingle,
LocalValueVariable,
} from '@grafana/scenes';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
import { getMultiVariableValues } from '../utils/utils';
interface PanelRepeaterGridItemState extends SceneGridItemStateLike {
source: VizPanel;
repeatedPanels?: VizPanel[];
@ -106,7 +107,7 @@ export class PanelRepeaterGridItem extends SceneObjectBase<PanelRepeaterGridItem
}
const panelToRepeat = this.state.source;
const { values, texts } = this.getVariableValues(variable);
const { values, texts } = getMultiVariableValues(variable);
const repeatedPanels: VizPanel[] = [];
// Loop through variable values and create repeates
@ -143,25 +144,6 @@ export class PanelRepeaterGridItem extends SceneObjectBase<PanelRepeaterGridItem
}
}
private getVariableValues(variable: MultiValueVariable): {
values: VariableValueSingle[];
texts: VariableValueSingle[];
} {
const { value, text, options } = variable.state;
if (variable.hasAllValue()) {
return {
values: options.map((o) => o.value),
texts: options.map((o) => o.label),
};
}
return {
values: Array.isArray(value) ? value : [value],
texts: Array.isArray(text) ? text : [text],
};
}
private getMaxPerRow(): number {
return this.state.maxPerRow ?? 4;
}

@ -0,0 +1,144 @@
import {
EmbeddedScene,
SceneCanvasText,
SceneGridItem,
SceneGridLayout,
SceneGridRow,
SceneTimeRange,
SceneVariableSet,
TestVariable,
} from '@grafana/scenes';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
import { activateFullSceneTree } from '../utils/test-utils';
import { RepeatDirection } from './PanelRepeaterGridItem';
import { RowRepeaterBehavior } from './RowRepeaterBehavior';
describe('RowRepeaterBehavior', () => {
describe('Given scene with variable with 5 values', () => {
let scene: EmbeddedScene, grid: SceneGridLayout;
beforeEach(async () => {
({ scene, grid } = buildScene({ variableQueryTime: 0 }));
activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
});
it('Should repeat row', () => {
// Verify that panel above row remains
expect(grid.state.children[0]).toBeInstanceOf(SceneGridItem);
// Verify that first row still has repeat behavior
const row1 = grid.state.children[1] as SceneGridRow;
expect(row1.state.$behaviors?.[0]).toBeInstanceOf(RowRepeaterBehavior);
expect(row1.state.$variables!.state.variables[0].getValue()).toBe('1');
const row2 = grid.state.children[2] as SceneGridRow;
expect(row2.state.$variables!.state.variables[0].getValueText?.()).toBe('B');
// Should give repeated panels unique keys
const gridItem = row2.state.children[0] as SceneGridItem;
expect(gridItem.state.body?.state.key).toBe('canvas-1-row-1');
});
it('Should push row at the bottom down', () => {
// Should push row at the bottom down
const rowAtTheBottom = grid.state.children[6] as SceneGridRow;
expect(rowAtTheBottom.state.title).toBe('Row at the bottom');
// Panel at the top is 10, each row is (1+5)*5 = 30, so the grid item below it should be 40
expect(rowAtTheBottom.state.y).toBe(40);
});
it('Should handle second repeat cycle and update remove old repeats', async () => {
// trigger another repeat cycle by changing the variable
const variable = scene.state.$variables!.state.variables[0] as TestVariable;
variable.changeValueTo(['2', '3']);
await new Promise((r) => setTimeout(r, 1));
// should now only have 2 repeated rows (and the panel above + the row at the bottom)
expect(grid.state.children.length).toBe(4);
});
});
});
interface SceneOptions {
variableQueryTime: number;
maxPerRow?: number;
itemHeight?: number;
repeatDirection?: RepeatDirection;
}
function buildScene(options: SceneOptions) {
const grid = new SceneGridLayout({
children: [
new SceneGridItem({
x: 0,
y: 0,
width: 24,
height: 10,
body: new SceneCanvasText({
text: 'Panel above row',
}),
}),
new SceneGridRow({
x: 0,
y: 10,
width: 24,
height: 1,
$behaviors: [
new RowRepeaterBehavior({
variableName: 'server',
sources: [
new SceneGridItem({
x: 0,
y: 11,
width: 24,
height: 5,
body: new SceneCanvasText({
key: 'canvas-1',
text: 'Panel inside repeated row, server = $server',
}),
}),
],
}),
],
}),
new SceneGridRow({
x: 0,
y: 16,
width: 24,
height: 5,
title: 'Row at the bottom',
}),
],
});
const scene = new EmbeddedScene({
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
$variables: new SceneVariableSet({
variables: [
new TestVariable({
name: 'server',
query: 'A.*',
value: ALL_VARIABLE_VALUE,
text: ALL_VARIABLE_TEXT,
isMulti: true,
includeAll: true,
delayMs: options.variableQueryTime,
optionsToReturn: [
{ label: 'A', value: '1' },
{ label: 'B', value: '2' },
{ label: 'C', value: '3' },
{ label: 'D', value: '4' },
{ label: 'E', value: '5' },
],
}),
],
}),
body: grid,
});
return { scene, grid };
}

@ -0,0 +1,215 @@
import {
LocalValueVariable,
MultiValueVariable,
sceneGraph,
SceneGridItemLike,
SceneGridLayout,
SceneGridRow,
SceneObjectBase,
SceneObjectState,
SceneVariable,
SceneVariableSet,
VariableDependencyConfig,
VariableValueSingle,
} from '@grafana/scenes';
import { getMultiVariableValues } from '../utils/utils';
interface RowRepeaterBehaviorState extends SceneObjectState {
variableName: string;
sources: SceneGridItemLike[];
}
/**
* This behavior will run an effect function when specified variables change
*/
export class RowRepeaterBehavior extends SceneObjectBase<RowRepeaterBehaviorState> {
protected _variableDependency = new VariableDependencyConfig(this, {
variableNames: [this.state.variableName],
onVariableUpdatesCompleted: this._onVariableChanged.bind(this),
});
private _isWaitingForVariables = false;
public constructor(state: RowRepeaterBehaviorState) {
super(state);
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
// If we our variable is ready we can process repeats on activation
if (sceneGraph.hasVariableDependencyInLoadingState(this)) {
this._isWaitingForVariables = true;
} else {
this._performRepeat();
}
}
private _onVariableChanged(changedVariables: Set<SceneVariable>, dependencyChanged: boolean): void {
if (dependencyChanged) {
this._performRepeat();
return;
}
// If we are waiting for variables and the variable is no longer loading then we are ready to repeat as well
if (this._isWaitingForVariables && !sceneGraph.hasVariableDependencyInLoadingState(this)) {
this._isWaitingForVariables = false;
this._performRepeat();
}
}
private _performRepeat() {
const variable = sceneGraph.lookupVariable(this.state.variableName, this.parent?.parent!);
if (!variable) {
console.error('RepeatedRowBehavior: Variable not found');
return;
}
if (!(variable instanceof MultiValueVariable)) {
console.error('RepeatedRowBehavior: Variable is not a MultiValueVariable');
return;
}
if (!(this.parent instanceof SceneGridRow)) {
console.error('RepeatedRowBehavior: Parent is not a SceneGridRow');
return;
}
const layout = sceneGraph.getLayout(this);
if (!(layout instanceof SceneGridLayout)) {
console.error('RepeatedRowBehavior: Layout is not a SceneGridLayout');
return;
}
const rowToRepeat = this.parent as SceneGridRow;
const { values, texts } = getMultiVariableValues(variable);
const rows: SceneGridRow[] = [];
const rowContentHeight = getRowContentHeight(this.state.sources);
let maxYOfRows = 0;
// Loop through variable values and create repeates
for (let index = 0; index < values.length; index++) {
const children: SceneGridItemLike[] = [];
// Loop through panels inside row
for (const source of this.state.sources) {
const sourceItemY = source.state.y ?? 0;
const itemY = sourceItemY + (rowContentHeight + 1) * index;
const itemClone = source.clone({
key: `${source.state.key}-clone-${index}`,
y: itemY,
});
//Make sure all the child scene objects have unique keys
ensureUniqueKeys(itemClone, index);
children.push(itemClone);
if (maxYOfRows < itemY + itemClone.state.height!) {
maxYOfRows = itemY + itemClone.state.height!;
}
}
const rowClone = this.getRowClone(rowToRepeat, index, values[index], texts[index], rowContentHeight, children);
rows.push(rowClone);
}
updateLayout(layout, rows, maxYOfRows, rowToRepeat);
}
getRowClone(
rowToRepeat: SceneGridRow,
index: number,
value: VariableValueSingle,
text: VariableValueSingle,
rowContentHeight: number,
children: SceneGridItemLike[]
): SceneGridRow {
if (index === 0) {
rowToRepeat.setState({
// not activated
$variables: new SceneVariableSet({
variables: [new LocalValueVariable({ name: this.state.variableName, value, text: String(text) })],
}),
children,
});
return rowToRepeat;
}
const sourceRowY = rowToRepeat.state.y ?? 0;
return rowToRepeat.clone({
key: `${rowToRepeat.state.key}-clone-${index}`,
$variables: new SceneVariableSet({
variables: [new LocalValueVariable({ name: this.state.variableName, value, text: String(text) })],
}),
$behaviors: [],
children,
y: sourceRowY + rowContentHeight * index + index,
});
}
}
function getRowContentHeight(panels: SceneGridItemLike[]): number {
let maxY = 0;
let minY = Number.MAX_VALUE;
for (const panel of panels) {
if (panel.state.y! + panel.state.height! > maxY) {
maxY = panel.state.y! + panel.state.height!;
}
if (panel.state.y! < minY) {
minY = panel.state.y!;
}
}
return maxY - minY;
}
function updateLayout(layout: SceneGridLayout, rows: SceneGridRow[], maxYOfRows: number, rowToRepeat: SceneGridRow) {
const allChildren = getLayoutChildrenFilterOutRepeatClones(layout, rowToRepeat);
const index = allChildren.indexOf(rowToRepeat);
if (index === -1) {
throw new Error('RowRepeaterBehavior: Parent row not found in layout children');
}
const newChildren = [...allChildren.slice(0, index), ...rows, ...allChildren.slice(index + 1)];
// Is there grid items after rows?
if (allChildren.length > index + 1) {
const childrenAfter = allChildren.slice(index + 1);
const firstChildAfterY = childrenAfter[0].state.y!;
const diff = maxYOfRows - firstChildAfterY;
for (const child of childrenAfter) {
if (child.state.y! < maxYOfRows) {
child.setState({ y: child.state.y! + diff });
}
}
}
layout.setState({ children: newChildren });
}
function getLayoutChildrenFilterOutRepeatClones(layout: SceneGridLayout, rowToRepeat: SceneGridRow) {
return layout.state.children.filter((child) => {
if (child.state.key?.startsWith(`${rowToRepeat.state.key}-clone-`)) {
return false;
}
return true;
});
}
function ensureUniqueKeys(item: SceneGridItemLike, rowIndex: number) {
item.forEachChild((child) => {
child.setState({ key: `${child.state.key}-row-${rowIndex}` });
ensureUniqueKeys(child, rowIndex);
});
}

@ -1,6 +1,124 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transformSceneToSaveModel Given a scene Should transform back to peristed model 1`] = `
exports[`transformSceneToSaveModel Given a scene with rows Should transform back to peristed model 1`] = `
{
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0,
},
"id": 20,
"panels": [],
"title": "Row at the top - not repeated - saved expanded",
"type": "row",
},
{
"fieldConfig": {
"defaults": {},
"overrides": [],
},
"gridPos": {
"h": 2,
"w": 24,
"x": 0,
"y": 1,
},
"id": 15,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false,
},
"content": "<div class="center-vh">
Repeated row below. The row has
a panel that is also repeated horizontally based
on values in the $pod variable.
</div>",
"mode": "markdown",
},
"title": "",
"transformations": [],
"transparent": false,
"type": "text",
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 3,
},
"id": 16,
"panels": [],
"repeat": "server",
"title": "Row for server $server",
"type": "row",
},
{
"collapsed": true,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 25,
},
"id": 25,
"panels": [
{
"fieldConfig": {
"defaults": {},
"overrides": [],
},
"gridPos": {
"h": 2,
"w": 24,
"x": 0,
"y": 26,
},
"id": 30,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false,
},
"content": "<div class="center-vh">
Just a panel
</div>",
"mode": "markdown",
},
"transformations": [],
"transparent": false,
"type": "text",
},
],
"title": "Row at the bottom - not repeated - saved collapsed ",
"type": "row",
},
],
"schemaVersion": 36,
"tags": [],
"time": {
"from": "now-6h",
"to": "now",
},
"timezone": "browser",
"title": "Repeating rows",
"uid": "Repeating-rows-uid",
}
`;
exports[`transformSceneToSaveModel Given a simple scene Should transform back to peristed model 1`] = `
{
"editable": true,
"fiscalYearStartMonth": 0,
@ -45,6 +163,63 @@ exports[`transformSceneToSaveModel Given a scene Should transform back to perist
"transparent": false,
"type": "timeseries",
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 8,
},
"id": 5,
"panels": [],
"title": "Row title",
"type": "row",
},
{
"fieldConfig": {
"defaults": {},
"overrides": [],
},
"gridPos": {
"h": 10,
"w": 12,
"x": 0,
"y": 9,
},
"id": 29,
"options": {},
"title": "panel inside row",
"transformations": [],
"transparent": false,
"type": "timeseries",
},
{
"fieldConfig": {
"defaults": {},
"overrides": [],
},
"gridPos": {
"h": 10,
"w": 11,
"x": 12,
"y": 9,
},
"id": 25,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false,
},
"content": "content",
"mode": "markdown",
},
"title": "Transparent text panel",
"transformations": [],
"transparent": true,
"type": "text",
},
],
"schemaVersion": 36,
"tags": [],

@ -0,0 +1,353 @@
{
"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": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 20,
"panels": [],
"title": "Row at the top - not repeated - saved expanded",
"type": "row"
},
{
"gridPos": {
"h": 2,
"w": 24,
"x": 0,
"y": 1
},
"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": 3
},
"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",
"axisShow": 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": 4
},
"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,
"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,
"weekStart": ""
}

@ -20,15 +20,18 @@ import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { ShareQueryDataProvider } from '../scene/ShareQueryDataProvider';
import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json';
import {
createDashboardSceneFromDashboardModel,
buildGridItemForPanel,
createSceneVariableFromVariableModel,
transformSaveModelToScene,
} from './transformSaveModelToScene';
describe('DashboardLoader', () => {
describe('transformSaveModelToScene', () => {
describe('when creating dashboard scene', () => {
it('should initialize the DashboardScene with the model state', () => {
const dash = {
@ -130,6 +133,7 @@ describe('DashboardLoader', () => {
const rowWithPanel = createPanelJSONFixture({
title: 'Row with panel',
type: 'row',
id: 10,
collapsed: false,
gridPos: {
h: 1,
@ -182,6 +186,7 @@ describe('DashboardLoader', () => {
expect(body.state.children[1]).toBeInstanceOf(SceneGridRow);
const rowWithPanelsScene = body.state.children[1] as SceneGridRow;
expect(rowWithPanelsScene.state.title).toBe(rowWithPanel.title);
expect(rowWithPanelsScene.state.key).toBe('panel-10');
expect(rowWithPanelsScene.state.children).toHaveLength(1);
// Panel within row
expect(rowWithPanelsScene.state.children[0]).toBeInstanceOf(SceneGridItem);
@ -410,6 +415,7 @@ describe('DashboardLoader', () => {
hide: 0,
});
});
it('should migrate query variable', () => {
const variable = {
allValue: null,
@ -615,6 +621,22 @@ describe('DashboardLoader', () => {
expect(() => createSceneVariableFromVariableModel(variable)).toThrow();
});
});
describe('Repeating rows', () => {
it('Should build correct scene model', () => {
const scene = transformSaveModelToScene({ dashboard: repeatingRowsAndPanelsDashboardJson as any, meta: {} });
const body = scene.state.body as SceneGridLayout;
const row2 = body.state.children[1] as SceneGridRow;
expect(row2.state.$behaviors?.[0]).toBeInstanceOf(RowRepeaterBehavior);
const repeatBehavior = row2.state.$behaviors?.[0] as RowRepeaterBehavior;
expect(repeatBehavior.state.variableName).toBe('server');
const lastRow = body.state.children[body.state.children.length - 1] as SceneGridRow;
expect(lastRow.state.isCollapsed).toBe(true);
});
});
});
function buildGridItemForTest(saveModel: Partial<Panel>): { gridItem: SceneGridItem; vizPanel: VizPanel } {

@ -35,6 +35,7 @@ import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { createPanelDataProvider } from '../utils/createPanelDataProvider';
import { getVizPanelKeyForPanelId } from '../utils/utils';
@ -67,14 +68,7 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridI
if (!currentRow) {
if (Boolean(panel.collapsed)) {
// collapsed rows contain their panels within the row model
panels.push(
new SceneGridRow({
title: panel.title,
isCollapsed: true,
y: panel.gridPos.y,
children: panel.panels ? panel.panels.map(buildGridItemForPanel) : [],
})
);
panels.push(createRowFromPanelModel(panel, []));
} else {
// indicate new row to be processed
currentRow = panel;
@ -83,13 +77,7 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridI
// when a row has been processed, and we hit a next one for processing
if (currentRow.id !== panel.id) {
// commit previous row panels
panels.push(
new SceneGridRow({
title: currentRow!.title,
y: currentRow.gridPos.y,
children: currentRowPanels,
})
);
panels.push(createRowFromPanelModel(currentRow, currentRowPanels));
currentRow = panel;
currentRowPanels = [];
@ -121,18 +109,43 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridI
// commit a row if it's the last one
if (currentRow) {
panels.push(
new SceneGridRow({
title: currentRow!.title,
y: currentRow.gridPos.y,
children: currentRowPanels,
})
);
panels.push(createRowFromPanelModel(currentRow, currentRowPanels));
}
return panels;
}
function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]): SceneGridItemLike {
if (Boolean(row.collapsed)) {
if (row.panels) {
content = row.panels.map(buildGridItemForPanel);
}
}
let behaviors: SceneObject[] | undefined;
let children = content;
if (row.repeat) {
// For repeated rows the children are stored in the behavior
children = [];
behaviors = [
new RowRepeaterBehavior({
variableName: row.repeat,
sources: content,
}),
];
}
return new SceneGridRow({
key: getVizPanelKeyForPanelId(row.id),
title: row.title,
y: row.gridPos.y,
isCollapsed: row.collapsed,
children: children,
$behaviors: behaviors,
});
}
export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) {
let variables: SceneVariableSet | undefined = undefined;

@ -1,13 +1,16 @@
import { SceneGridItemLike } from '@grafana/scenes';
import { Panel } from '@grafana/schema';
import { MultiValueVariable, SceneGridItemLike, SceneGridLayout, SceneGridRow, SceneVariable } from '@grafana/scenes';
import { Panel, RowPanel } from '@grafana/schema';
import { PanelModel } from 'app/features/dashboard/state';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json';
import { buildGridItemForPanel, transformSaveModelToScene } from './transformSaveModelToScene';
import { gridItemToPanel, transformSceneToSaveModel } from './transformSceneToSaveModel';
describe('transformSceneToSaveModel', () => {
describe('Given a scene', () => {
describe('Given a simple scene', () => {
it('Should transform back to peristed model', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
const saveModel = transformSceneToSaveModel(scene);
@ -16,6 +19,40 @@ describe('transformSceneToSaveModel', () => {
});
});
describe('Given a scene with rows', () => {
it('Should transform back to peristed model', () => {
const scene = transformSaveModelToScene({ dashboard: repeatingRowsAndPanelsDashboardJson as any, meta: {} });
const saveModel = transformSceneToSaveModel(scene);
const row2: RowPanel = saveModel.panels![2] as RowPanel;
expect(row2.type).toBe('row');
expect(row2.repeat).toBe('server');
expect(saveModel).toMatchSnapshot();
});
it('Should remove repeated rows in save model', () => {
const scene = transformSaveModelToScene({ dashboard: repeatingRowsAndPanelsDashboardJson as any, meta: {} });
const variable = scene.state.$variables?.state.variables[0] as MultiValueVariable;
variable.changeValueTo(['a', 'b', 'c']);
const grid = scene.state.body as SceneGridLayout;
const rowWithRepeat = grid.state.children[1] as SceneGridRow;
const rowRepeater = rowWithRepeat.state.$behaviors![0] as RowRepeaterBehavior;
// trigger row repeater
rowRepeater.variableDependency?.variableUpdatesCompleted(new Set<SceneVariable>([variable]));
// Make sure the repeated rows have been added to runtime scene model
expect(grid.state.children.length).toBe(5);
const saveModel = transformSceneToSaveModel(scene);
const rows = saveModel.panels!.filter((p) => p.type === 'row');
// Verify the save model does not contain any repeated rows
expect(rows.length).toBe(3);
});
});
describe('Panel options', () => {
it('Given panel with time override', () => {
const gridItem = buildGridItemFromPanelSchema({

@ -1,10 +1,11 @@
import { SceneGridItem, SceneGridItemLike, SceneGridLayout, VizPanel } from '@grafana/scenes';
import { Dashboard, defaultDashboard, FieldConfigSource, Panel } from '@grafana/schema';
import { SceneGridItem, SceneGridItemLike, SceneGridLayout, SceneGridRow, VizPanel } from '@grafana/scenes';
import { Dashboard, defaultDashboard, FieldConfigSource, Panel, RowPanel } from '@grafana/schema';
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
import { DashboardScene } from '../scene/DashboardScene';
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { getPanelIdForVizPanel } from '../utils/utils';
export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
@ -18,6 +19,14 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
if (child instanceof SceneGridItem) {
panels.push(gridItemToPanel(child));
}
if (child instanceof SceneGridRow) {
// Skip repeat clones
if (child.state.key!.indexOf('-clone-') > 0) {
continue;
}
gridRowToSaveModel(child, panels);
}
}
}
@ -98,3 +107,37 @@ export function gridItemToPanel(gridItem: SceneGridItemLike): Panel {
return panel;
}
export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array<Panel | RowPanel>) {
const rowPanel: RowPanel = {
type: 'row',
id: getPanelIdForVizPanel(gridRow),
title: gridRow.state.title,
gridPos: {
x: gridRow.state.x ?? 0,
y: gridRow.state.y ?? 0,
w: gridRow.state.width ?? 24,
h: gridRow.state.height ?? 1,
},
collapsed: Boolean(gridRow.state.isCollapsed),
panels: [],
};
if (gridRow.state.$behaviors?.length) {
const behavior = gridRow.state.$behaviors[0];
if (behavior instanceof RowRepeaterBehavior) {
rowPanel.repeat = behavior.state.variableName;
}
}
panelsArray.push(rowPanel);
const panelsInsideRow = gridRow.state.children.map(gridItemToPanel);
if (gridRow.state.isCollapsed) {
rowPanel.panels = panelsInsideRow;
} else {
panelsArray.push(...panelsInsideRow);
}
}

@ -1,10 +1,10 @@
import { sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
import { MultiValueVariable, sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
export function getVizPanelKeyForPanelId(panelId: number) {
return `panel-${panelId}`;
}
export function getPanelIdForVizPanel(panel: VizPanel): number {
export function getPanelIdForVizPanel(panel: SceneObject): number {
return parseInt(panel.state.key!.replace('panel-', ''), 10);
}
@ -66,3 +66,19 @@ export function forceRenderChildren(model: SceneObject, recursive?: boolean) {
forceRenderChildren(child, recursive);
});
}
export function getMultiVariableValues(variable: MultiValueVariable) {
const { value, text, options } = variable.state;
if (variable.hasAllValue()) {
return {
values: options.map((o) => o.value),
texts: options.map((o) => o.label),
};
}
return {
values: Array.isArray(value) ? value : [value],
texts: Array.isArray(text) ? text : [text],
};
}

@ -4,7 +4,7 @@ import { getGridWithMultipleTimeRanges } from './gridMultiTimeRange';
import { getMultipleGridLayoutTest } from './gridMultiple';
import { getGridWithMultipleData } from './gridWithMultipleData';
import { getQueryVariableDemo } from './queryVariableDemo';
import { getRepeatingPanelsDemo } from './repeatingPanels';
import { getRepeatingPanelsDemo, getRepeatingRowsDemo } from './repeatingPanels';
import { getSceneWithRows } from './sceneWithRows';
import { getTransformationsDemo } from './transformations';
import { getVariablesDemo, getVariablesDemoWithAll } from './variablesDemo';
@ -22,6 +22,7 @@ export function getScenes(): SceneDef[] {
{ title: 'Variables', getScene: getVariablesDemo },
{ title: 'Variables with All values', getScene: getVariablesDemoWithAll },
{ title: 'Variables - Repeating panels', getScene: getRepeatingPanelsDemo },
{ title: 'Variables - Repeating rows', getScene: getRepeatingRowsDemo },
{ title: 'Query variable', getScene: getQueryVariableDemo },
{ title: 'Transformations demo', getScene: getTransformationsDemo },
];

@ -8,9 +8,11 @@ import {
PanelBuilders,
SceneGridLayout,
SceneControlsSpacer,
SceneGridRow,
} from '@grafana/scenes';
import { VariableRefresh } from '@grafana/schema';
import { PanelRepeaterGridItem } from 'app/features/dashboard-scene/scene/PanelRepeaterGridItem';
import { RowRepeaterBehavior } from 'app/features/dashboard-scene/scene/RowRepeaterBehavior';
import { DashboardScene } from '../../dashboard-scene/scene/DashboardScene';
@ -38,7 +40,7 @@ export function getRepeatingPanelsDemo(): DashboardScene {
refresh: VariableRefresh.onTimeRangeChanged,
optionsToReturn: [
{ label: 'A', value: 'A' },
{ label: 'B', value: 'C' },
{ label: 'B', value: 'B' },
],
options: [],
$behaviors: [changeVariable],
@ -78,27 +80,24 @@ export function getRepeatingPanelsDemo(): DashboardScene {
function changeVariable(variable: TestVariable) {
const sub = variable.subscribeToState((state, old) => {
if (!state.loading && old.loading) {
setTimeout(() => {
if (variable.state.query === 'AB') {
variable.setState({
query: 'ABC',
optionsToReturn: [
{ label: 'A', value: 'A' },
{ label: 'B', value: 'B' },
{ label: 'C', value: 'C' },
],
});
} else {
variable.setState({
query: 'AB',
optionsToReturn: [
{ label: 'A', value: 'A' },
{ label: 'B', value: 'B' },
],
});
}
});
return;
if (variable.state.optionsToReturn.length === 2) {
variable.setState({
query: 'ABC',
optionsToReturn: [
{ label: 'A', value: 'A' },
{ label: 'B', value: 'B' },
{ label: 'C', value: 'C' },
],
});
} else {
variable.setState({
query: 'AB',
optionsToReturn: [
{ label: 'A', value: 'A' },
{ label: 'B', value: 'B' },
],
});
}
}
});
@ -106,3 +105,87 @@ function changeVariable(variable: TestVariable) {
sub.unsubscribe();
};
}
export function getRepeatingRowsDemo(): DashboardScene {
return new DashboardScene({
title: 'Variables - Repeating rows',
$variables: new SceneVariableSet({
variables: [
new TestVariable({
name: 'server',
query: 'AB',
value: ['A', 'B', 'C'],
text: ['A', 'B', 'C'],
delayMs: 2000,
isMulti: true,
includeAll: true,
refresh: VariableRefresh.onTimeRangeChanged,
optionsToReturn: [
{ label: 'A', value: 'A' },
{ label: 'B', value: 'B' },
{ label: 'C', value: 'C' },
],
options: [],
//$behaviors: [changeVariable],
}),
new TestVariable({
name: 'pod',
query: 'AB',
value: ['Mu', 'Ma', 'Mi'],
text: ['Mu', 'Ma', 'Mi'],
delayMs: 2000,
isMulti: true,
includeAll: true,
refresh: VariableRefresh.onTimeRangeChanged,
optionsToReturn: [
{ label: 'Mu', value: 'Mu' },
{ label: 'Ma', value: 'Ma' },
{ label: 'Mi', value: 'Mi' },
],
options: [],
}),
],
}),
body: new SceneGridLayout({
isDraggable: true,
isResizable: true,
children: [
new SceneGridRow({
title: 'Row $server',
key: 'Row A',
isCollapsed: false,
y: 0,
x: 0,
$behaviors: [
new RowRepeaterBehavior({
variableName: 'server',
sources: [
new PanelRepeaterGridItem({
variableName: 'pod',
x: 0,
y: 0,
width: 24,
height: 5,
itemHeight: 5,
//@ts-expect-error
source: PanelBuilders.timeseries()
.setTitle('server = $server, pod = $pod')
.setData(getQueryRunnerWithRandomWalkQuery({ alias: 'server = $server, pod = $pod' }))
.build(),
}),
],
}),
],
}),
],
}),
$timeRange: new SceneTimeRange(),
actions: [],
controls: [
new VariableValueSelectors({}),
new SceneControlsSpacer(),
new SceneTimePicker({}),
new SceneRefreshPicker({}),
],
});
}

@ -3939,9 +3939,9 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes@npm:^0.29.0":
version: 0.29.0
resolution: "@grafana/scenes@npm:0.29.0"
"@grafana/scenes@npm:^1.1.1":
version: 1.1.1
resolution: "@grafana/scenes@npm:1.1.1"
dependencies:
"@grafana/e2e-selectors": 10.0.2
react-grid-layout: 1.3.4
@ -3953,7 +3953,7 @@ __metadata:
"@grafana/runtime": 10.0.3
"@grafana/schema": 10.0.3
"@grafana/ui": 10.0.3
checksum: 8a91ea0290d54c5c081595e85f853b14af90468da3d85b5cd83e26d24d4fc84cceea9be930aa9239439ff3af7388ae3f5bebe1973214c686f4cf143a64752548
checksum: 6405998a40e38f088443f5d4b1f5ea1f73e5bc0d08216e4aaccf8ff0b68ec4c3d691430857f357d3eed335dee0dc2e24c41c5c2f286fc7fdd32375382ad3eafe
languageName: node
linkType: hard
@ -19288,7 +19288,7 @@ __metadata:
"@grafana/lezer-traceql": 0.0.5
"@grafana/monaco-logql": ^0.0.7
"@grafana/runtime": "workspace:*"
"@grafana/scenes": ^0.29.0
"@grafana/scenes": ^1.1.1
"@grafana/schema": "workspace:*"
"@grafana/tsconfig": ^1.3.0-rc1
"@grafana/ui": "workspace:*"

Loading…
Cancel
Save