mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Repeat panel by variable (#74294)
* Progress * think this a bad approach * Scene panel repeats looking good * Update * update * Update * Use key instead for inspect/view * refactorings to improve tests * Update * More tests * Update * added support for key / value variables * Update * Fixes * remove log * Update * Removed old gdev templating dashboard and added new and improved one * Update * Added repeating panels coded demo * Update to latest scenes lib * review feedback fixes * update * Sync schema
This commit is contained in:
parent
f18cd13f2b
commit
d82a3c9fc6
@ -106,7 +106,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"schemaVersion": 36,
|
"schemaVersion": 36,
|
||||||
"tags": [],
|
"tags": ["gdev", "templating"],
|
||||||
"templating": {
|
"templating": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
@ -199,7 +199,7 @@
|
|||||||
},
|
},
|
||||||
"timepicker": {},
|
"timepicker": {},
|
||||||
"timezone": "utc",
|
"timezone": "utc",
|
||||||
"title": "Repeating a panel horizontally",
|
"title": "Templating - Repeating a panel horizontally",
|
||||||
"uid": "WVpf2jp7z",
|
"uid": "WVpf2jp7z",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"weekStart": ""
|
"weekStart": ""
|
||||||
|
@ -102,7 +102,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"schemaVersion": 34,
|
"schemaVersion": 34,
|
||||||
"tags": [],
|
"tags": ["gdev", "templating"],
|
||||||
"templating": {
|
"templating": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,423 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"links": [],
|
||||||
|
"liveNow": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"gridPos": {
|
||||||
|
"h": 2,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 15,
|
||||||
|
"options": {
|
||||||
|
"code": {
|
||||||
|
"language": "plaintext",
|
||||||
|
"showLineNumbers": false,
|
||||||
|
"showMiniMap": false
|
||||||
|
},
|
||||||
|
"content": "<div class=\"center-vh\">\n Horizontally repeated panel below\n</div>",
|
||||||
|
"mode": "markdown"
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.2.0-pre",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": 6,
|
||||||
|
"w": 8,
|
||||||
|
"x": 0,
|
||||||
|
"y": 2
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"maxPerRow": 3,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repeat": "server",
|
||||||
|
"repeatDirection": "h",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"alias": "server = $server",
|
||||||
|
"datasource": {
|
||||||
|
"type": "testdata",
|
||||||
|
"uid": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk",
|
||||||
|
"seriesCount": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "server=$server",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"gridPos": {
|
||||||
|
"h": 20,
|
||||||
|
"w": 16,
|
||||||
|
"x": 0,
|
||||||
|
"y": 12
|
||||||
|
},
|
||||||
|
"id": 10,
|
||||||
|
"options": {
|
||||||
|
"code": {
|
||||||
|
"language": "plaintext",
|
||||||
|
"showLineNumbers": false,
|
||||||
|
"showMiniMap": false
|
||||||
|
},
|
||||||
|
"content": "### \n\nIt also has a variable with different value and text representations (A=1, B=2, etc). \nTo test that this works for the scoped variable. \n\nIn the title the text representation should be seen (A,B,C, etc). In the legend you\nshould see both the text and value (id). \n\n",
|
||||||
|
"mode": "markdown"
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.2.0-pre",
|
||||||
|
"title": "Panel to the right is configured for vertical repeat",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "testdata",
|
||||||
|
"uid": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"fixedColor": "blue",
|
||||||
|
"mode": "fixed"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"axisShow": false,
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 25,
|
||||||
|
"gradientMode": "hue",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "smooth",
|
||||||
|
"lineWidth": 2,
|
||||||
|
"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": 4,
|
||||||
|
"w": 8,
|
||||||
|
"x": 16,
|
||||||
|
"y": 12
|
||||||
|
},
|
||||||
|
"id": 5,
|
||||||
|
"maxDataPoints": 50,
|
||||||
|
"maxPerRow": 3,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repeat": "host",
|
||||||
|
"repeatDirection": "v",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"alias": "host = ${host:text} / id = $host",
|
||||||
|
"datasource": {
|
||||||
|
"type": "testdata",
|
||||||
|
"uid": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk",
|
||||||
|
"seriesCount": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "host_name = $host",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "",
|
||||||
|
"schemaVersion": 38,
|
||||||
|
"tags": ["gdev", "templating"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"current": {
|
||||||
|
"selected": false,
|
||||||
|
"text": [
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
"C"
|
||||||
|
],
|
||||||
|
"value": [
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
"C"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": true,
|
||||||
|
"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": [
|
||||||
|
"All"
|
||||||
|
],
|
||||||
|
"value": [
|
||||||
|
"$__all"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": true,
|
||||||
|
"multi": true,
|
||||||
|
"name": "host",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"selected": true,
|
||||||
|
"text": "All",
|
||||||
|
"value": "$__all"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selected": false,
|
||||||
|
"text": "A",
|
||||||
|
"value": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selected": false,
|
||||||
|
"text": "B",
|
||||||
|
"value": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selected": false,
|
||||||
|
"text": "C",
|
||||||
|
"value": "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selected": false,
|
||||||
|
"text": "D",
|
||||||
|
"value": "4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selected": false,
|
||||||
|
"text": "E",
|
||||||
|
"value": "5"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"query": "A : 1, B : 2,C : 3, D : 4, E : 5",
|
||||||
|
"queryValue": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Templating - Repeating Panels",
|
||||||
|
"uid": "templating-repeating-panels",
|
||||||
|
"version": 37,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -618,6 +618,13 @@ local dashboard = grafana.dashboard;
|
|||||||
id: 0,
|
id: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
dashboard.new('templating-repeating-panels', import '../dev-dashboards/feature-templating/templating-repeating-panels.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') +
|
dashboard.new('templating-textbox-e2e-scenarios', import '../dev-dashboards/feature-templating/templating-textbox-e2e-scenarios.json') +
|
||||||
resource.addMetadata('folder', 'dev-dashboards') +
|
resource.addMetadata('folder', 'dev-dashboards') +
|
||||||
{
|
{
|
||||||
@ -646,13 +653,6 @@ local dashboard = grafana.dashboard;
|
|||||||
id: 0,
|
id: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dashboard.new('testdata-repeating', import '../dev-dashboards/feature-templating/testdata-repeating.json') +
|
|
||||||
resource.addMetadata('folder', 'dev-dashboards') +
|
|
||||||
{
|
|
||||||
spec+: {
|
|
||||||
id: 0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dashboard.new('testdata-test-variable-output', import '../dev-dashboards/feature-templating/testdata-test-variable-output.json') +
|
dashboard.new('testdata-test-variable-output', import '../dev-dashboards/feature-templating/testdata-test-variable-output.json') +
|
||||||
resource.addMetadata('folder', 'dev-dashboards') +
|
resource.addMetadata('folder', 'dev-dashboards') +
|
||||||
{
|
{
|
||||||
|
@ -463,9 +463,9 @@ Dashboard panels are the basic visualization building blocks.
|
|||||||
| `libraryPanel` | [LibraryPanelRef](#librarypanelref) | No | | A library panel is a reusable panel that you can use in any dashboard.<br/>When you make a change to a library panel, that change propagates to all instances of where the panel is used.<br/>Library panels streamline reuse of panels across multiple dashboards. |
|
| `libraryPanel` | [LibraryPanelRef](#librarypanelref) | No | | A library panel is a reusable panel that you can use in any dashboard.<br/>When you make a change to a library panel, that change propagates to all instances of where the panel is used.<br/>Library panels streamline reuse of panels across multiple dashboards. |
|
||||||
| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. |
|
| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. |
|
||||||
| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. |
|
| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. |
|
||||||
|
| `maxPerRow` | number | No | | Option for repeated panels that controls max items per row<br/>Only relevant for horizontally repeated panels |
|
||||||
| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. |
|
| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. |
|
||||||
| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.<br/>`h` for horizontal, `v` for vertical.<br/>Possible values are: `h`, `v`. |
|
| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.<br/>`h` for horizontal, `v` for vertical.<br/>Possible values are: `h`, `v`. |
|
||||||
| `repeatPanelId` | integer | No | | Id of the repeating panel. |
|
|
||||||
| `repeat` | string | No | | Name of template variable to repeat for. |
|
| `repeat` | string | No | | Name of template variable to repeat for. |
|
||||||
| `tags` | string[] | No | | Tags for the panel. |
|
| `tags` | string[] | No | | Tags for the panel. |
|
||||||
| `targets` | [Target](#target)[] | No | | Depends on the panel plugin. See the plugin documentation for details. |
|
| `targets` | [Target](#target)[] | No | | Depends on the panel plugin. See the plugin documentation for details. |
|
||||||
@ -611,9 +611,9 @@ Dashboard panels are the basic visualization building blocks.
|
|||||||
| `libraryPanel` | [LibraryPanelRef](#librarypanelref) | No | | A library panel is a reusable panel that you can use in any dashboard.<br/>When you make a change to a library panel, that change propagates to all instances of where the panel is used.<br/>Library panels streamline reuse of panels across multiple dashboards. |
|
| `libraryPanel` | [LibraryPanelRef](#librarypanelref) | No | | A library panel is a reusable panel that you can use in any dashboard.<br/>When you make a change to a library panel, that change propagates to all instances of where the panel is used.<br/>Library panels streamline reuse of panels across multiple dashboards. |
|
||||||
| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. |
|
| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. |
|
||||||
| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. |
|
| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. |
|
||||||
|
| `maxPerRow` | number | No | | Option for repeated panels that controls max items per row<br/>Only relevant for horizontally repeated panels |
|
||||||
| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. |
|
| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. |
|
||||||
| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.<br/>`h` for horizontal, `v` for vertical.<br/>Possible values are: `h`, `v`. |
|
| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.<br/>`h` for horizontal, `v` for vertical.<br/>Possible values are: `h`, `v`. |
|
||||||
| `repeatPanelId` | integer | No | | Id of the repeating panel. |
|
|
||||||
| `repeat` | string | No | | Name of template variable to repeat for. |
|
| `repeat` | string | No | | Name of template variable to repeat for. |
|
||||||
| `tags` | string[] | No | | Tags for the panel. |
|
| `tags` | string[] | No | | Tags for the panel. |
|
||||||
| `targets` | [Target](#target)[] | No | | Depends on the panel plugin. See the plugin documentation for details. |
|
| `targets` | [Target](#target)[] | No | | Depends on the panel plugin. See the plugin documentation for details. |
|
||||||
|
@ -539,8 +539,9 @@ lineage: schemas: [{
|
|||||||
// `h` for horizontal, `v` for vertical.
|
// `h` for horizontal, `v` for vertical.
|
||||||
repeatDirection?: *"h" | "v"
|
repeatDirection?: *"h" | "v"
|
||||||
|
|
||||||
// Id of the repeating panel.
|
// Option for repeated panels that controls max items per row
|
||||||
repeatPanelId?: int64
|
// Only relevant for horizontally repeated panels
|
||||||
|
maxPerRow?: number
|
||||||
|
|
||||||
// The maximum number of data points that the panel queries are retrieving.
|
// The maximum number of data points that the panel queries are retrieving.
|
||||||
maxDataPoints?: number
|
maxDataPoints?: number
|
||||||
|
@ -245,7 +245,7 @@
|
|||||||
"@grafana/lezer-traceql": "0.0.4",
|
"@grafana/lezer-traceql": "0.0.4",
|
||||||
"@grafana/monaco-logql": "^0.0.7",
|
"@grafana/monaco-logql": "^0.0.7",
|
||||||
"@grafana/runtime": "workspace:*",
|
"@grafana/runtime": "workspace:*",
|
||||||
"@grafana/scenes": "^0.27.0",
|
"@grafana/scenes": "^0.29.0",
|
||||||
"@grafana/schema": "workspace:*",
|
"@grafana/schema": "workspace:*",
|
||||||
"@grafana/ui": "workspace:*",
|
"@grafana/ui": "workspace:*",
|
||||||
"@kusto/monaco-kusto": "^7.4.0",
|
"@kusto/monaco-kusto": "^7.4.0",
|
||||||
|
@ -693,6 +693,11 @@ export interface Panel {
|
|||||||
* The maximum number of data points that the panel queries are retrieving.
|
* The maximum number of data points that the panel queries are retrieving.
|
||||||
*/
|
*/
|
||||||
maxDataPoints?: number;
|
maxDataPoints?: number;
|
||||||
|
/**
|
||||||
|
* Option for repeated panels that controls max items per row
|
||||||
|
* Only relevant for horizontally repeated panels
|
||||||
|
*/
|
||||||
|
maxPerRow?: number;
|
||||||
/**
|
/**
|
||||||
* It depends on the panel plugin. They are specified by the Options field in panel plugin schemas.
|
* It depends on the panel plugin. They are specified by the Options field in panel plugin schemas.
|
||||||
*/
|
*/
|
||||||
@ -710,10 +715,6 @@ export interface Panel {
|
|||||||
* `h` for horizontal, `v` for vertical.
|
* `h` for horizontal, `v` for vertical.
|
||||||
*/
|
*/
|
||||||
repeatDirection?: ('h' | 'v');
|
repeatDirection?: ('h' | 'v');
|
||||||
/**
|
|
||||||
* Id of the repeating panel.
|
|
||||||
*/
|
|
||||||
repeatPanelId?: number;
|
|
||||||
/**
|
/**
|
||||||
* Tags for the panel.
|
* Tags for the panel.
|
||||||
*/
|
*/
|
||||||
|
@ -546,6 +546,10 @@ type Panel struct {
|
|||||||
// The maximum number of data points that the panel queries are retrieving.
|
// The maximum number of data points that the panel queries are retrieving.
|
||||||
MaxDataPoints *float32 `json:"maxDataPoints,omitempty"`
|
MaxDataPoints *float32 `json:"maxDataPoints,omitempty"`
|
||||||
|
|
||||||
|
// Option for repeated panels that controls max items per row
|
||||||
|
// Only relevant for horizontally repeated panels
|
||||||
|
MaxPerRow *float32 `json:"maxPerRow,omitempty"`
|
||||||
|
|
||||||
// It depends on the panel plugin. They are specified by the Options field in panel plugin schemas.
|
// It depends on the panel plugin. They are specified by the Options field in panel plugin schemas.
|
||||||
Options map[string]any `json:"options"`
|
Options map[string]any `json:"options"`
|
||||||
|
|
||||||
@ -559,9 +563,6 @@ type Panel struct {
|
|||||||
// `h` for horizontal, `v` for vertical.
|
// `h` for horizontal, `v` for vertical.
|
||||||
RepeatDirection *PanelRepeatDirection `json:"repeatDirection,omitempty"`
|
RepeatDirection *PanelRepeatDirection `json:"repeatDirection,omitempty"`
|
||||||
|
|
||||||
// Id of the repeating panel.
|
|
||||||
RepeatPanelId *int64 `json:"repeatPanelId,omitempty"`
|
|
||||||
|
|
||||||
// Tags for the panel.
|
// Tags for the panel.
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
|
||||||
|
@ -7,19 +7,19 @@ describe('DashboardScene', () => {
|
|||||||
it('Should set inspectPanelKey when url has inspect key', () => {
|
it('Should set inspectPanelKey when url has inspect key', () => {
|
||||||
const scene = buildTestScene();
|
const scene = buildTestScene();
|
||||||
scene.urlSync?.updateFromUrl({ inspect: '2' });
|
scene.urlSync?.updateFromUrl({ inspect: '2' });
|
||||||
expect(scene.state.inspectPanelId).toBe('2');
|
expect(scene.state.inspectPanelKey).toBe('2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should handle inspect key that is not found', () => {
|
it('Should handle inspect key that is not found', () => {
|
||||||
const scene = buildTestScene();
|
const scene = buildTestScene();
|
||||||
scene.urlSync?.updateFromUrl({ inspect: '12321' });
|
scene.urlSync?.updateFromUrl({ inspect: '12321' });
|
||||||
expect(scene.state.inspectPanelId).toBe(undefined);
|
expect(scene.state.inspectPanelKey).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set viewPanelKey when url has viewPanel', () => {
|
it('Should set viewPanelKey when url has viewPanel', () => {
|
||||||
const scene = buildTestScene();
|
const scene = buildTestScene();
|
||||||
scene.urlSync?.updateFromUrl({ viewPanel: '2' });
|
scene.urlSync?.updateFromUrl({ viewPanel: '2' });
|
||||||
expect(scene.state.viewPanelId).toBe('2');
|
expect(scene.state.viewPanelKey).toBe('2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
|
|
||||||
import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer';
|
import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer';
|
||||||
import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer';
|
import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer';
|
||||||
import { findVizPanelById, forceRenderChildren } from '../utils/utils';
|
import { findVizPanelByKey, forceRenderChildren } from '../utils/utils';
|
||||||
|
|
||||||
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
||||||
|
|
||||||
@ -29,9 +29,9 @@ export interface DashboardSceneState extends SceneObjectState {
|
|||||||
isEditing?: boolean;
|
isEditing?: boolean;
|
||||||
isDirty?: boolean;
|
isDirty?: boolean;
|
||||||
/** Panel to inspect */
|
/** Panel to inspect */
|
||||||
inspectPanelId?: string;
|
inspectPanelKey?: string;
|
||||||
/** Panel to view in full screen */
|
/** Panel to view in full screen */
|
||||||
viewPanelId?: string;
|
viewPanelKey?: string;
|
||||||
/** Scene object that handles the current drawer */
|
/** Scene object that handles the current drawer */
|
||||||
drawer?: SceneObject;
|
drawer?: SceneObject;
|
||||||
}
|
}
|
||||||
@ -128,7 +128,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
url: locationUtil.getUrlForPartial(location, { viewPanel: null, inspect: null }),
|
url: locationUtil.getUrlForPartial(location, { viewPanel: null, inspect: null }),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.state.viewPanelId) {
|
if (this.state.viewPanelKey) {
|
||||||
pageNav = {
|
pageNav = {
|
||||||
text: 'View panel',
|
text: 'View panel',
|
||||||
parentItem: pageNav,
|
parentItem: pageNav,
|
||||||
@ -141,8 +141,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
/**
|
/**
|
||||||
* Returns the body (layout) or the full view panel
|
* Returns the body (layout) or the full view panel
|
||||||
*/
|
*/
|
||||||
public getBodyToRender(viewPanelId?: string): SceneObject {
|
public getBodyToRender(viewPanelKey?: string): SceneObject {
|
||||||
const viewPanel = findVizPanelById(this, viewPanelId);
|
const viewPanel = findVizPanelByKey(this, viewPanelKey);
|
||||||
return viewPanel ?? this.state.body;
|
return viewPanel ?? this.state.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||||
import { SceneComponentProps } from '@grafana/scenes';
|
import { SceneComponentProps, SceneDebugger } from '@grafana/scenes';
|
||||||
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ import { DashboardScene } from './DashboardScene';
|
|||||||
import { NavToolbarActions } from './NavToolbarActions';
|
import { NavToolbarActions } from './NavToolbarActions';
|
||||||
|
|
||||||
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
||||||
const { controls, viewPanelId, drawer } = model.useState();
|
const { controls, viewPanelKey: viewPanelId, drawer } = model.useState();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const pageNav = model.getPageNav(location);
|
const pageNav = model.getPageNav(location);
|
||||||
@ -27,6 +27,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
|||||||
{controls.map((control) => (
|
{controls.map((control) => (
|
||||||
<control.Component key={control.state.key} model={control} />
|
<control.Component key={control.state.key} model={control} />
|
||||||
))}
|
))}
|
||||||
|
<SceneDebugger scene={model} key={'scene-debugger'} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
|
@ -4,7 +4,7 @@ import { SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '@grafana/scenes
|
|||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
|
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
|
||||||
import { findVizPanelById } from '../utils/utils';
|
import { findVizPanelByKey } from '../utils/utils';
|
||||||
|
|
||||||
import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||||
|
|
||||||
@ -17,41 +17,41 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
|||||||
|
|
||||||
getUrlState(): SceneObjectUrlValues {
|
getUrlState(): SceneObjectUrlValues {
|
||||||
const state = this._scene.state;
|
const state = this._scene.state;
|
||||||
return { inspect: state.inspectPanelId, viewPanel: state.viewPanelId };
|
return { inspect: state.inspectPanelKey, viewPanel: state.viewPanelKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromUrl(values: SceneObjectUrlValues): void {
|
updateFromUrl(values: SceneObjectUrlValues): void {
|
||||||
const { inspectPanelId, viewPanelId } = this._scene.state;
|
const { inspectPanelKey: inspectPanelId, viewPanelKey: viewPanelId } = this._scene.state;
|
||||||
const update: Partial<DashboardSceneState> = {};
|
const update: Partial<DashboardSceneState> = {};
|
||||||
|
|
||||||
// Handle inspect object state
|
// Handle inspect object state
|
||||||
if (typeof values.inspect === 'string') {
|
if (typeof values.inspect === 'string') {
|
||||||
const panel = findVizPanelById(this._scene, values.inspect);
|
const panel = findVizPanelByKey(this._scene, values.inspect);
|
||||||
if (!panel) {
|
if (!panel) {
|
||||||
appEvents.emit(AppEvents.alertError, ['Panel not found']);
|
appEvents.emit(AppEvents.alertError, ['Panel not found']);
|
||||||
locationService.partial({ inspect: null });
|
locationService.partial({ inspect: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
update.inspectPanelId = values.inspect;
|
update.inspectPanelKey = values.inspect;
|
||||||
update.drawer = new PanelInspectDrawer(panel);
|
update.drawer = new PanelInspectDrawer(panel);
|
||||||
} else if (inspectPanelId) {
|
} else if (inspectPanelId) {
|
||||||
update.inspectPanelId = undefined;
|
update.inspectPanelKey = undefined;
|
||||||
update.drawer = undefined;
|
update.drawer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle view panel state
|
// Handle view panel state
|
||||||
if (typeof values.viewPanel === 'string') {
|
if (typeof values.viewPanel === 'string') {
|
||||||
const panel = findVizPanelById(this._scene, values.viewPanel);
|
const panel = findVizPanelByKey(this._scene, values.viewPanel);
|
||||||
if (!panel) {
|
if (!panel) {
|
||||||
appEvents.emit(AppEvents.alertError, ['Panel not found']);
|
appEvents.emit(AppEvents.alertError, ['Panel not found']);
|
||||||
locationService.partial({ viewPanel: null });
|
locationService.partial({ viewPanel: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
update.viewPanelId = values.viewPanel;
|
update.viewPanelKey = values.viewPanel;
|
||||||
} else if (viewPanelId) {
|
} else if (viewPanelId) {
|
||||||
update.viewPanelId = undefined;
|
update.viewPanelKey = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(update).length > 0) {
|
if (Object.keys(update).length > 0) {
|
||||||
|
@ -13,7 +13,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
||||||
const { actions = [], isEditing, viewPanelId, isDirty, uid } = dashboard.useState();
|
const { actions = [], isEditing, viewPanelKey, isDirty, uid } = dashboard.useState();
|
||||||
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
|
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
|
||||||
|
|
||||||
if (uid) {
|
if (uid) {
|
||||||
@ -29,7 +29,7 @@ export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
|||||||
|
|
||||||
toolbarActions.push(<NavToolbarSeparator leftActionsSeparator key="separator" />);
|
toolbarActions.push(<NavToolbarSeparator leftActionsSeparator key="separator" />);
|
||||||
|
|
||||||
if (viewPanelId) {
|
if (viewPanelKey) {
|
||||||
toolbarActions.push(
|
toolbarActions.push(
|
||||||
<Button
|
<Button
|
||||||
onClick={() => locationService.partial({ viewPanel: null })}
|
onClick={() => locationService.partial({ viewPanel: null })}
|
||||||
|
@ -3,8 +3,6 @@ import { locationService } from '@grafana/runtime';
|
|||||||
import { VizPanel, VizPanelMenu } from '@grafana/scenes';
|
import { VizPanel, VizPanelMenu } from '@grafana/scenes';
|
||||||
import { t } from 'app/core/internationalization';
|
import { t } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { getPanelIdForVizPanel } from '../utils/utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Behavior is called when VizPanelMenu is activated (ie when it's opened).
|
* Behavior is called when VizPanelMenu is activated (ie when it's opened).
|
||||||
*/
|
*/
|
||||||
@ -24,7 +22,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
|||||||
iconClassName: 'eye',
|
iconClassName: 'eye',
|
||||||
shortcut: 'v',
|
shortcut: 'v',
|
||||||
// Hm... need the numeric id to be url compatible?
|
// Hm... need the numeric id to be url compatible?
|
||||||
href: locationUtil.getUrlForPartial(location, { viewPanel: getPanelIdForVizPanel(panel) }),
|
href: locationUtil.getUrlForPartial(location, { viewPanel: panel.state.key }),
|
||||||
});
|
});
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
@ -32,7 +30,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
|||||||
iconClassName: 'info-circle',
|
iconClassName: 'info-circle',
|
||||||
shortcut: 'i',
|
shortcut: 'i',
|
||||||
// Hm... need the numeric id to be url compatible?
|
// Hm... need the numeric id to be url compatible?
|
||||||
href: locationUtil.getUrlForPartial(location, { inspect: getPanelIdForVizPanel(panel) }),
|
href: locationUtil.getUrlForPartial(location, { inspect: panel.state.key }),
|
||||||
});
|
});
|
||||||
|
|
||||||
menu.setState({ items });
|
menu.setState({ items });
|
||||||
|
@ -0,0 +1,143 @@
|
|||||||
|
import { EmbeddedScene, SceneTimeRange, SceneVariableSet, TestVariable, VizPanel } from '@grafana/scenes';
|
||||||
|
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
|
||||||
|
|
||||||
|
import { PanelRepeaterGridItem, RepeatDirection } from './PanelRepeaterGridItem';
|
||||||
|
|
||||||
|
describe('PanelRepeaterGridItem', () => {
|
||||||
|
it('Given scene with variable with 2 values', async () => {
|
||||||
|
const { scene, repeater } = buildScene({ variableQueryTime: 0 });
|
||||||
|
|
||||||
|
scene.activate();
|
||||||
|
repeater.activate();
|
||||||
|
|
||||||
|
expect(repeater.state.repeatedPanels?.length).toBe(5);
|
||||||
|
|
||||||
|
const panel1 = repeater.state.repeatedPanels![0];
|
||||||
|
const panel2 = repeater.state.repeatedPanels![1];
|
||||||
|
|
||||||
|
// Panels should have scoped variables
|
||||||
|
expect(panel1.state.$variables?.state.variables[0].getValue()).toBe('1');
|
||||||
|
expect(panel1.state.$variables?.state.variables[0].getValueText?.()).toBe('A');
|
||||||
|
expect(panel2.state.$variables?.state.variables[0].getValue()).toBe('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should wait for variable to load', async () => {
|
||||||
|
const { scene, repeater } = buildScene({ variableQueryTime: 1 });
|
||||||
|
|
||||||
|
scene.activate();
|
||||||
|
repeater.activate();
|
||||||
|
|
||||||
|
expect(repeater.state.repeatedPanels?.length).toBe(0);
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
|
|
||||||
|
expect(repeater.state.repeatedPanels?.length).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should adjust container height to fit panels direction is horizontal', async () => {
|
||||||
|
const { scene, repeater } = buildScene({ variableQueryTime: 0, maxPerRow: 2, itemHeight: 10 });
|
||||||
|
|
||||||
|
scene.activate();
|
||||||
|
repeater.activate();
|
||||||
|
|
||||||
|
// panels require 3 rows so total height should be 30
|
||||||
|
expect(repeater.state.height).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should adjust container height to fit panels when direction is vertical', async () => {
|
||||||
|
const { scene, repeater } = buildScene({ variableQueryTime: 0, itemHeight: 10, repeatDirection: 'v' });
|
||||||
|
|
||||||
|
scene.activate();
|
||||||
|
repeater.activate();
|
||||||
|
|
||||||
|
// In vertical direction height itemCount * itemHeight
|
||||||
|
expect(repeater.state.height).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should adjust itemHeight when container is resized, direction horizontal', async () => {
|
||||||
|
const { scene, repeater } = buildScene({
|
||||||
|
variableQueryTime: 0,
|
||||||
|
itemHeight: 10,
|
||||||
|
repeatDirection: 'h',
|
||||||
|
maxPerRow: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
scene.activate();
|
||||||
|
repeater.activate();
|
||||||
|
|
||||||
|
// Sould be two rows (5 panels and maxPerRow 5)
|
||||||
|
expect(repeater.state.height).toBe(20);
|
||||||
|
|
||||||
|
// resize container
|
||||||
|
repeater.setState({ height: 10 });
|
||||||
|
// given 2 current rows, the itemHeight is halved
|
||||||
|
expect(repeater.state.itemHeight).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should adjust itemHeight when container is resized, direction vertical', async () => {
|
||||||
|
const { scene, repeater } = buildScene({
|
||||||
|
variableQueryTime: 0,
|
||||||
|
itemHeight: 10,
|
||||||
|
repeatDirection: 'v',
|
||||||
|
});
|
||||||
|
|
||||||
|
scene.activate();
|
||||||
|
repeater.activate();
|
||||||
|
|
||||||
|
// In vertical direction height itemCount * itemHeight
|
||||||
|
expect(repeater.state.height).toBe(50);
|
||||||
|
|
||||||
|
// resize container
|
||||||
|
repeater.setState({ height: 25 });
|
||||||
|
// given 5 rows with total height 25 gives new itemHeight of 5
|
||||||
|
expect(repeater.state.itemHeight).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SceneOptions {
|
||||||
|
variableQueryTime: number;
|
||||||
|
maxPerRow?: number;
|
||||||
|
itemHeight?: number;
|
||||||
|
repeatDirection?: RepeatDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildScene(options: SceneOptions) {
|
||||||
|
const repeater = new PanelRepeaterGridItem({
|
||||||
|
variableName: 'server',
|
||||||
|
repeatedPanels: [],
|
||||||
|
repeatDirection: options.repeatDirection,
|
||||||
|
maxPerRow: options.maxPerRow,
|
||||||
|
itemHeight: options.itemHeight,
|
||||||
|
source: new VizPanel({
|
||||||
|
title: 'Panel $server',
|
||||||
|
pluginId: 'timeseries',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
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: repeater,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { scene, repeater };
|
||||||
|
}
|
@ -0,0 +1,244 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import {
|
||||||
|
VizPanel,
|
||||||
|
SceneObjectBase,
|
||||||
|
VariableDependencyConfig,
|
||||||
|
SceneVariable,
|
||||||
|
SceneGridLayout,
|
||||||
|
SceneVariableSet,
|
||||||
|
SceneComponentProps,
|
||||||
|
SceneGridItemStateLike,
|
||||||
|
SceneGridItemLike,
|
||||||
|
sceneGraph,
|
||||||
|
MultiValueVariable,
|
||||||
|
VariableValueSingle,
|
||||||
|
LocalValueVariable,
|
||||||
|
} from '@grafana/scenes';
|
||||||
|
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
|
||||||
|
|
||||||
|
interface PanelRepeaterGridItemState extends SceneGridItemStateLike {
|
||||||
|
source: VizPanel;
|
||||||
|
repeatedPanels?: VizPanel[];
|
||||||
|
variableName: string;
|
||||||
|
itemHeight?: number;
|
||||||
|
repeatDirection?: RepeatDirection | string;
|
||||||
|
maxPerRow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RepeatDirection = 'v' | 'h';
|
||||||
|
|
||||||
|
export class PanelRepeaterGridItem extends SceneObjectBase<PanelRepeaterGridItemState> implements SceneGridItemLike {
|
||||||
|
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||||
|
variableNames: [this.state.variableName],
|
||||||
|
onVariableUpdatesCompleted: this._onVariableChanged.bind(this),
|
||||||
|
});
|
||||||
|
|
||||||
|
private _isWaitingForVariables = false;
|
||||||
|
|
||||||
|
public constructor(state: PanelRepeaterGridItemState) {
|
||||||
|
super(state);
|
||||||
|
|
||||||
|
this.addActivationHandler(() => this._activationHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
private _activationHandler() {
|
||||||
|
this._subs.add(this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState)));
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the current repeat item count to calculate the user intended desired itemHeight
|
||||||
|
*/
|
||||||
|
private _handleGridResize(newState: PanelRepeaterGridItemState, prevState: PanelRepeaterGridItemState) {
|
||||||
|
const itemCount = this.state.repeatedPanels?.length ?? 1;
|
||||||
|
const stateChange: Partial<PanelRepeaterGridItemState> = {};
|
||||||
|
|
||||||
|
// Height changed
|
||||||
|
if (newState.height === prevState.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.getRepeatDirection() === 'v') {
|
||||||
|
const itemHeight = Math.ceil(newState.height! / itemCount);
|
||||||
|
stateChange.itemHeight = itemHeight;
|
||||||
|
} else {
|
||||||
|
const rowCount = Math.ceil(itemCount / this.getMaxPerRow());
|
||||||
|
stateChange.itemHeight = Math.ceil(newState.height! / rowCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateChange.itemHeight !== this.state.itemHeight) {
|
||||||
|
this.setState(stateChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _performRepeat() {
|
||||||
|
const variable = sceneGraph.lookupVariable(this.state.variableName, this);
|
||||||
|
if (!variable) {
|
||||||
|
console.error('SceneGridItemRepeater: Variable not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(variable instanceof MultiValueVariable)) {
|
||||||
|
console.error('PanelRepeaterGridItem: Variable is not a MultiValueVariable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelToRepeat = this.state.source;
|
||||||
|
const { values, texts } = this.getVariableValues(variable);
|
||||||
|
const repeatedPanels: VizPanel[] = [];
|
||||||
|
|
||||||
|
// Loop through variable values and create repeates
|
||||||
|
for (let index = 0; index < values.length; index++) {
|
||||||
|
const clone = panelToRepeat.clone({
|
||||||
|
$variables: new SceneVariableSet({
|
||||||
|
variables: [
|
||||||
|
new LocalValueVariable({ name: variable.state.name, value: values[index], text: String(texts[index]) }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
key: `${panelToRepeat.state.key}-clone-${index}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
repeatedPanels.push(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = this.getRepeatDirection();
|
||||||
|
const stateChange: Partial<PanelRepeaterGridItemState> = { repeatedPanels: repeatedPanels };
|
||||||
|
const itemHeight = this.state.itemHeight ?? 10;
|
||||||
|
const maxPerRow = this.getMaxPerRow();
|
||||||
|
|
||||||
|
if (direction === 'h') {
|
||||||
|
const rowCount = Math.ceil(repeatedPanels.length / maxPerRow);
|
||||||
|
stateChange.height = rowCount * itemHeight;
|
||||||
|
} else {
|
||||||
|
stateChange.height = repeatedPanels.length * itemHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(stateChange);
|
||||||
|
|
||||||
|
// In case we updated our height the grid layout needs to be update
|
||||||
|
if (this.parent instanceof SceneGridLayout) {
|
||||||
|
this.parent!.forceRender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRepeatDirection(): RepeatDirection {
|
||||||
|
return this.state.repeatDirection === 'v' ? 'v' : 'h';
|
||||||
|
}
|
||||||
|
|
||||||
|
public getClassName() {
|
||||||
|
return 'panel-repeater-grid-item';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component = ({ model }: SceneComponentProps<PanelRepeaterGridItem>) => {
|
||||||
|
const { repeatedPanels, itemHeight } = model.useState();
|
||||||
|
const itemCount = repeatedPanels?.length ?? 0;
|
||||||
|
const layoutStyle = useLayoutStyle(model.getRepeatDirection(), itemCount, model.getMaxPerRow(), itemHeight ?? 10);
|
||||||
|
|
||||||
|
if (!repeatedPanels) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={layoutStyle}>
|
||||||
|
{repeatedPanels.map((panel) => (
|
||||||
|
<div className={itemStyle} key={panel.state.key}>
|
||||||
|
<panel.Component model={panel} key={panel.state.key} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useLayoutStyle(direction: RepeatDirection, itemCount: number, maxPerRow: number, itemHeight: number) {
|
||||||
|
return useMemo(() => {
|
||||||
|
const theme = config.theme2;
|
||||||
|
|
||||||
|
// In mobile responsive layout we have to calculate the absolute height
|
||||||
|
const mobileHeight = itemHeight * GRID_CELL_HEIGHT * itemCount + (itemCount - 1) * GRID_CELL_VMARGIN;
|
||||||
|
|
||||||
|
if (direction === 'h') {
|
||||||
|
const rowCount = Math.ceil(itemCount / maxPerRow);
|
||||||
|
const columnCount = Math.ceil(itemCount / rowCount);
|
||||||
|
|
||||||
|
return css({
|
||||||
|
display: 'grid',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
gridTemplateColumns: `repeat(${columnCount}, 1fr)`,
|
||||||
|
gridTemplateRows: `repeat(${rowCount}, 1fr)`,
|
||||||
|
gridColumnGap: theme.spacing(1),
|
||||||
|
gridRowGap: theme.spacing(1),
|
||||||
|
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: mobileHeight,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical is a bit simpler
|
||||||
|
return css({
|
||||||
|
display: 'flex',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
height: mobileHeight,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [direction, itemCount, maxPerRow, itemHeight]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemStyle = css({
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
position: 'relative',
|
||||||
|
});
|
@ -3,7 +3,7 @@ import { advanceTo, clear } from 'jest-date-mock';
|
|||||||
import { dateTime } from '@grafana/data';
|
import { dateTime } from '@grafana/data';
|
||||||
import { SceneCanvasText, SceneFlexItem, SceneFlexLayout, SceneTimeRange } from '@grafana/scenes';
|
import { SceneCanvasText, SceneFlexItem, SceneFlexLayout, SceneTimeRange } from '@grafana/scenes';
|
||||||
|
|
||||||
import { activateFullSceneTree } from '../utils/utils';
|
import { activateFullSceneTree } from '../utils/test-utils';
|
||||||
|
|
||||||
import { PanelTimeRange } from './PanelTimeRange';
|
import { PanelTimeRange } from './PanelTimeRange';
|
||||||
|
|
||||||
|
@ -8,7 +8,8 @@ import {
|
|||||||
SceneObjectState,
|
SceneObjectState,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
|
|
||||||
import { activateFullSceneTree, getVizPanelKeyForPanelId } from '../utils/utils';
|
import { activateFullSceneTree } from '../utils/test-utils';
|
||||||
|
import { getVizPanelKeyForPanelId } from '../utils/utils';
|
||||||
|
|
||||||
import { ShareQueryDataProvider } from './ShareQueryDataProvider';
|
import { ShareQueryDataProvider } from './ShareQueryDataProvider';
|
||||||
|
|
||||||
|
@ -18,12 +18,13 @@ import { createPanelJSONFixture } from 'app/features/dashboard/state/__fixtures_
|
|||||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||||
|
|
||||||
|
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
|
||||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||||
import { ShareQueryDataProvider } from '../scene/ShareQueryDataProvider';
|
import { ShareQueryDataProvider } from '../scene/ShareQueryDataProvider';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createDashboardSceneFromDashboardModel,
|
createDashboardSceneFromDashboardModel,
|
||||||
buildSceneFromPanelModel,
|
buildGridItemForPanel,
|
||||||
createSceneVariableFromVariableModel,
|
createSceneVariableFromVariableModel,
|
||||||
} from './transformSaveModelToScene';
|
} from './transformSaveModelToScene';
|
||||||
|
|
||||||
@ -214,6 +215,7 @@ describe('DashboardLoader', () => {
|
|||||||
defaults: {
|
defaults: {
|
||||||
unit: 'none',
|
unit: 'none',
|
||||||
},
|
},
|
||||||
|
overrides: [],
|
||||||
},
|
},
|
||||||
pluginVersion: '1.0.0',
|
pluginVersion: '1.0.0',
|
||||||
transformations: [
|
transformations: [
|
||||||
@ -235,24 +237,26 @@ describe('DashboardLoader', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const vizPanelSceneObject = buildSceneFromPanelModel(new PanelModel(panel));
|
|
||||||
const vizPanelItelf = vizPanelSceneObject.state.body as VizPanel;
|
const { gridItem, vizPanel } = buildGridItemForTest(panel);
|
||||||
expect(vizPanelItelf?.state.title).toBe('test');
|
|
||||||
expect(vizPanelItelf?.state.pluginId).toBe('test-plugin');
|
expect(gridItem.state.x).toEqual(0);
|
||||||
expect(vizPanelSceneObject.state.x).toEqual(0);
|
expect(gridItem.state.y).toEqual(0);
|
||||||
expect(vizPanelSceneObject.state.y).toEqual(0);
|
expect(gridItem.state.width).toEqual(12);
|
||||||
expect(vizPanelSceneObject.state.width).toEqual(12);
|
expect(gridItem.state.height).toEqual(8);
|
||||||
expect(vizPanelSceneObject.state.height).toEqual(8);
|
|
||||||
expect(vizPanelItelf?.state.options).toEqual(panel.options);
|
expect(vizPanel.state.title).toBe('test');
|
||||||
expect(vizPanelItelf?.state.fieldConfig).toEqual(panel.fieldConfig);
|
expect(vizPanel.state.pluginId).toBe('test-plugin');
|
||||||
expect(vizPanelItelf?.state.pluginVersion).toBe('1.0.0');
|
expect(vizPanel.state.options).toEqual(panel.options);
|
||||||
|
expect(vizPanel.state.fieldConfig).toEqual(panel.fieldConfig);
|
||||||
|
expect(vizPanel.state.pluginVersion).toBe('1.0.0');
|
||||||
|
expect(((vizPanel.state.$data as SceneDataTransformer)?.state.$data as SceneQueryRunner).state.queries).toEqual(
|
||||||
|
panel.targets
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
((vizPanelItelf.state.$data as SceneDataTransformer)?.state.$data as SceneQueryRunner).state.queries
|
((vizPanel.state.$data as SceneDataTransformer)?.state.$data as SceneQueryRunner).state.maxDataPoints
|
||||||
).toEqual(panel.targets);
|
|
||||||
expect(
|
|
||||||
((vizPanelItelf.state.$data as SceneDataTransformer)?.state.$data as SceneQueryRunner).state.maxDataPoints
|
|
||||||
).toEqual(100);
|
).toEqual(100);
|
||||||
expect((vizPanelItelf.state.$data as SceneDataTransformer)?.state.transformations).toEqual(panel.transformations);
|
expect((vizPanel.state.$data as SceneDataTransformer)?.state.transformations).toEqual(panel.transformations);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initalize the VizPanel without title and transparent true', () => {
|
it('should initalize the VizPanel without title and transparent true', () => {
|
||||||
@ -263,8 +267,7 @@ describe('DashboardLoader', () => {
|
|||||||
transparent: true,
|
transparent: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const gridItem = buildSceneFromPanelModel(new PanelModel(panel));
|
const { vizPanel } = buildGridItemForTest(panel);
|
||||||
const vizPanel = gridItem.state.body as VizPanel;
|
|
||||||
|
|
||||||
expect(vizPanel.state.displayMode).toEqual('transparent');
|
expect(vizPanel.state.displayMode).toEqual('transparent');
|
||||||
expect(vizPanel.state.hoverHeader).toEqual(true);
|
expect(vizPanel.state.hoverHeader).toEqual(true);
|
||||||
@ -277,8 +280,7 @@ describe('DashboardLoader', () => {
|
|||||||
timeShift: '1d',
|
timeShift: '1d',
|
||||||
};
|
};
|
||||||
|
|
||||||
const gridItem = buildSceneFromPanelModel(new PanelModel(panel));
|
const { vizPanel } = buildGridItemForTest(panel);
|
||||||
const vizPanel = gridItem.state.body as VizPanel;
|
|
||||||
const timeRange = vizPanel.state.$timeRange as PanelTimeRange;
|
const timeRange = vizPanel.state.$timeRange as PanelTimeRange;
|
||||||
|
|
||||||
expect(timeRange).toBeInstanceOf(PanelTimeRange);
|
expect(timeRange).toBeInstanceOf(PanelTimeRange);
|
||||||
@ -296,8 +298,7 @@ describe('DashboardLoader', () => {
|
|||||||
targets: [{ refId: 'A', panelId: 10 }],
|
targets: [{ refId: 'A', panelId: 10 }],
|
||||||
};
|
};
|
||||||
|
|
||||||
const vizPanel = buildSceneFromPanelModel(new PanelModel(panel)).state.body as VizPanel;
|
const { vizPanel } = buildGridItemForTest(panel);
|
||||||
|
|
||||||
expect(vizPanel.state.$data).toBeInstanceOf(ShareQueryDataProvider);
|
expect(vizPanel.state.$data).toBeInstanceOf(ShareQueryDataProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -314,11 +315,31 @@ describe('DashboardLoader', () => {
|
|||||||
skipDataQuery: true,
|
skipDataQuery: true,
|
||||||
}).meta;
|
}).meta;
|
||||||
|
|
||||||
const gridItem = buildSceneFromPanelModel(new PanelModel(panel));
|
const { vizPanel } = buildGridItemForTest(panel);
|
||||||
const vizPanel = gridItem.state.body as VizPanel;
|
|
||||||
|
|
||||||
expect(vizPanel.state.$data).toBeUndefined();
|
expect(vizPanel.state.$data).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('When repeat is set should build PanelRepeaterGridItem', () => {
|
||||||
|
const panel = {
|
||||||
|
title: '',
|
||||||
|
type: 'text-plugin-34',
|
||||||
|
gridPos: { x: 0, y: 0, w: 8, h: 8 },
|
||||||
|
repeat: 'server',
|
||||||
|
repeatDirection: 'v',
|
||||||
|
maxPerRow: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridItem = buildGridItemForPanel(new PanelModel(panel));
|
||||||
|
const repeater = gridItem as PanelRepeaterGridItem;
|
||||||
|
|
||||||
|
expect(repeater.state.maxPerRow).toBe(8);
|
||||||
|
expect(repeater.state.variableName).toBe('server');
|
||||||
|
expect(repeater.state.width).toBe(8);
|
||||||
|
expect(repeater.state.height).toBe(8);
|
||||||
|
expect(repeater.state.repeatDirection).toBe('v');
|
||||||
|
expect(repeater.state.maxPerRow).toBe(8);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when creating variables objects', () => {
|
describe('when creating variables objects', () => {
|
||||||
@ -595,3 +616,12 @@ describe('DashboardLoader', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function buildGridItemForTest(saveModel: Partial<Panel>): { gridItem: SceneGridItem; vizPanel: VizPanel } {
|
||||||
|
const gridItem = buildGridItemForPanel(new PanelModel(saveModel));
|
||||||
|
if (gridItem instanceof SceneGridItem) {
|
||||||
|
return { gridItem, vizPanel: gridItem.state.body as VizPanel };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('buildGridItemForPanel to return SceneGridItem');
|
||||||
|
}
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
VizPanelMenu,
|
VizPanelMenu,
|
||||||
behaviors,
|
behaviors,
|
||||||
VizPanelState,
|
VizPanelState,
|
||||||
|
SceneGridItemLike,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||||
import { DashboardDTO } from 'app/types';
|
import { DashboardDTO } from 'app/types';
|
||||||
@ -32,6 +33,7 @@ import { DashboardDTO } from 'app/types';
|
|||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||||
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
|
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
|
||||||
|
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
|
||||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||||
import { createPanelDataProvider } from '../utils/createPanelDataProvider';
|
import { createPanelDataProvider } from '../utils/createPanelDataProvider';
|
||||||
import { getVizPanelKeyForPanelId } from '../utils/utils';
|
import { getVizPanelKeyForPanelId } from '../utils/utils';
|
||||||
@ -51,14 +53,14 @@ export function transformSaveModelToScene(rsp: DashboardDTO): DashboardScene {
|
|||||||
return createDashboardSceneFromDashboardModel(oldModel);
|
return createDashboardSceneFromDashboardModel(oldModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSceneObjectsForPanels(oldPanels: PanelModel[]): Array<SceneGridItem | SceneGridRow> {
|
export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridItemLike[] {
|
||||||
// collects all panels and rows
|
// collects all panels and rows
|
||||||
const panels: Array<SceneGridItem | SceneGridRow> = [];
|
const panels: SceneGridItemLike[] = [];
|
||||||
|
|
||||||
// indicates expanded row that's currently processed
|
// indicates expanded row that's currently processed
|
||||||
let currentRow: PanelModel | null = null;
|
let currentRow: PanelModel | null = null;
|
||||||
// collects panels in the currently processed, expanded row
|
// collects panels in the currently processed, expanded row
|
||||||
let currentRowPanels: SceneGridItem[] = [];
|
let currentRowPanels: SceneGridItemLike[] = [];
|
||||||
|
|
||||||
for (const panel of oldPanels) {
|
for (const panel of oldPanels) {
|
||||||
if (panel.type === 'row') {
|
if (panel.type === 'row') {
|
||||||
@ -70,7 +72,7 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): Array<Scen
|
|||||||
title: panel.title,
|
title: panel.title,
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
y: panel.gridPos.y,
|
y: panel.gridPos.y,
|
||||||
children: panel.panels ? panel.panels.map(buildSceneFromPanelModel) : [],
|
children: panel.panels ? panel.panels.map(buildGridItemForPanel) : [],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -106,7 +108,7 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): Array<Scen
|
|||||||
});
|
});
|
||||||
panels.push(gridItem);
|
panels.push(gridItem);
|
||||||
} else {
|
} else {
|
||||||
const panelObject = buildSceneFromPanelModel(panel);
|
const panelObject = buildGridItemForPanel(panel);
|
||||||
|
|
||||||
// when processing an expanded row, collect its panels
|
// when processing an expanded row, collect its panels
|
||||||
if (currentRow) {
|
if (currentRow) {
|
||||||
@ -246,7 +248,7 @@ export function createSceneVariableFromVariableModel(variable: VariableModel): S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSceneFromPanelModel(panel: PanelModel): SceneGridItem {
|
export function buildGridItemForPanel(panel: PanelModel): SceneGridItemLike {
|
||||||
const vizPanelState: VizPanelState = {
|
const vizPanelState: VizPanelState = {
|
||||||
key: getVizPanelKeyForPanelId(panel.id),
|
key: getVizPanelKeyForPanelId(panel.id),
|
||||||
title: panel.title,
|
title: panel.title,
|
||||||
@ -271,6 +273,24 @@ export function buildSceneFromPanelModel(panel: PanelModel): SceneGridItem {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (panel.repeat) {
|
||||||
|
const repeatDirection = panel.repeatDirection ?? 'h';
|
||||||
|
|
||||||
|
return new PanelRepeaterGridItem({
|
||||||
|
key: `grid-item-${panel.id}`,
|
||||||
|
x: panel.gridPos.x,
|
||||||
|
y: panel.gridPos.y,
|
||||||
|
width: repeatDirection === 'h' ? 24 : panel.gridPos.w,
|
||||||
|
height: panel.gridPos.h,
|
||||||
|
itemHeight: panel.gridPos.h,
|
||||||
|
source: new VizPanel(vizPanelState),
|
||||||
|
variableName: panel.repeat,
|
||||||
|
repeatedPanels: [],
|
||||||
|
repeatDirection: panel.repeatDirection,
|
||||||
|
maxPerRow: panel.maxPerRow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return new SceneGridItem({
|
return new SceneGridItem({
|
||||||
key: `grid-item-${panel.id}`,
|
key: `grid-item-${panel.id}`,
|
||||||
x: panel.gridPos.x,
|
x: panel.gridPos.x,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { SceneGridItem } from '@grafana/scenes';
|
import { SceneGridItemLike } from '@grafana/scenes';
|
||||||
import { Panel } from '@grafana/schema';
|
import { Panel } from '@grafana/schema';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
|
|
||||||
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
|
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
|
||||||
import { buildSceneFromPanelModel, transformSaveModelToScene } from './transformSaveModelToScene';
|
import { buildGridItemForPanel, transformSaveModelToScene } from './transformSaveModelToScene';
|
||||||
import { gridItemToPanel, transformSceneToSaveModel } from './transformSceneToSaveModel';
|
import { gridItemToPanel, transformSceneToSaveModel } from './transformSceneToSaveModel';
|
||||||
|
|
||||||
describe('transformSceneToSaveModel', () => {
|
describe('transformSceneToSaveModel', () => {
|
||||||
@ -18,7 +18,7 @@ describe('transformSceneToSaveModel', () => {
|
|||||||
|
|
||||||
describe('Panel options', () => {
|
describe('Panel options', () => {
|
||||||
it('Given panel with time override', () => {
|
it('Given panel with time override', () => {
|
||||||
const gridItem = createVizPanelFromPanelSchema({
|
const gridItem = buildGridItemFromPanelSchema({
|
||||||
timeFrom: '2h',
|
timeFrom: '2h',
|
||||||
timeShift: '1d',
|
timeShift: '1d',
|
||||||
hideTimeOverride: true,
|
hideTimeOverride: true,
|
||||||
@ -31,14 +31,34 @@ describe('transformSceneToSaveModel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('transparent panel', () => {
|
it('transparent panel', () => {
|
||||||
const gridItem = createVizPanelFromPanelSchema({ transparent: true });
|
const gridItem = buildGridItemFromPanelSchema({ transparent: true });
|
||||||
const saveModel = gridItemToPanel(gridItem);
|
const saveModel = gridItemToPanel(gridItem);
|
||||||
|
|
||||||
expect(saveModel.transparent).toBe(true);
|
expect(saveModel.transparent).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Given panel with repeat', () => {
|
||||||
|
const gridItem = buildGridItemFromPanelSchema({
|
||||||
|
title: '',
|
||||||
|
type: 'text-plugin-34',
|
||||||
|
gridPos: { x: 1, y: 2, w: 12, h: 8 },
|
||||||
|
repeat: 'server',
|
||||||
|
repeatDirection: 'v',
|
||||||
|
maxPerRow: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveModel = gridItemToPanel(gridItem);
|
||||||
|
expect(saveModel.repeat).toBe('server');
|
||||||
|
expect(saveModel.repeatDirection).toBe('v');
|
||||||
|
expect(saveModel.maxPerRow).toBe(8);
|
||||||
|
expect(saveModel.gridPos?.x).toBe(1);
|
||||||
|
expect(saveModel.gridPos?.y).toBe(2);
|
||||||
|
expect(saveModel.gridPos?.w).toBe(12);
|
||||||
|
expect(saveModel.gridPos?.h).toBe(8);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export function createVizPanelFromPanelSchema(panel: Partial<Panel>): SceneGridItem {
|
export function buildGridItemFromPanelSchema(panel: Partial<Panel>): SceneGridItemLike {
|
||||||
return buildSceneFromPanelModel(new PanelModel(panel));
|
return buildGridItemForPanel(new PanelModel(panel));
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes';
|
import { SceneGridItem, SceneGridItemLike, SceneGridLayout, VizPanel } from '@grafana/scenes';
|
||||||
import { Dashboard, defaultDashboard, FieldConfigSource, Panel } from '@grafana/schema';
|
import { Dashboard, defaultDashboard, FieldConfigSource, Panel } from '@grafana/schema';
|
||||||
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
|
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
|
||||||
|
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
|
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
|
||||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||||
import { getPanelIdForVizPanel } from '../utils/utils';
|
import { getPanelIdForVizPanel } from '../utils/utils';
|
||||||
|
|
||||||
@ -34,22 +35,43 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
|
|||||||
return sortedDeepCloneWithoutNulls(dashboard);
|
return sortedDeepCloneWithoutNulls(dashboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gridItemToPanel(gridItem: SceneGridItem): Panel {
|
export function gridItemToPanel(gridItem: SceneGridItemLike): Panel {
|
||||||
const vizPanel = gridItem.state.body;
|
let vizPanel: VizPanel | undefined;
|
||||||
if (!(vizPanel instanceof VizPanel)) {
|
let x = 0,
|
||||||
throw new Error('SceneGridItem body expected to be VizPanel');
|
y = 0,
|
||||||
|
w = 0,
|
||||||
|
h = 0;
|
||||||
|
|
||||||
|
if (gridItem instanceof SceneGridItem) {
|
||||||
|
if (!(gridItem.state.body instanceof VizPanel)) {
|
||||||
|
throw new Error('SceneGridItem body expected to be VizPanel');
|
||||||
|
}
|
||||||
|
|
||||||
|
vizPanel = gridItem.state.body;
|
||||||
|
x = gridItem.state.x ?? 0;
|
||||||
|
y = gridItem.state.y ?? 0;
|
||||||
|
w = gridItem.state.width ?? 0;
|
||||||
|
h = gridItem.state.height ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gridItem instanceof PanelRepeaterGridItem) {
|
||||||
|
vizPanel = gridItem.state.source;
|
||||||
|
|
||||||
|
x = gridItem.state.x ?? 0;
|
||||||
|
y = gridItem.state.y ?? 0;
|
||||||
|
w = gridItem.state.width ?? 0;
|
||||||
|
h = gridItem.state.height ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vizPanel) {
|
||||||
|
throw new Error('Unsupported grid item type');
|
||||||
}
|
}
|
||||||
|
|
||||||
const panel: Panel = {
|
const panel: Panel = {
|
||||||
id: getPanelIdForVizPanel(vizPanel),
|
id: getPanelIdForVizPanel(vizPanel),
|
||||||
type: vizPanel.state.pluginId,
|
type: vizPanel.state.pluginId,
|
||||||
title: vizPanel.state.title,
|
title: vizPanel.state.title,
|
||||||
gridPos: {
|
gridPos: { x, y, w, h },
|
||||||
x: gridItem.state.x ?? 0,
|
|
||||||
y: gridItem.state.y ?? 0,
|
|
||||||
w: gridItem.state.width ?? 0,
|
|
||||||
h: gridItem.state.height ?? 0,
|
|
||||||
},
|
|
||||||
options: vizPanel.state.options,
|
options: vizPanel.state.options,
|
||||||
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
|
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
|
||||||
transformations: [],
|
transformations: [],
|
||||||
@ -68,5 +90,11 @@ export function gridItemToPanel(gridItem: SceneGridItem): Panel {
|
|||||||
panel.transparent = true;
|
panel.transparent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gridItem instanceof PanelRepeaterGridItem) {
|
||||||
|
panel.repeat = gridItem.state.variableName;
|
||||||
|
panel.maxPerRow = gridItem.state.maxPerRow;
|
||||||
|
panel.repeatDirection = gridItem.getRepeatDirection();
|
||||||
|
}
|
||||||
|
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DeepPartial } from '@grafana/scenes';
|
import { DeepPartial, SceneDeactivationHandler, SceneObject } from '@grafana/scenes';
|
||||||
import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||||
import { DashboardDTO } from 'app/types';
|
import { DashboardDTO } from 'app/types';
|
||||||
|
|
||||||
@ -38,3 +38,24 @@ export function mockResizeObserver() {
|
|||||||
unobserve() {}
|
unobserve() {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Useful from tests to simulate mounting a full scene. Children are activated before parents to simulate the real order
|
||||||
|
* of React mount order and useEffect ordering.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function activateFullSceneTree(scene: SceneObject): SceneDeactivationHandler {
|
||||||
|
const deactivationHandlers: SceneDeactivationHandler[] = [];
|
||||||
|
|
||||||
|
scene.forEachChild((child) => {
|
||||||
|
deactivationHandlers.push(activateFullSceneTree(child));
|
||||||
|
});
|
||||||
|
|
||||||
|
deactivationHandlers.push(scene.activate());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const handler of deactivationHandlers) {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SceneDeactivationHandler, sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
|
import { sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
|
||||||
|
|
||||||
export function getVizPanelKeyForPanelId(panelId: number) {
|
export function getVizPanelKeyForPanelId(panelId: number) {
|
||||||
return `panel-${panelId}`;
|
return `panel-${panelId}`;
|
||||||
@ -8,43 +8,45 @@ export function getPanelIdForVizPanel(panel: VizPanel): number {
|
|||||||
return parseInt(panel.state.key!.replace('panel-', ''), 10);
|
return parseInt(panel.state.key!.replace('panel-', ''), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findVizPanelById(scene: SceneObject, id: string | undefined): VizPanel | null {
|
/**
|
||||||
if (!id) {
|
* This will also try lookup based on panelId
|
||||||
|
*/
|
||||||
|
export function findVizPanelByKey(scene: SceneObject, key: string | undefined): VizPanel | null {
|
||||||
|
if (!key) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const panelId = parseInt(id, 10);
|
const panel = findVizPanelInternal(scene, key);
|
||||||
const key = getVizPanelKeyForPanelId(panelId);
|
if (panel) {
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
const obj = sceneGraph.findObject(scene, (obj) => obj.state.key === key);
|
// Also try to find by panel id
|
||||||
if (obj instanceof VizPanel) {
|
const id = parseInt(key, 10);
|
||||||
return obj;
|
if (isNaN(id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return findVizPanelInternal(scene, getVizPanelKeyForPanelId(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizPanel | null {
|
||||||
|
if (!key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = sceneGraph.findObject(scene, (obj) => obj.state.key === key);
|
||||||
|
if (panel) {
|
||||||
|
if (panel instanceof VizPanel) {
|
||||||
|
return panel;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Found panel with key ${key} but it was not a VizPanel`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Useful from tests to simulate mounting a full scene. Children are activated before parents to simulate the real order
|
|
||||||
* of React mount order and useEffect ordering.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function activateFullSceneTree(scene: SceneObject): SceneDeactivationHandler {
|
|
||||||
const deactivationHandlers: SceneDeactivationHandler[] = [];
|
|
||||||
|
|
||||||
scene.forEachChild((child) => {
|
|
||||||
deactivationHandlers.push(activateFullSceneTree(child));
|
|
||||||
});
|
|
||||||
|
|
||||||
deactivationHandlers.push(scene.activate());
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
for (const handler of deactivationHandlers) {
|
|
||||||
handler();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force re-render children. This is useful in some edge case scenarios when
|
* Force re-render children. This is useful in some edge case scenarios when
|
||||||
* children deep down the scene graph needs to be re-rendered when some parent state change.
|
* children deep down the scene graph needs to be re-rendered when some parent state change.
|
||||||
|
@ -1090,10 +1090,13 @@ describe('DashboardModel', () => {
|
|||||||
panels: [
|
panels: [
|
||||||
{ id: 1, type: 'row', collapsed: false, panels: [], gridPos: { x: 0, y: 0, w: 24, h: 6 } },
|
{ id: 1, type: 'row', collapsed: false, panels: [], gridPos: { x: 0, y: 0, w: 24, h: 6 } },
|
||||||
{ id: 2, type: 'graph', gridPos: { x: 0, y: 7, w: 12, h: 2 } },
|
{ id: 2, type: 'graph', gridPos: { x: 0, y: 7, w: 12, h: 2 } },
|
||||||
{ id: 3, type: 'graph', gridPos: { x: 0, y: 7, w: 12, h: 2 }, repeatPanelId: 2 },
|
{ id: 3, type: 'graph', gridPos: { x: 0, y: 7, w: 12, h: 2 } },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const panel = dashboard.getPanelById(3);
|
const panel = dashboard.getPanelById(3);
|
||||||
|
panel!.repeatPanelId = 1;
|
||||||
|
|
||||||
expect(dashboard.canEditPanel(panel)).toBe(false);
|
expect(dashboard.canEditPanel(panel)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { getGridWithMultipleTimeRanges } from './gridMultiTimeRange';
|
|||||||
import { getMultipleGridLayoutTest } from './gridMultiple';
|
import { getMultipleGridLayoutTest } from './gridMultiple';
|
||||||
import { getGridWithMultipleData } from './gridWithMultipleData';
|
import { getGridWithMultipleData } from './gridWithMultipleData';
|
||||||
import { getQueryVariableDemo } from './queryVariableDemo';
|
import { getQueryVariableDemo } from './queryVariableDemo';
|
||||||
|
import { getRepeatingPanelsDemo } from './repeatingPanels';
|
||||||
import { getSceneWithRows } from './sceneWithRows';
|
import { getSceneWithRows } from './sceneWithRows';
|
||||||
import { getTransformationsDemo } from './transformations';
|
import { getTransformationsDemo } from './transformations';
|
||||||
import { getVariablesDemo, getVariablesDemoWithAll } from './variablesDemo';
|
import { getVariablesDemo, getVariablesDemoWithAll } from './variablesDemo';
|
||||||
@ -20,6 +21,7 @@ export function getScenes(): SceneDef[] {
|
|||||||
{ title: 'Multiple grid layouts test', getScene: getMultipleGridLayoutTest },
|
{ title: 'Multiple grid layouts test', getScene: getMultipleGridLayoutTest },
|
||||||
{ title: 'Variables', getScene: getVariablesDemo },
|
{ title: 'Variables', getScene: getVariablesDemo },
|
||||||
{ title: 'Variables with All values', getScene: getVariablesDemoWithAll },
|
{ title: 'Variables with All values', getScene: getVariablesDemoWithAll },
|
||||||
|
{ title: 'Variables - Repeating panels', getScene: getRepeatingPanelsDemo },
|
||||||
{ title: 'Query variable', getScene: getQueryVariableDemo },
|
{ title: 'Query variable', getScene: getQueryVariableDemo },
|
||||||
{ title: 'Transformations demo', getScene: getTransformationsDemo },
|
{ title: 'Transformations demo', getScene: getTransformationsDemo },
|
||||||
];
|
];
|
||||||
|
108
public/app/features/scenes/scenes/repeatingPanels.tsx
Normal file
108
public/app/features/scenes/scenes/repeatingPanels.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import {
|
||||||
|
SceneTimePicker,
|
||||||
|
SceneTimeRange,
|
||||||
|
VariableValueSelectors,
|
||||||
|
SceneVariableSet,
|
||||||
|
TestVariable,
|
||||||
|
SceneRefreshPicker,
|
||||||
|
PanelBuilders,
|
||||||
|
SceneGridLayout,
|
||||||
|
SceneControlsSpacer,
|
||||||
|
} from '@grafana/scenes';
|
||||||
|
import { VariableRefresh } from '@grafana/schema';
|
||||||
|
import { PanelRepeaterGridItem } from 'app/features/dashboard-scene/scene/PanelRepeaterGridItem';
|
||||||
|
|
||||||
|
import { DashboardScene } from '../../dashboard-scene/scene/DashboardScene';
|
||||||
|
|
||||||
|
import { getQueryRunnerWithRandomWalkQuery } from './queries';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repeat panels by variable that changes with time refresh. This tries to setup a very specific scenario
|
||||||
|
* where a variable that is slow (2s) and constantly changing it's result is used to repeat panels. This
|
||||||
|
* can be used to verify that when the time range change the repeated panels with locally scoped variable value
|
||||||
|
* still wait for the top level variable to finish loading and the repeat process to complete.
|
||||||
|
*/
|
||||||
|
export function getRepeatingPanelsDemo(): DashboardScene {
|
||||||
|
return new DashboardScene({
|
||||||
|
title: 'Variables - Repeating panels',
|
||||||
|
$variables: new SceneVariableSet({
|
||||||
|
variables: [
|
||||||
|
new TestVariable({
|
||||||
|
name: 'server',
|
||||||
|
query: 'AB',
|
||||||
|
value: 'server',
|
||||||
|
text: '',
|
||||||
|
delayMs: 2000,
|
||||||
|
isMulti: true,
|
||||||
|
includeAll: true,
|
||||||
|
refresh: VariableRefresh.onTimeRangeChanged,
|
||||||
|
optionsToReturn: [
|
||||||
|
{ label: 'A', value: 'A' },
|
||||||
|
{ label: 'B', value: 'C' },
|
||||||
|
],
|
||||||
|
options: [],
|
||||||
|
$behaviors: [changeVariable],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
body: new SceneGridLayout({
|
||||||
|
isDraggable: true,
|
||||||
|
isResizable: true,
|
||||||
|
children: [
|
||||||
|
new PanelRepeaterGridItem({
|
||||||
|
variableName: 'server',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 24,
|
||||||
|
height: 8,
|
||||||
|
itemHeight: 8,
|
||||||
|
//@ts-expect-error
|
||||||
|
source: PanelBuilders.timeseries()
|
||||||
|
.setTitle('server = $server')
|
||||||
|
.setData(getQueryRunnerWithRandomWalkQuery({ alias: 'server = $server' }))
|
||||||
|
.build(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
$timeRange: new SceneTimeRange(),
|
||||||
|
actions: [],
|
||||||
|
controls: [
|
||||||
|
new VariableValueSelectors({}),
|
||||||
|
new SceneControlsSpacer(),
|
||||||
|
new SceneTimePicker({}),
|
||||||
|
new SceneRefreshPicker({}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
sub.unsubscribe();
|
||||||
|
};
|
||||||
|
}
|
@ -59,6 +59,9 @@
|
|||||||
transform: translate(0px, 0px) !important;
|
transform: translate(0px, 0px) !important;
|
||||||
margin-bottom: $space-md;
|
margin-bottom: $space-md;
|
||||||
}
|
}
|
||||||
|
.panel-repeater-grid-item {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-grid-item.react-grid-placeholder {
|
.react-grid-item.react-grid-placeholder {
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -3939,9 +3939,9 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@grafana/scenes@npm:^0.27.0":
|
"@grafana/scenes@npm:^0.29.0":
|
||||||
version: 0.27.0
|
version: 0.29.0
|
||||||
resolution: "@grafana/scenes@npm:0.27.0"
|
resolution: "@grafana/scenes@npm:0.29.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@grafana/e2e-selectors": 10.0.2
|
"@grafana/e2e-selectors": 10.0.2
|
||||||
react-grid-layout: 1.3.4
|
react-grid-layout: 1.3.4
|
||||||
@ -3953,7 +3953,7 @@ __metadata:
|
|||||||
"@grafana/runtime": 10.0.3
|
"@grafana/runtime": 10.0.3
|
||||||
"@grafana/schema": 10.0.3
|
"@grafana/schema": 10.0.3
|
||||||
"@grafana/ui": 10.0.3
|
"@grafana/ui": 10.0.3
|
||||||
checksum: 71b2ea13c6afca0d8d101e9a7d945ebb181ad2acbeb6fa5ca4018a34332a9b1e09a434feea9080665327d3d30a7e4d2542a7491a2f68a944717d56ba014fba25
|
checksum: 8a91ea0290d54c5c081595e85f853b14af90468da3d85b5cd83e26d24d4fc84cceea9be930aa9239439ff3af7388ae3f5bebe1973214c686f4cf143a64752548
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -19229,7 +19229,7 @@ __metadata:
|
|||||||
"@grafana/lezer-traceql": 0.0.4
|
"@grafana/lezer-traceql": 0.0.4
|
||||||
"@grafana/monaco-logql": ^0.0.7
|
"@grafana/monaco-logql": ^0.0.7
|
||||||
"@grafana/runtime": "workspace:*"
|
"@grafana/runtime": "workspace:*"
|
||||||
"@grafana/scenes": ^0.27.0
|
"@grafana/scenes": ^0.29.0
|
||||||
"@grafana/schema": "workspace:*"
|
"@grafana/schema": "workspace:*"
|
||||||
"@grafana/tsconfig": ^1.3.0-rc1
|
"@grafana/tsconfig": ^1.3.0-rc1
|
||||||
"@grafana/ui": "workspace:*"
|
"@grafana/ui": "workspace:*"
|
||||||
|
Loading…
Reference in New Issue
Block a user