diff --git a/.betterer.results b/.betterer.results index 86d7ad924b9..c43b7369f1a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] ], diff --git a/devenv/dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel-and-vertical-repeating-panel.json b/devenv/dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel-and-vertical-repeating-panel.json deleted file mode 100644 index d241b37a2a8..00000000000 --- a/devenv/dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel-and-vertical-repeating-panel.json +++ /dev/null @@ -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": "" -} diff --git a/devenv/dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel.json b/devenv/dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel.json deleted file mode 100644 index 976cf891c46..00000000000 --- a/devenv/dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel.json +++ /dev/null @@ -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": "" -} diff --git a/devenv/dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel-and-horizontal-repeating-panel.json b/devenv/dev-dashboards/feature-templating/templating-repeating-rows.json similarity index 50% rename from devenv/dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel-and-horizontal-repeating-panel.json rename to devenv/dev-dashboards/feature-templating/templating-repeating-rows.json index 1f11911b38d..f8939497d93 100644 --- a/devenv/dev-dashboards/e2e-repeats/Repeating-a-row-with-a-non-repeating-panel-and-horizontal-repeating-panel.json +++ b/devenv/dev-dashboards/feature-templating/templating-repeating-rows.json @@ -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,41 +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" + }, + { + "gridPos": { + "h": 2, + "w": 24, + "x": 0, + "y": 1 + }, + "id": 15, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "
\n Repeated row below. The row has \n a panel that is also repeated horizontally based\n on values in the $pod variable. \n
", + "mode": "markdown" + }, + "pluginVersion": "10.2.0-pre", + "type": "text" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 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, @@ -68,6 +93,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, @@ -89,7 +115,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -101,145 +128,174 @@ "overrides": [] }, "gridPos": { - "h": 8, - "w": 8, + "h": 6, + "w": 12, "x": 0, - "y": 1 + "y": 4 }, - "id": 4, + "id": 2, + "maxPerRow": 3, "options": { "legend": { "calcs": [], "displayMode": "list", - "placement": "bottom" + "placement": "bottom", + "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, - "title": "Row $row non-repeating panel", + "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" }, { - "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": [] - }, + "collapsed": true, "gridPos": { - "h": 8, - "w": 8, + "h": 1, + "w": 24, "x": 0, - "y": 9 + "y": 21 }, - "id": 9, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "single", - "sort": "none" + "id": 25, + "panels": [ + { + "gridPos": { + "h": 2, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 30, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "
\n Just a panel\n
", + "mode": "markdown" + }, + "pluginVersion": "10.2.0-pre", + "type": "text" } - }, - "repeat": "horizontal", - "repeatDirection": "h", - "title": "Row $row repeating panel $horizontal", - "type": "timeseries" + ], + "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": false, - "text": "1", - "value": "1" + "selected": true, + "text": "A", + "value": "A" + }, + { + "selected": true, + "text": "B", + "value": "B" }, { "selected": false, - "text": "2", - "value": "2" + "text": "C", + "value": "C" }, { "selected": false, - "text": "3", - "value": "3" + "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": "1,2,3", + "query": "A,B,C,D,E,F,E,G,H,I,J,K,L", "queryValue": "", "skipUrlSync": false, "type": "custom" @@ -248,80 +304,51 @@ "current": { "selected": true, "text": [ - "All" + "Bob", + "Rob" ], "value": [ - "$__all" + "1", + "2" ] }, "hide": 0, "includeAll": true, "multi": true, - "name": "horizontal", + "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", - "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", + "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": "" -} +} \ No newline at end of file diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index 69a1666fc23..73541ca8bb1 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -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') + { diff --git a/e2e/dashboards-suite/Repeating_a_row_with_a_non_repeating_panel.spec.ts b/e2e/dashboards-suite/Repeating_a_row_with_a_non_repeating_panel.spec.ts deleted file mode 100644 index d07b4e609a6..00000000000 --- a/e2e/dashboards-suite/Repeating_a_row_with_a_non_repeating_panel.spec.ts +++ /dev/null @@ -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'); - }); - }); -}); diff --git a/package.json b/package.json index edfed010391..43cacb56df7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx index 60ac7438eec..6fd7506d29d 100644 --- a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx @@ -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 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; } diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx new file mode 100644 index 00000000000..204394c372f --- /dev/null +++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx @@ -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 }; +} diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts new file mode 100644 index 00000000000..fa8dc661005 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts @@ -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 { + 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, 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); + }); +} diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index 73b4e697eb5..332723bfc4f 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -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": "
+ Repeated row below. The row has + a panel that is also repeated horizontally based + on values in the $pod variable. +
", + "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": "
+ Just a panel +
", + "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": [], diff --git a/public/app/features/dashboard-scene/serialization/testfiles/repeating_rows_and_panels.json b/public/app/features/dashboard-scene/serialization/testfiles/repeating_rows_and_panels.json new file mode 100644 index 00000000000..555e7b80bd1 --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/testfiles/repeating_rows_and_panels.json @@ -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": "
\n Repeated row below. The row has \n a panel that is also repeated horizontally based\n on values in the $pod variable. \n
", + "mode": "markdown" + }, + "pluginVersion": "10.2.0-pre", + "type": "text" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 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": "
\n Just a panel\n
", + "mode": "markdown" + }, + "pluginVersion": "10.2.0-pre", + "type": "text" + } + ], + "title": "Row at the bottom - not repeated - saved collapsed ", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": ["templating", "gdev"], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": ["A", "B"], + "value": ["A", "B"] + }, + "hide": 0, + "includeAll": true, + "multi": true, + "name": "server", + "options": [ + { + "selected": false, + "text": "All", + "value": "$__all" + }, + { + "selected": true, + "text": "A", + "value": "A" + }, + { + "selected": true, + "text": "B", + "value": "B" + }, + { + "selected": false, + "text": "C", + "value": "C" + }, + { + "selected": false, + "text": "D", + "value": "D" + }, + { + "selected": false, + "text": "E", + "value": "E" + }, + { + "selected": false, + "text": "F", + "value": "F" + }, + { + "selected": false, + "text": "E", + "value": "E" + }, + { + "selected": false, + "text": "G", + "value": "G" + }, + { + "selected": false, + "text": "H", + "value": "H" + }, + { + "selected": false, + "text": "I", + "value": "I" + }, + { + "selected": false, + "text": "J", + "value": "J" + }, + { + "selected": false, + "text": "K", + "value": "K" + }, + { + "selected": false, + "text": "L", + "value": "L" + } + ], + "query": "A,B,C,D,E,F,E,G,H,I,J,K,L", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": true, + "text": ["Bob", "Rob"], + "value": ["1", "2"] + }, + "hide": 0, + "includeAll": true, + "multi": true, + "name": "pod", + "options": [ + { + "selected": false, + "text": "All", + "value": "$__all" + }, + { + "selected": true, + "text": "Bob", + "value": "1" + }, + { + "selected": true, + "text": "Rob", + "value": "2" + }, + { + "selected": false, + "text": "Sod", + "value": "3" + }, + { + "selected": false, + "text": "Hod", + "value": "4" + }, + { + "selected": false, + "text": "Cod", + "value": "5" + } + ], + "query": "Bob : 1, Rob : 2,Sod : 3, Hod : 4, Cod : 5", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Repeating rows", + "uid": "Repeating-rows-uid", + "version": 1, + "weekStart": "" +} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index d015934e6fc..fc107fadff5 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -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): { gridItem: SceneGridItem; vizPanel: VizPanel } { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index f52f74fa8e3..34600d74f8f 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -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; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index 36e3689738b..774bc109778 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -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([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({ diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 0745c47af55..6ef94c84f37 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -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) { + 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); + } +} diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 2ede1d237f1..c6fabed5e62 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -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], + }; +} diff --git a/public/app/features/scenes/scenes/index.tsx b/public/app/features/scenes/scenes/index.tsx index 34c2a83d4b4..b5bcf608dc9 100644 --- a/public/app/features/scenes/scenes/index.tsx +++ b/public/app/features/scenes/scenes/index.tsx @@ -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 }, ]; diff --git a/public/app/features/scenes/scenes/repeatingPanels.tsx b/public/app/features/scenes/scenes/repeatingPanels.tsx index c510b24f6d5..81442507e9f 100644 --- a/public/app/features/scenes/scenes/repeatingPanels.tsx +++ b/public/app/features/scenes/scenes/repeatingPanels.tsx @@ -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({}), + ], + }); +} diff --git a/yarn.lock b/yarn.lock index 81acbeeda27..1e530ed3683 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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:*"