The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts

529 lines
16 KiB

import { defaults, isEqual } from 'lodash';
import { isEmptyObject, ScopedVars, TimeRange } from '@grafana/data';
import {
behaviors,
SceneGridItemLike,
SceneGridRow,
VizPanel,
SceneDataTransformer,
SceneVariableSet,
LocalValueVariable,
} from '@grafana/scenes';
import {
AnnotationQuery,
Dashboard,
DashboardLink,
DataTransformerConfig,
defaultDashboard,
defaultTimePickerConfig,
FieldConfigSource,
GridPos,
Panel,
RowPanel,
TimePickerConfig,
VariableModel,
VariableRefresh,
} from '@grafana/schema';
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard/utils';
import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator';
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardScene } from '../scene/DashboardScene';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior';
import { isClonedKey } from '../utils/clone';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import {
calculateGridItemDimensions,
getLibraryPanelBehavior,
getPanelIdForVizPanel,
getQueryRunnerFor,
isLibraryPanel,
} from '../utils/utils';
import { GRAFANA_DATASOURCE_REF } from './const';
import { dataLayersToAnnotations } from './dataLayersToAnnotations';
import { sceneVariablesSetToVariables } from './sceneVariablesSetToVariables';
export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = false): Dashboard {
const state = scene.state;
const timeRange = state.$timeRange!.state;
const data = state.$data;
const variablesSet = state.$variables;
const body = state.body;
let panels: Panel[] = [];
let variables: VariableModel[] = [];
if (body instanceof DefaultGridLayoutManager) {
for (const child of body.state.grid.state.children) {
if (child instanceof DashboardGridItem) {
// handle panel repeater scenario
if (child.state.variableName) {
panels = panels.concat(panelRepeaterToPanels(child, isSnapshot));
} else {
panels.push(gridItemToPanel(child, isSnapshot));
}
}
if (child instanceof SceneGridRow) {
// Skip repeat clones or when generating a snapshot
if (isClonedKey(child.state.key!) && !isSnapshot) {
continue;
}
gridRowToSaveModel(child, panels, isSnapshot);
}
}
}
let annotations: AnnotationQuery[] = [];
if (data instanceof DashboardDataLayerSet) {
annotations = dataLayersToAnnotations(data.state.annotationLayers);
}
if (variablesSet instanceof SceneVariableSet) {
variables = sceneVariablesSetToVariables(variablesSet);
}
const controlsState = state.controls?.state;
const refreshPicker = controlsState?.refreshPicker;
const timePickerWithoutDefaults = removeDefaults<TimePickerConfig>(
{
refresh_intervals: refreshPicker?.state.intervals,
hidden: controlsState?.hideTimeControls,
nowDelay: timeRange.UNSAFE_nowDelay,
quick_ranges: controlsState?.timePicker.state.quickRanges,
},
defaultTimePickerConfig
);
const graphTooltip =
state.$behaviors?.find((b): b is behaviors.CursorSync => b instanceof behaviors.CursorSync)?.state.sync ??
defaultDashboard.graphTooltip;
const liveNow =
state.$behaviors?.find((b): b is behaviors.LiveNowTimer => b instanceof behaviors.LiveNowTimer)?.isEnabled ||
undefined;
const dashboard: Dashboard = {
...defaultDashboard,
title: state.title,
description: state.description || undefined,
uid: state.uid,
id: state.id,
editable: state.editable,
preload: state.preload,
time: {
from: timeRange.from,
to: timeRange.to,
},
timepicker: timePickerWithoutDefaults,
panels,
annotations: {
list: annotations,
},
templating: {
list: variables,
},
version: state.version,
timezone: timeRange.timeZone,
fiscalYearStartMonth: timeRange.fiscalYearStartMonth,
weekStart: timeRange.weekStart,
tags: state.tags,
links: state.links,
graphTooltip,
liveNow,
schemaVersion: DASHBOARD_SCHEMA_VERSION,
refresh: refreshPicker?.state.refresh,
// @ts-expect-error not in dashboard schema because it's experimental
scopeMeta: state.scopeMeta,
};
return sortedDeepCloneWithoutNulls(dashboard);
}
export function gridItemToPanel(gridItem: DashboardGridItem, isSnapshot = false): Panel {
let vizPanel: VizPanel | undefined;
let x = 0,
y = 0,
w = 0,
h = 0;
let gridItem_ = gridItem;
if (!(gridItem_.state.body instanceof VizPanel)) {
throw new Error('DashboardGridItem body expected to be VizPanel');
}
vizPanel = gridItem_.state.body;
x = gridItem_.state.x ?? 0;
y = gridItem_.state.y ?? 0;
w = gridItem_.state.width ?? 0;
h = (gridItem_.state.variableName ? gridItem_.state.itemHeight : gridItem_.state.height) ?? 0;
if (!vizPanel) {
throw new Error('Unsupported grid item type');
}
const panel: Panel = vizPanelToPanel(vizPanel, { x, y, h, w }, isSnapshot, gridItem_);
return panel;
}
export function vizPanelToPanel(
vizPanel: VizPanel,
gridPos?: GridPos,
isSnapshot = false,
gridItem?: SceneGridItemLike
) {
let panel: Panel;
if (isLibraryPanel(vizPanel)) {
const libPanel = getLibraryPanelBehavior(vizPanel);
panel = {
id: getPanelIdForVizPanel(vizPanel),
title: vizPanel.state.title,
gridPos: gridPos,
libraryPanel: {
name: libPanel!.state.name,
uid: libPanel!.state.uid,
},
type: 'library-panel-ref',
} as Panel;
return panel;
} else {
panel = {
id: getPanelIdForVizPanel(vizPanel),
type: vizPanel.state.pluginId,
title: vizPanel.state.title,
description: vizPanel.state.description ?? undefined,
gridPos,
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
transformations: [],
transparent: vizPanel.state.displayMode === 'transparent',
pluginVersion: vizPanel.state.pluginVersion,
...vizPanelDataToPanel(vizPanel, isSnapshot),
};
}
if (vizPanel.state.options) {
const { angularOptions, ...rest } = vizPanel.state.options as any;
panel.options = rest;
if (angularOptions) {
// Allow angularOptions to overwrite non system level root properties
defaults(panel, angularOptions);
}
}
const panelTime = vizPanel.state.$timeRange;
if (panelTime instanceof PanelTimeRange) {
panel.timeFrom = panelTime.state.timeFrom;
panel.timeShift = panelTime.state.timeShift;
panel.hideTimeOverride = panelTime.state.hideTimeOverride;
}
if (gridItem instanceof DashboardGridItem) {
if (gridItem.state.variableName) {
panel.repeat = gridItem.state.variableName;
}
if (gridItem.state.maxPerRow) {
panel.maxPerRow = gridItem.state.maxPerRow;
}
if (gridItem.state.repeatDirection) {
panel.repeatDirection = gridItem.getRepeatDirection();
}
}
const panelLinks = dashboardSceneGraph.getPanelLinks(vizPanel);
panel.links = (panelLinks?.state.rawLinks as DashboardLink[]) ?? [];
if (panel.links.length === 0) {
delete panel.links;
}
if (panel.transformations?.length === 0) {
delete panel.transformations;
}
if (!panel.transparent) {
delete panel.transparent;
}
return panel;
}
function vizPanelDataToPanel(
vizPanel: VizPanel,
isSnapshot = false
): Pick<Panel, 'datasource' | 'targets' | 'maxDataPoints' | 'transformations'> {
const dataProvider = vizPanel.state.$data;
const panel: Pick<
Panel,
'datasource' | 'targets' | 'maxDataPoints' | 'transformations' | 'cacheTimeout' | 'queryCachingTTL' | 'interval'
> = {};
const queryRunner = getQueryRunnerFor(vizPanel);
if (queryRunner) {
panel.targets = queryRunner.state.queries;
panel.maxDataPoints = queryRunner.state.maxDataPoints;
panel.datasource = queryRunner.state.datasource;
if (queryRunner.state.cacheTimeout) {
panel.cacheTimeout = queryRunner.state.cacheTimeout;
}
if (queryRunner.state.queryCachingTTL) {
panel.queryCachingTTL = queryRunner.state.queryCachingTTL;
}
if (queryRunner.state.minInterval) {
panel.interval = queryRunner.state.minInterval;
}
}
if (dataProvider instanceof SceneDataTransformer) {
panel.transformations = dataProvider.state.transformations as DataTransformerConfig[];
}
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,
},
];
}
return panel;
}
export function panelRepeaterToPanels(repeater: DashboardGridItem, isSnapshot = false): Panel[] {
if (!isSnapshot) {
return [gridItemToPanel(repeater)];
} else {
// return early if the repeated panel is a library panel
if (repeater.state.body instanceof VizPanel && isLibraryPanel(repeater.state.body)) {
const { x = 0, y = 0, width: w = 0, height: h = 0 } = repeater.state;
return [vizPanelToPanel(repeater.state.body, { x, y, w, h }, isSnapshot)];
}
if (repeater.state.repeatedPanels) {
const { h, w, columnCount } = calculateGridItemDimensions(repeater);
const panels = repeater.state.repeatedPanels!.map((panel, index) => {
let x = 0,
y = 0;
if (repeater.state.repeatDirection === 'v') {
x = repeater.state.x!;
y = index * h;
} else {
x = (index % columnCount) * w;
y = repeater.state.y! + Math.floor(index / columnCount) * h;
}
const gridPos = { x, y, w, h };
const localVariable = panel.state.$variables!.getByName(repeater.state.variableName!) as LocalValueVariable;
const result: Panel = {
id: getPanelIdForVizPanel(panel),
type: panel.state.pluginId,
title: panel.state.title,
gridPos,
options: panel.state.options,
fieldConfig: (panel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
transformations: [],
transparent: panel.state.displayMode === 'transparent',
// @ts-expect-error scopedVars are runtime only properties, not part of the persisted Dashboardmodel
scopedVars: {
[repeater.state.variableName!]: {
text: localVariable?.state.text,
value: localVariable?.state.value,
},
},
...vizPanelDataToPanel(panel, isSnapshot),
};
return result;
});
return panels;
}
return [];
}
}
export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array<Panel | RowPanel>, isSnapshot = false) {
const collapsed = Boolean(gridRow.state.isCollapsed);
const rowPanel: RowPanel = {
type: 'row',
id: getPanelIdForVizPanel(gridRow),
title: gridRow.state.title,
gridPos: {
x: gridRow.state.x ?? 0,
y: gridRow.state.y ?? 0,
w: gridRow.state.width ?? 24,
h: gridRow.state.height ?? 1,
},
collapsed,
panels: [],
};
if (gridRow.state.$behaviors?.length) {
const behavior = gridRow.state.$behaviors[0];
if (behavior instanceof RowRepeaterBehavior) {
rowPanel.repeat = behavior.state.variableName;
}
}
if (isSnapshot) {
// Rows that are repeated has SceneVariableSet attached to them.
if (gridRow.state.$variables) {
const localVariable = gridRow.state.$variables;
const scopedVars: ScopedVars = (localVariable.state.variables as LocalValueVariable[]).reduce((acc, variable) => {
return {
...acc,
[variable.state.name]: {
text: variable.state.text,
value: variable.state.value,
},
};
}, {});
// @ts-expect-error
rowPanel.scopedVars = scopedVars;
}
}
panelsArray.push(rowPanel);
let panelsInsideRow: Panel[] = [];
if (isSnapshot) {
gridRow.state.children.forEach((c) => {
if (c instanceof DashboardGridItem) {
if (c.state.variableName) {
// Perform snapshot only for uncollapsed rows
panelsInsideRow = panelsInsideRow.concat(panelRepeaterToPanels(c, !collapsed));
} else {
// Perform snapshot only for uncollapsed panels
panelsInsideRow.push(gridItemToPanel(c, !collapsed));
}
}
});
} else {
panelsInsideRow = gridRow.state.children.map((c) => {
if (!(c instanceof DashboardGridItem)) {
throw new Error('Row child expected to be DashboardGridItem');
}
return gridItemToPanel(c);
});
}
if (gridRow.state.isCollapsed) {
rowPanel.panels = panelsInsideRow;
} else {
panelsArray.push(...panelsInsideRow);
}
}
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: [],
};
// 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) {
const singlePanel = dash.panels?.find((p) => p.id === getPanelIdForVizPanel(panel));
if (singlePanel) {
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,
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 result;
}
function removeDefaults<T>(object: T, defaults: T): T {
const newObj = { ...object };
for (const key in defaults) {
if (isEqual(newObj[key], defaults[key])) {
delete newObj[key];
}
}
return newObj;
}