mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Share snapshot (#76132)
* Dashboard snapshot creation logic and tests * Add variable type to serialized json, add refresh prop to datasource var * WIP Snapshots tab UI * Use Grafana snapshot query for a generated snapshot * Make anno snapshots backwards compatible * Share snapshot tab UI * Single panel snapshot * Remove unused param * Snap update * Ts fix * One more snap fix * Basic rows support * Add tests for basic rows * Bettererupdate
This commit is contained in:
parent
555acdf180
commit
6968f4d6ff
@ -3009,7 +3009,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
|
@ -61,7 +61,7 @@ export function getPanelPlugin(
|
||||
},
|
||||
screenshots: [],
|
||||
updated: '',
|
||||
version: '',
|
||||
version: '1',
|
||||
},
|
||||
hideFromList: options.hideFromList === true,
|
||||
module: options.module ?? '',
|
||||
|
@ -36,6 +36,9 @@ export interface AnnotationQuery<TQuery extends DataQuery = DataQuery>
|
||||
extends Omit<raw.AnnotationQuery, 'target' | 'datasource'> {
|
||||
datasource?: DataSourceRef | null;
|
||||
target?: TQuery;
|
||||
// TODO: When migrating to snapshot queries, remove this property.
|
||||
// With snapshot queries annotations become a part of the panel snapshot data.
|
||||
snapshotData?: unknown;
|
||||
}
|
||||
|
||||
export interface AnnotationContainer extends Omit<raw.AnnotationContainer, 'list'> {
|
||||
|
@ -209,6 +209,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
|
||||
"name": "server",
|
||||
"options": [],
|
||||
"query": "A,B,C,D,E,F,E,G,H,I,J,K,L",
|
||||
"type": "custom",
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
@ -226,6 +227,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
|
||||
"name": "pod",
|
||||
"options": [],
|
||||
"query": "Bob : 1, Rob : 2,Sod : 3, Hod : 4, Cod : 5",
|
||||
"type": "custom",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -0,0 +1,4 @@
|
||||
export const GRAFANA_DATASOURCE_REF = {
|
||||
name: 'grafana',
|
||||
uid: 'grafana',
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import { SceneDataLayerProvider, dataLayers } from '@grafana/scenes';
|
||||
import { AnnotationQuery } from '@grafana/schema';
|
||||
|
||||
export function dataLayersToAnnotations(layers: SceneDataLayerProvider[]) {
|
||||
const annotations: AnnotationQuery[] = [];
|
||||
for (const layer of layers) {
|
||||
if (!(layer instanceof dataLayers.AnnotationsDataLayer)) {
|
||||
continue;
|
||||
}
|
||||
const result = {
|
||||
...layer.state.query,
|
||||
enable: Boolean(layer.state.isEnabled),
|
||||
hide: Boolean(layer.state.isHidden),
|
||||
};
|
||||
|
||||
annotations.push(result);
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
@ -122,6 +122,7 @@ describe('sceneVariablesSetToVariables', () => {
|
||||
"query": "query",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"type": "query",
|
||||
}
|
||||
`);
|
||||
});
|
||||
@ -165,7 +166,9 @@ describe('sceneVariablesSetToVariables', () => {
|
||||
"name": "test",
|
||||
"options": [],
|
||||
"query": "fake-std",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"type": "datasource",
|
||||
}
|
||||
`);
|
||||
});
|
||||
@ -214,6 +217,7 @@ describe('sceneVariablesSetToVariables', () => {
|
||||
"name": "test",
|
||||
"options": [],
|
||||
"query": "test,test1,test2",
|
||||
"type": "custom",
|
||||
}
|
||||
`);
|
||||
});
|
||||
@ -245,6 +249,7 @@ describe('sceneVariablesSetToVariables', () => {
|
||||
"name": "test",
|
||||
"query": "constant value",
|
||||
"skipUrlSync": true,
|
||||
"type": "constant",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { SceneVariableSet, QueryVariable, CustomVariable, DataSourceVariable, ConstantVariable } from '@grafana/scenes';
|
||||
import { VariableModel, VariableHide, VariableSort } from '@grafana/schema';
|
||||
import { VariableModel, VariableHide, VariableRefresh, VariableSort } from '@grafana/schema';
|
||||
|
||||
export function sceneVariablesSetToVariables(set: SceneVariableSet) {
|
||||
const variables: VariableModel[] = [];
|
||||
@ -10,6 +10,7 @@ export function sceneVariablesSetToVariables(set: SceneVariableSet) {
|
||||
description: variable.state.description,
|
||||
skipUrlSync: Boolean(variable.state.skipUrlSync),
|
||||
hide: variable.state.hide || VariableHide.dontHide,
|
||||
type: variable.state.type,
|
||||
};
|
||||
if (variable instanceof QueryVariable) {
|
||||
variables.push({
|
||||
@ -58,6 +59,7 @@ export function sceneVariablesSetToVariables(set: SceneVariableSet) {
|
||||
},
|
||||
options: [],
|
||||
regex: variable.state.regex,
|
||||
refresh: VariableRefresh.onDashboardLoad,
|
||||
query: variable.state.pluginId,
|
||||
multi: variable.state.isMulti,
|
||||
allValue: variable.state.allValue,
|
||||
|
@ -0,0 +1,360 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"enable": true,
|
||||
"iconColor": "red",
|
||||
"name": "New annotation",
|
||||
"target": {
|
||||
"lines": 10,
|
||||
"refId": "Anno1111",
|
||||
"scenarioId": "annotations"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"asDropdown": false,
|
||||
"icon": "external link",
|
||||
"includeVars": false,
|
||||
"keepTime": false,
|
||||
"tags": [],
|
||||
"targetBlank": false,
|
||||
"title": "New link",
|
||||
"tooltip": "",
|
||||
"type": "link",
|
||||
"url": "https://test.link.co"
|
||||
}
|
||||
],
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 2306,
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"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": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"seriesCount": 1
|
||||
}
|
||||
],
|
||||
"title": "Panel 1",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "10.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk",
|
||||
"seriesCount": 1
|
||||
}
|
||||
],
|
||||
"title": "Panel 2",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "reduce",
|
||||
"options": {}
|
||||
}
|
||||
],
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"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": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel 3",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"schemaVersion": 38,
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": "annotations",
|
||||
"value": "annotations"
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "gdev-prometheus"
|
||||
},
|
||||
"definition": "label_values(backend)",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"multi": false,
|
||||
"name": "query0",
|
||||
"options": [],
|
||||
"query": {
|
||||
"qryType": 1,
|
||||
"query": "label_values(backend)",
|
||||
"refId": "VariableQuery"
|
||||
},
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Snapshots repro",
|
||||
"uid": "d6657f14-9c6a-4eaa-9593-54c9c46b6394",
|
||||
"version": 4,
|
||||
"weekStart": ""
|
||||
}
|
@ -0,0 +1,425 @@
|
||||
{
|
||||
"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,
|
||||
"id": 2312,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"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": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"seriesCount": 1
|
||||
}
|
||||
],
|
||||
"title": "Panel 1",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 2,
|
||||
"panels": [],
|
||||
"title": "Expanded row",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"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": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 9
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk",
|
||||
"seriesCount": 1
|
||||
}
|
||||
],
|
||||
"title": "Panel 2",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"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": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 9
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk",
|
||||
"seriesCount": 1
|
||||
}
|
||||
],
|
||||
"title": "Panel 3",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 17
|
||||
},
|
||||
"id": 4,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 10
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"title": "Panel inside colapsed row",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"title": "Collapsed row",
|
||||
"type": "row"
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"schemaVersion": 38,
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Snapshots - basic rows",
|
||||
"uid": "a3b23921-287d-4f90-9a14-c04228057dfa",
|
||||
"version": 4,
|
||||
"weekStart": ""
|
||||
}
|
@ -1,3 +1,20 @@
|
||||
import { advanceTo } from 'jest-date-mock';
|
||||
import { map, of } from 'rxjs';
|
||||
|
||||
import {
|
||||
DataFrame,
|
||||
DataQueryRequest,
|
||||
DataSourceApi,
|
||||
dateTime,
|
||||
FieldType,
|
||||
PanelData,
|
||||
standardTransformersRegistry,
|
||||
StandardVariableQuery,
|
||||
toDataFrame,
|
||||
VariableSupportType,
|
||||
} from '@grafana/data';
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { setPluginImportUtils } from '@grafana/runtime';
|
||||
import {
|
||||
MultiValueVariable,
|
||||
SceneDataLayers,
|
||||
@ -5,22 +22,121 @@ import {
|
||||
SceneGridLayout,
|
||||
SceneGridRow,
|
||||
SceneVariable,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { Panel, RowPanel } from '@grafana/schema';
|
||||
import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
||||
import { reduceTransformRegistryItem } from 'app/features/transformers/editors/ReduceTransformerEditor';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
|
||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
import { activateFullSceneTree } from '../utils/test-utils';
|
||||
import { getVizPanelKeyForPanelId } from '../utils/utils';
|
||||
|
||||
import { GRAFANA_DATASOURCE_REF } from './const';
|
||||
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
|
||||
import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json';
|
||||
import snapshotableDashboardJson from './testfiles/snapshotable_dashboard.json';
|
||||
import snapshotableWithRowsDashboardJson from './testfiles/snapshotable_with_rows.json';
|
||||
import {
|
||||
buildGridItemForLibPanel,
|
||||
buildGridItemForPanel,
|
||||
transformSaveModelToScene,
|
||||
} from './transformSaveModelToScene';
|
||||
import { gridItemToPanel, transformSceneToSaveModel } from './transformSceneToSaveModel';
|
||||
import { gridItemToPanel, transformSceneToSaveModel, trimDashboardForSnapshot } from './transformSceneToSaveModel';
|
||||
|
||||
standardTransformersRegistry.setInit(() => [reduceTransformRegistryItem]);
|
||||
setPluginImportUtils({
|
||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||
getPanelPluginFromCache: (id: string) => undefined,
|
||||
});
|
||||
|
||||
const AFrame = toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||
{ name: 'values', type: FieldType.number, values: [1, 2, 3] },
|
||||
],
|
||||
});
|
||||
|
||||
const BFrame = toDataFrame({
|
||||
name: 'B',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||
{ name: 'values', type: FieldType.number, values: [10, 20, 30] },
|
||||
],
|
||||
});
|
||||
|
||||
const CFrame = toDataFrame({
|
||||
name: 'C',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000] },
|
||||
{ name: 'values', type: FieldType.number, values: [100, 200, 300] },
|
||||
],
|
||||
});
|
||||
|
||||
const AnnoFrame = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', values: [1, 2, 2, 5, 5] },
|
||||
{ name: 'id', values: ['1', '2', '2', '5', '5'] },
|
||||
{ name: 'text', values: ['t1', 't2', 't3', 't4', 't5'] },
|
||||
],
|
||||
});
|
||||
|
||||
const VariableQueryFrame = toDataFrame({
|
||||
fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }],
|
||||
});
|
||||
|
||||
const testSeries: Record<string, DataFrame> = {
|
||||
A: AFrame,
|
||||
B: BFrame,
|
||||
C: CFrame,
|
||||
Anno: AnnoFrame,
|
||||
VariableQuery: VariableQueryFrame,
|
||||
};
|
||||
|
||||
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
|
||||
const result: PanelData = {
|
||||
state: LoadingState.Loading,
|
||||
series: [],
|
||||
timeRange: request.range,
|
||||
};
|
||||
|
||||
return of([]).pipe(
|
||||
map(() => {
|
||||
result.state = LoadingState.Done;
|
||||
|
||||
const refId = request.targets[0].refId;
|
||||
result.series = [testSeries[refId]];
|
||||
|
||||
return result;
|
||||
})
|
||||
);
|
||||
});
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: () => ({
|
||||
get: () => ({
|
||||
getRef: () => ({ type: 'mock-ds', uid: 'mock-uid' }),
|
||||
variables: {
|
||||
getType: () => VariableSupportType.Standard,
|
||||
toDataQuery: (q: StandardVariableQuery) => q,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
||||
return runRequestMock(ds, request);
|
||||
},
|
||||
config: {
|
||||
panels: [],
|
||||
theme2: {
|
||||
visualization: {
|
||||
getColorByName: jest.fn().mockReturnValue('red'),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
describe('transformSceneToSaveModel', () => {
|
||||
describe('Given a simple scene', () => {
|
||||
it('Should transform back to peristed model', () => {
|
||||
@ -360,6 +476,206 @@ describe('transformSceneToSaveModel', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snapshots', () => {
|
||||
const fakeCurrentDate = dateTime('2023-01-01T20:00:00.000Z').toDate();
|
||||
|
||||
beforeEach(() => {
|
||||
advanceTo(fakeCurrentDate);
|
||||
});
|
||||
|
||||
it('attaches snapshot data to panels using Grafana snapshot query', async () => {
|
||||
const scene = transformSaveModelToScene({ dashboard: snapshotableDashboardJson as any, meta: {} });
|
||||
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
const snapshot = transformSceneToSaveModel(scene, true);
|
||||
|
||||
expect(snapshot.panels?.length).toBe(3);
|
||||
|
||||
// Regular panel with SceneQueryRunner
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[0].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[0].targets?.[0].snapshot[0].data).toEqual({
|
||||
values: [
|
||||
[100, 200, 300],
|
||||
[1, 2, 3],
|
||||
],
|
||||
});
|
||||
|
||||
// Panel with transformations
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[1].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[1].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[1].targets?.[0].snapshot[0].data).toEqual({
|
||||
values: [
|
||||
[100, 200, 300],
|
||||
[10, 20, 30],
|
||||
],
|
||||
});
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[1].transformations).toEqual([
|
||||
{
|
||||
id: 'reduce',
|
||||
options: {},
|
||||
},
|
||||
]);
|
||||
|
||||
// Panel with a shared query (dahsboard query)
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[2].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[2].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[2].targets?.[0].snapshot[0].data).toEqual({
|
||||
values: [
|
||||
[100, 200, 300],
|
||||
[1, 2, 3],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles basic rows', async () => {
|
||||
const scene = transformSaveModelToScene({ dashboard: snapshotableWithRowsDashboardJson as any, meta: {} });
|
||||
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
const snapshot = transformSceneToSaveModel(scene, true);
|
||||
|
||||
expect(snapshot.panels?.length).toBe(5);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[0].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[0].targets?.[0].snapshot[0].data).toEqual({
|
||||
values: [
|
||||
[100, 200, 300],
|
||||
[1, 2, 3],
|
||||
],
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[1].targets).toBeUndefined();
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[1].panels).toEqual([]);
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[1].collapsed).toEqual(false);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[2].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[2].targets?.[0].snapshot[0].data).toEqual({
|
||||
values: [
|
||||
[100, 200, 300],
|
||||
[10, 20, 30],
|
||||
],
|
||||
});
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[3].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[3].targets?.[0].snapshot[0].data).toEqual({
|
||||
values: [
|
||||
[1000, 2000, 3000],
|
||||
[100, 200, 300],
|
||||
],
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[4].targets).toBeUndefined();
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[4].panels).toHaveLength(1);
|
||||
// @ts-expect-error
|
||||
expect(snapshot.panels?.[4].collapsed).toEqual(true);
|
||||
});
|
||||
|
||||
describe('trimDashboardForSnapshot', () => {
|
||||
let snapshot: Dashboard = {} as Dashboard;
|
||||
|
||||
beforeEach(() => {
|
||||
const scene = transformSaveModelToScene({ dashboard: snapshotableDashboardJson as any, meta: {} });
|
||||
activateFullSceneTree(scene);
|
||||
snapshot = transformSceneToSaveModel(scene, true);
|
||||
});
|
||||
|
||||
it('should not mutate provided dashboard', () => {
|
||||
const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
||||
expect(result).not.toBe(snapshot);
|
||||
});
|
||||
|
||||
it('should apply provided title and absolute time range', async () => {
|
||||
const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
||||
|
||||
expect(result.title).toBe('Snap title');
|
||||
expect(result.time).toBeDefined();
|
||||
expect(result.time!.from).toEqual('2023-01-01T14:00:00.000Z');
|
||||
expect(result.time!.to).toEqual('2023-01-01T20:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should remove queries from annotations and attach empty snapshotData', () => {
|
||||
expect(snapshot.annotations?.list?.[0].target).toBeDefined();
|
||||
expect(snapshot.annotations?.list?.[1].target).toBeDefined();
|
||||
|
||||
const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
||||
|
||||
expect(result.annotations?.list?.length).toBe(2);
|
||||
expect(result.annotations?.list?.[0].target).toBeUndefined();
|
||||
expect(result.annotations?.list?.[0].snapshotData).toEqual([]);
|
||||
expect(result.annotations?.list?.[1].target).toBeUndefined();
|
||||
expect(result.annotations?.list?.[1].snapshotData).toEqual([]);
|
||||
});
|
||||
it('should remove queries from variables', () => {
|
||||
expect(snapshot.templating?.list?.length).toBe(1);
|
||||
|
||||
const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
||||
|
||||
expect(result.templating?.list?.length).toBe(1);
|
||||
expect(result.templating?.list?.[0].query).toBe('');
|
||||
expect(result.templating?.list?.[0].refresh).toBe(VariableRefresh.never);
|
||||
expect(result.templating?.list?.[0].options).toHaveLength(1);
|
||||
expect(result.templating?.list?.[0].options?.[0]).toEqual({
|
||||
text: 'annotations',
|
||||
value: 'annotations',
|
||||
});
|
||||
});
|
||||
|
||||
it('should snapshot a single panel when provided', () => {
|
||||
const vizPanel = new VizPanel({
|
||||
key: getVizPanelKeyForPanelId(2),
|
||||
});
|
||||
|
||||
const result = trimDashboardForSnapshot(
|
||||
'Snap title',
|
||||
getTimeRange({ from: 'now-6h', to: 'now' }),
|
||||
snapshot,
|
||||
vizPanel
|
||||
);
|
||||
|
||||
expect(snapshot.panels?.length).toBe(3);
|
||||
expect(result.panels?.length).toBe(1);
|
||||
// @ts-expect-error
|
||||
expect(result.panels?.[0].gridPos).toEqual({ w: 24, x: 0, y: 0, h: 20 });
|
||||
});
|
||||
|
||||
// TODO: Uncomment when we support links
|
||||
// it('should remove links', async () => {
|
||||
// const scene = transformSaveModelToScene({ dashboard: snapshotableDashboardJson as any, meta: {} });
|
||||
// activateFullSceneTree(scene);
|
||||
// const snapshot = transformSceneToSaveModel(scene, true);
|
||||
// expect(snapshot.links?.length).toBe(1);
|
||||
// const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
||||
// expect(result.links?.length).toBe(0);
|
||||
// });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export function buildGridItemFromPanelSchema(panel: Partial<Panel>): SceneGridItemLike {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { isEmptyObject, TimeRange } from '@grafana/data';
|
||||
import {
|
||||
SceneDataLayers,
|
||||
SceneGridItem,
|
||||
@ -5,8 +6,6 @@ import {
|
||||
SceneGridLayout,
|
||||
SceneGridRow,
|
||||
VizPanel,
|
||||
dataLayers,
|
||||
SceneDataLayerProvider,
|
||||
SceneQueryRunner,
|
||||
SceneDataTransformer,
|
||||
SceneVariableSet,
|
||||
@ -21,9 +20,12 @@ import {
|
||||
Panel,
|
||||
RowPanel,
|
||||
VariableModel,
|
||||
VariableRefresh,
|
||||
} from '@grafana/schema';
|
||||
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
|
||||
import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard/utils';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
@ -33,9 +35,11 @@ import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
import { ShareQueryDataProvider } from '../scene/ShareQueryDataProvider';
|
||||
import { getPanelIdForVizPanel } from '../utils/utils';
|
||||
|
||||
import { GRAFANA_DATASOURCE_REF } from './const';
|
||||
import { dataLayersToAnnotations } from './dataLayersToAnnotations';
|
||||
import { sceneVariablesSetToVariables } from './sceneVariablesSetToVariables';
|
||||
|
||||
export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
|
||||
export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = false): Dashboard {
|
||||
const state = scene.state;
|
||||
const timeRange = state.$timeRange!.state;
|
||||
const data = state.$data;
|
||||
@ -48,7 +52,7 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
|
||||
if (body instanceof SceneGridLayout) {
|
||||
for (const child of body.state.children) {
|
||||
if (child instanceof SceneGridItem) {
|
||||
panels.push(gridItemToPanel(child));
|
||||
panels.push(gridItemToPanel(child, isSnapshot));
|
||||
}
|
||||
|
||||
if (child instanceof SceneGridRow) {
|
||||
@ -56,7 +60,7 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
|
||||
if (child.state.key!.indexOf('-clone-') > 0) {
|
||||
continue;
|
||||
}
|
||||
gridRowToSaveModel(child, panels);
|
||||
gridRowToSaveModel(child, panels, isSnapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,6 +68,7 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
|
||||
let annotations: AnnotationQuery[] = [];
|
||||
if (data instanceof SceneDataLayers) {
|
||||
const layers = data.state.layers;
|
||||
|
||||
annotations = dataLayersToAnnotations(layers);
|
||||
}
|
||||
|
||||
@ -106,7 +111,7 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
|
||||
return sortedDeepCloneWithoutNulls(dashboard);
|
||||
}
|
||||
|
||||
export function gridItemToPanel(gridItem: SceneGridItemLike): Panel {
|
||||
export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false): Panel {
|
||||
let vizPanel: VizPanel | undefined;
|
||||
let x = 0,
|
||||
y = 0,
|
||||
@ -164,7 +169,7 @@ export function gridItemToPanel(gridItem: SceneGridItemLike): Panel {
|
||||
options: vizPanel.state.options,
|
||||
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
|
||||
transformations: [],
|
||||
transparent: false,
|
||||
transparent: vizPanel.state.displayMode === 'transparent',
|
||||
};
|
||||
|
||||
const panelTime = vizPanel.state.$timeRange;
|
||||
@ -227,8 +232,23 @@ export function gridItemToPanel(gridItem: SceneGridItemLike): Panel {
|
||||
panel.transformations = dataProvider.state.transformations as DataTransformerConfig[];
|
||||
}
|
||||
|
||||
if (vizPanel.state.displayMode === 'transparent') {
|
||||
panel.transparent = true;
|
||||
if (dataProvider && isSnapshot) {
|
||||
panel.datasource = GRAFANA_DATASOURCE_REF;
|
||||
|
||||
let data = getPanelDataFrames(dataProvider.state.data);
|
||||
if (dataProvider instanceof SceneDataTransformer) {
|
||||
// For transformations the non-transformed data is snapshoted
|
||||
data = getPanelDataFrames(dataProvider.state.$data!.state.data);
|
||||
}
|
||||
|
||||
panel.targets = [
|
||||
{
|
||||
refId: 'A',
|
||||
datasource: panel.datasource,
|
||||
queryType: GrafanaQueryType.Snapshot,
|
||||
snapshot: data,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (gridItem instanceof PanelRepeaterGridItem) {
|
||||
@ -240,7 +260,7 @@ export function gridItemToPanel(gridItem: SceneGridItemLike): Panel {
|
||||
return panel;
|
||||
}
|
||||
|
||||
export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array<Panel | RowPanel>) {
|
||||
export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array<Panel | RowPanel>, isSnapshot = false) {
|
||||
const rowPanel: RowPanel = {
|
||||
type: 'row',
|
||||
id: getPanelIdForVizPanel(gridRow),
|
||||
@ -265,7 +285,7 @@ export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array<Pan
|
||||
|
||||
panelsArray.push(rowPanel);
|
||||
|
||||
const panelsInsideRow = gridRow.state.children.map(gridItemToPanel);
|
||||
const panelsInsideRow = gridRow.state.children.map((c) => gridItemToPanel(c, isSnapshot));
|
||||
|
||||
if (gridRow.state.isCollapsed) {
|
||||
rowPanel.panels = panelsInsideRow;
|
||||
@ -274,19 +294,75 @@ export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array<Pan
|
||||
}
|
||||
}
|
||||
|
||||
export function dataLayersToAnnotations(layers: SceneDataLayerProvider[]) {
|
||||
const annotations: AnnotationQuery[] = [];
|
||||
for (const layer of layers) {
|
||||
if (!(layer instanceof dataLayers.AnnotationsDataLayer)) {
|
||||
continue;
|
||||
}
|
||||
export function trimDashboardForSnapshot(title: string, time: TimeRange, dash: Dashboard, panel?: VizPanel) {
|
||||
let result = {
|
||||
...dash,
|
||||
title,
|
||||
time: {
|
||||
from: time.from.toISOString(),
|
||||
to: time.to.toISOString(),
|
||||
},
|
||||
links: [],
|
||||
};
|
||||
|
||||
annotations.push({
|
||||
...layer.state.query,
|
||||
enable: Boolean(layer.state.isEnabled),
|
||||
hide: Boolean(layer.state.isHidden),
|
||||
// When VizPanel is present, we are snapshoting a single panel. The rest of the panels is removed from the dashboard,
|
||||
// and the panel is resized to 24x20 grid and placed at the top of the dashboard.
|
||||
if (panel) {
|
||||
// @ts-expect-error Due to legacy panels types. Id is present on such panels too.
|
||||
const singlePanel = dash.panels?.find((p) => p.id === getPanelIdForVizPanel(panel));
|
||||
if (singlePanel) {
|
||||
// @ts-expect-error Due to legacy panels types. Id is present on such panels too.
|
||||
singlePanel.gridPos = { w: 24, x: 0, y: 0, h: 20 };
|
||||
result = {
|
||||
...result,
|
||||
panels: [singlePanel],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Remove links from all panels
|
||||
result.panels?.forEach((panel) => {
|
||||
if ('links' in panel) {
|
||||
panel.links = [];
|
||||
}
|
||||
});
|
||||
|
||||
// Remove annotation queries, attach snapshotData: [] for backwards compatibility
|
||||
if (result.annotations) {
|
||||
const annotations = result.annotations.list?.filter((annotation) => annotation.enable) || [];
|
||||
const trimedAnnotations = annotations.map((annotation) => {
|
||||
return {
|
||||
name: annotation.name,
|
||||
enable: annotation.enable,
|
||||
iconColor: annotation.iconColor,
|
||||
type: annotation.type,
|
||||
// @ts-expect-error
|
||||
builtIn: annotation.builtIn,
|
||||
hide: annotation.hide,
|
||||
// TODO: Remove when we migrate snapshots to snapshot queries.
|
||||
// For now leaving this in here to avoid annotation queries in snapshots.
|
||||
// Annotations per panel are part of the snapshot query, so we don't need to store them here.
|
||||
snapshotData: [],
|
||||
};
|
||||
});
|
||||
|
||||
result.annotations.list = trimedAnnotations;
|
||||
}
|
||||
|
||||
if (result.templating) {
|
||||
result.templating.list?.forEach((variable) => {
|
||||
if ('query' in variable) {
|
||||
variable.query = '';
|
||||
}
|
||||
if ('options' in variable) {
|
||||
variable.options = variable.current && !isEmptyObject(variable.current) ? [variable.current] : [];
|
||||
}
|
||||
|
||||
if ('refresh' in variable) {
|
||||
variable.refresh = VariableRefresh.never;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return annotations;
|
||||
return result;
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
|
||||
}
|
||||
|
||||
if (contextSrv.isSignedIn && config.snapshotEnabled) {
|
||||
tabs.push(new ShareSnapshotTab({ panelRef, modalRef: this.getRef() }));
|
||||
tabs.push(new ShareSnapshotTab({ panelRef, dashboardRef, modalRef: this.getRef() }));
|
||||
}
|
||||
|
||||
this.setState({ tabs });
|
||||
|
@ -1,20 +1,249 @@
|
||||
import React from 'react';
|
||||
import useAsyncFn from 'react-use/lib/useAsyncFn';
|
||||
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes';
|
||||
import { Button, ClipboardButton, Field, Input, Modal, RadioButtonGroup } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { trackDashboardSharingActionPerType } from 'app/features/dashboard/components/ShareModal/analytics';
|
||||
import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { transformSceneToSaveModel, trimDashboardForSnapshot } from '../serialization/transformSceneToSaveModel';
|
||||
|
||||
import { SceneShareTabState } from './types';
|
||||
|
||||
const SNAPSHOTS_API_ENDPOINT = '/api/snapshots';
|
||||
const DEFAULT_EXPIRE_OPTION: SelectableValue<number> = {
|
||||
label: t('share-modal.snapshot.expire-never', `Never`),
|
||||
value: 0,
|
||||
};
|
||||
|
||||
const EXPIRE_OPTIONS = [
|
||||
DEFAULT_EXPIRE_OPTION,
|
||||
{
|
||||
label: t('share-modal.snapshot.expire-hour', `1 Hour`),
|
||||
value: 60 * 60,
|
||||
},
|
||||
{
|
||||
label: t('share-modal.snapshot.expire-day', `1 Day`),
|
||||
value: 60 * 60 * 24,
|
||||
},
|
||||
{
|
||||
label: t('share-modal.snapshot.expire-week', `7 Days`),
|
||||
value: 60 * 60 * 24 * 7,
|
||||
},
|
||||
];
|
||||
|
||||
type SnapshotSharingOptions = {
|
||||
externalEnabled: boolean;
|
||||
externalSnapshotName: string;
|
||||
externalSnapshotURL: string;
|
||||
snapshotEnabled: boolean;
|
||||
};
|
||||
export interface ShareSnapshotTabState extends SceneShareTabState {
|
||||
panelRef?: SceneObjectRef<VizPanel>;
|
||||
dashboardRef: SceneObjectRef<DashboardScene>;
|
||||
snapshotName?: string;
|
||||
selectedExpireOption?: SelectableValue<number>;
|
||||
|
||||
snapshotSharingOptions?: SnapshotSharingOptions;
|
||||
}
|
||||
|
||||
export class ShareSnapshotTab extends SceneObjectBase<ShareSnapshotTabState> {
|
||||
static Component = ShareSnapshoTabRenderer;
|
||||
|
||||
public constructor(state: ShareSnapshotTabState) {
|
||||
super({
|
||||
...state,
|
||||
snapshotName: state.dashboardRef.resolve().state.title,
|
||||
selectedExpireOption: DEFAULT_EXPIRE_OPTION,
|
||||
});
|
||||
|
||||
this.addActivationHandler(() => {
|
||||
this._onActivate();
|
||||
});
|
||||
}
|
||||
|
||||
private _onActivate() {
|
||||
getBackendSrv()
|
||||
.get('/api/snapshot/shared-options')
|
||||
.then((shareOptions: SnapshotSharingOptions) => {
|
||||
if (this.isActive) {
|
||||
this.setState({
|
||||
snapshotSharingOptions: shareOptions,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getTabLabel() {
|
||||
return t('share-modal.tab-title.snapshot', 'Snapshot');
|
||||
}
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<ShareSnapshotTab>) => {
|
||||
return <div>Snapshot</div>;
|
||||
public onSnasphotNameChange = (snapshotName: string) => {
|
||||
this.setState({ snapshotName: snapshotName.trim() });
|
||||
};
|
||||
|
||||
public onExpireChange = (option: number) => {
|
||||
this.setState({
|
||||
selectedExpireOption: EXPIRE_OPTIONS.find((o) => o.value === option),
|
||||
});
|
||||
};
|
||||
|
||||
private prepareSnapshot() {
|
||||
const timeRange = sceneGraph.getTimeRange(this);
|
||||
const { dashboardRef, panelRef } = this.state;
|
||||
const saveModel = transformSceneToSaveModel(dashboardRef.resolve(), true);
|
||||
|
||||
return trimDashboardForSnapshot(
|
||||
this.state.snapshotName || '',
|
||||
timeRange.state.value,
|
||||
saveModel,
|
||||
panelRef?.resolve()
|
||||
);
|
||||
}
|
||||
|
||||
public onSnapshotCreate = async (external = false) => {
|
||||
const { selectedExpireOption } = this.state;
|
||||
const snapshot = this.prepareSnapshot();
|
||||
|
||||
// TODO
|
||||
// snapshot.snapshot = {
|
||||
// originalUrl: window.location.href,
|
||||
// };
|
||||
|
||||
const cmdData = {
|
||||
dashboard: snapshot,
|
||||
name: snapshot.title,
|
||||
expires: selectedExpireOption?.value,
|
||||
external,
|
||||
};
|
||||
|
||||
try {
|
||||
const results: { deleteUrl: string; url: string } = await getBackendSrv().post(SNAPSHOTS_API_ENDPOINT, cmdData);
|
||||
return results;
|
||||
} finally {
|
||||
trackDashboardSharingActionPerType(external ? 'publish_snapshot' : 'local_snapshot', shareDashboardType.snapshot);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function ShareSnapshoTabRenderer({ model }: SceneComponentProps<ShareSnapshotTab>) {
|
||||
const { snapshotName, selectedExpireOption, modalRef, snapshotSharingOptions } = model.useState();
|
||||
|
||||
const [snapshotResult, createSnapshot] = useAsyncFn(async (external = false) => {
|
||||
return model.onSnapshotCreate(external);
|
||||
});
|
||||
|
||||
const [deleteSnapshotResult, deleteSnapshot] = useAsyncFn(async (url: string) => {
|
||||
return await getBackendSrv().get(url);
|
||||
});
|
||||
|
||||
// If snapshot has been deleted - show message and allow to close modal
|
||||
if (deleteSnapshotResult.value) {
|
||||
return (
|
||||
<Trans i18nKey="share-modal.snapshot.deleted-message">
|
||||
The snapshot has been deleted. If you have already accessed it once, then it might take up to an hour before
|
||||
before it is removed from browser caches or CDN caches.
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Before snapshot has been created show configuration */}
|
||||
{!Boolean(snapshotResult.value) && (
|
||||
<>
|
||||
<div>
|
||||
<p className="share-modal-info-text">
|
||||
<Trans i18nKey="share-modal.snapshot.info-text-1">
|
||||
A snapshot is an instant way to share an interactive dashboard publicly. When created, we strip
|
||||
sensitive data like queries (metric, template, and annotation) and panel links, leaving only the visible
|
||||
metric data and series names embedded in your dashboard.
|
||||
</Trans>
|
||||
</p>
|
||||
<p className="share-modal-info-text">
|
||||
<Trans i18nKey="share-modal.snapshot.info-text-2">
|
||||
Keep in mind, your snapshot <em>can be viewed by anyone</em> that has the link and can access the URL.
|
||||
Share wisely.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Field label={t('share-modal.snapshot.name', `Snapshot name`)}>
|
||||
<Input
|
||||
id="snapshot-name-input"
|
||||
width={30}
|
||||
defaultValue={snapshotName}
|
||||
onBlur={(e) => model.onSnasphotNameChange(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={t('share-modal.snapshot.expire', `Expire`)}>
|
||||
<RadioButtonGroup<number>
|
||||
id="expire-select-input"
|
||||
options={EXPIRE_OPTIONS}
|
||||
value={selectedExpireOption?.value}
|
||||
onChange={model.onExpireChange}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Modal.ButtonRow>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
modalRef?.resolve().onDismiss();
|
||||
}}
|
||||
fill="outline"
|
||||
>
|
||||
<Trans i18nKey="share-modal.snapshot.cancel-button">Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
{snapshotSharingOptions?.externalEnabled && (
|
||||
<Button variant="secondary" disabled={snapshotResult.loading} onClick={() => createSnapshot(true)}>
|
||||
{snapshotSharingOptions?.externalSnapshotName}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="primary" disabled={snapshotResult.loading} onClick={() => createSnapshot()}>
|
||||
<Trans i18nKey="share-modal.snapshot.local-button">Local Snapshot</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* When snapshot has been created - show link and allow copy/deletion */}
|
||||
{snapshotResult.value && (
|
||||
<>
|
||||
<Field label={t('share-modal.snapshot.url-label', 'Snapshot URL')}>
|
||||
<Input
|
||||
id="snapshot-url-input"
|
||||
value={snapshotResult.value.url}
|
||||
readOnly
|
||||
addonAfter={
|
||||
<ClipboardButton icon="copy" variant="primary" getText={() => snapshotResult.value!.url}>
|
||||
<Trans i18nKey="share-modal.snapshot.copy-link-button">Copy</Trans>
|
||||
</ClipboardButton>
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="pull-right" style={{ padding: '5px' }}>
|
||||
<Trans i18nKey="share-modal.snapshot.mistake-message">Did you make a mistake? </Trans>
|
||||
<Button
|
||||
fill="outline"
|
||||
size="md"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
deleteSnapshot(snapshotResult.value!.deleteUrl);
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="share-modal.snapshot.delete-button">Delete snapshot.</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ export class ShareSnapshot extends PureComponent<Props, State> {
|
||||
saveSnapshot = async (dashboard: DashboardModel, external?: boolean) => {
|
||||
const { snapshotExpires } = this.state;
|
||||
const dash = this.dashboard.getSaveModelClone();
|
||||
|
||||
this.scrubDashboard(dash);
|
||||
|
||||
const cmdData = {
|
||||
@ -141,6 +142,7 @@ export class ShareSnapshot extends PureComponent<Props, State> {
|
||||
|
||||
// remove annotation queries
|
||||
const annotations = dash.annotations.list.filter((annotation) => annotation.enable);
|
||||
|
||||
dash.annotations.list = annotations.map((annotation) => {
|
||||
return {
|
||||
name: annotation.name,
|
||||
|
Loading…
Reference in New Issue
Block a user