diff --git a/.betterer.results b/.betterer.results index bb112843ee0..abfc6463bfe 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"], diff --git a/packages/grafana-data/test/__mocks__/pluginMocks.ts b/packages/grafana-data/test/__mocks__/pluginMocks.ts index 71268a244c9..226d6eb87cb 100644 --- a/packages/grafana-data/test/__mocks__/pluginMocks.ts +++ b/packages/grafana-data/test/__mocks__/pluginMocks.ts @@ -61,7 +61,7 @@ export function getPanelPlugin( }, screenshots: [], updated: '', - version: '', + version: '1', }, hideFromList: options.hideFromList === true, module: options.module ?? '', diff --git a/packages/grafana-schema/src/veneer/dashboard.types.ts b/packages/grafana-schema/src/veneer/dashboard.types.ts index 1d3522622ea..c444b0e7840 100644 --- a/packages/grafana-schema/src/veneer/dashboard.types.ts +++ b/packages/grafana-schema/src/veneer/dashboard.types.ts @@ -36,6 +36,9 @@ export interface AnnotationQuery extends Omit { 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 { diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index f71c10f518e..3e9a5012a51 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -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", }, ], }, diff --git a/public/app/features/dashboard-scene/serialization/const.ts b/public/app/features/dashboard-scene/serialization/const.ts new file mode 100644 index 00000000000..f7e03e4328b --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/const.ts @@ -0,0 +1,4 @@ +export const GRAFANA_DATASOURCE_REF = { + name: 'grafana', + uid: 'grafana', +}; diff --git a/public/app/features/dashboard-scene/serialization/dataLayersToAnnotations.ts b/public/app/features/dashboard-scene/serialization/dataLayersToAnnotations.ts new file mode 100644 index 00000000000..1261ec4157a --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/dataLayersToAnnotations.ts @@ -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; +} diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts index fb528d04bca..33df731b3f0 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -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", } `); }); diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index e11ad014301..17e69a1ac4b 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -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, diff --git a/public/app/features/dashboard-scene/serialization/testfiles/snapshotable_dashboard.json b/public/app/features/dashboard-scene/serialization/testfiles/snapshotable_dashboard.json new file mode 100644 index 00000000000..3d4528fc997 --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/testfiles/snapshotable_dashboard.json @@ -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": "" +} diff --git a/public/app/features/dashboard-scene/serialization/testfiles/snapshotable_with_rows.json b/public/app/features/dashboard-scene/serialization/testfiles/snapshotable_with_rows.json new file mode 100644 index 00000000000..408c5fc18cd --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/testfiles/snapshotable_with_rows.json @@ -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": "" + } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index af8e41af6e0..fd3e42565ae 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -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 = { + 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): SceneGridItemLike { diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index bac01eaaaac..795a7a50b67 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -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) { +export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array, isSnapshot = false) { const rowPanel: RowPanel = { type: 'row', id: getPanelIdForVizPanel(gridRow), @@ -265,7 +285,7 @@ export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array gridItemToPanel(c, isSnapshot)); if (gridRow.state.isCollapsed) { rowPanel.panels = panelsInsideRow; @@ -274,19 +294,75 @@ export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array 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; } diff --git a/public/app/features/dashboard-scene/sharing/ShareModal.tsx b/public/app/features/dashboard-scene/sharing/ShareModal.tsx index 4bc3b19e84b..e6ac04955a5 100644 --- a/public/app/features/dashboard-scene/sharing/ShareModal.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareModal.tsx @@ -46,7 +46,7 @@ export class ShareModal extends SceneObjectBase 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 }); diff --git a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx index fa3562c8cc8..591966f7fb4 100644 --- a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx @@ -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 = { + 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; + dashboardRef: SceneObjectRef; + snapshotName?: string; + selectedExpireOption?: SelectableValue; + + snapshotSharingOptions?: SnapshotSharingOptions; } export class ShareSnapshotTab extends SceneObjectBase { + 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) => { - return
Snapshot
; + 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) { + 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 ( + + 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. + + ); + } + + return ( + <> + {/* Before snapshot has been created show configuration */} + {!Boolean(snapshotResult.value) && ( + <> +
+

+ + 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. + +

+

+ + Keep in mind, your snapshot can be viewed by anyone that has the link and can access the URL. + Share wisely. + +

+
+ + + model.onSnasphotNameChange(e.target.value)} + /> + + + + + id="expire-select-input" + options={EXPIRE_OPTIONS} + value={selectedExpireOption?.value} + onChange={model.onExpireChange} + /> + + + + + + {snapshotSharingOptions?.externalEnabled && ( + + )} + + + + )} + + {/* When snapshot has been created - show link and allow copy/deletion */} + {snapshotResult.value && ( + <> + + snapshotResult.value!.url}> + Copy + + } + /> + + +
+ Did you make a mistake?   + +
+ + )} + + ); +} diff --git a/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx b/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx index 10bbf50b0e4..1923a0fb30f 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx @@ -98,6 +98,7 @@ export class ShareSnapshot extends PureComponent { 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 { // remove annotation queries const annotations = dash.annotations.list.filter((annotation) => annotation.enable); + dash.annotations.list = annotations.map((annotation) => { return { name: annotation.name,