mirror of https://github.com/grafana/grafana
prometheushacktoberfestmetricsmonitoringalertinggrafanagoinfluxdbmysqlpostgresanalyticsdata-visualizationdashboardbusiness-intelligenceelasticsearch
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.
826 lines
27 KiB
826 lines
27 KiB
import { omit } from 'lodash';
|
|
|
|
import { AnnotationQuery } from '@grafana/data';
|
|
import { config } from '@grafana/runtime';
|
|
import {
|
|
behaviors,
|
|
dataLayers,
|
|
QueryVariable,
|
|
SceneDataQuery,
|
|
SceneDataTransformer,
|
|
SceneQueryRunner,
|
|
SceneVariables,
|
|
SceneVariableSet,
|
|
VizPanel,
|
|
} from '@grafana/scenes';
|
|
import { DataSourceRef } from '@grafana/schema';
|
|
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
|
|
|
|
import {
|
|
Spec as DashboardV2Spec,
|
|
defaultSpec as defaultDashboardV2Spec,
|
|
defaultFieldConfigSource,
|
|
PanelKind,
|
|
PanelQueryKind,
|
|
TransformationKind,
|
|
FieldConfigSource,
|
|
DataTransformerConfig,
|
|
PanelQuerySpec,
|
|
DataQueryKind,
|
|
QueryOptionsSpec,
|
|
QueryVariableKind,
|
|
TextVariableKind,
|
|
IntervalVariableKind,
|
|
DatasourceVariableKind,
|
|
CustomVariableKind,
|
|
ConstantVariableKind,
|
|
GroupByVariableKind,
|
|
AdhocVariableKind,
|
|
AnnotationQueryKind,
|
|
DataLink,
|
|
LibraryPanelKind,
|
|
Element,
|
|
DashboardCursorSync,
|
|
FieldConfig,
|
|
FieldColor,
|
|
} from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen';
|
|
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
|
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
|
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
|
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
|
import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
|
|
|
|
import { DSReferencesMapping } from './DashboardSceneSerializer';
|
|
import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables';
|
|
import { colorIdEnumToColorIdV2, transformCursorSynctoEnum } from './transformToV2TypesUtils';
|
|
|
|
// FIXME: This is temporary to avoid creating partial types for all the new schema, it has some performance implications, but it's fine for now
|
|
type DeepPartial<T> = T extends object
|
|
? {
|
|
[P in keyof T]?: DeepPartial<T[P]>;
|
|
}
|
|
: T;
|
|
|
|
export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnapshot = false): DashboardV2Spec {
|
|
const sceneDash = scene.state;
|
|
const timeRange = sceneDash.$timeRange!.state;
|
|
|
|
const controlsState = sceneDash.controls?.state;
|
|
const refreshPicker = controlsState?.refreshPicker;
|
|
|
|
const dsReferencesMapping: DSReferencesMapping = scene.serializer.getDSReferencesMapping();
|
|
|
|
const dashboardSchemaV2: DeepPartial<DashboardV2Spec> = {
|
|
//dashboard settings
|
|
title: sceneDash.title,
|
|
description: sceneDash.description,
|
|
cursorSync: getCursorSync(sceneDash),
|
|
liveNow: getLiveNow(sceneDash),
|
|
preload: sceneDash.preload ?? defaultDashboardV2Spec().preload,
|
|
editable: sceneDash.editable ?? defaultDashboardV2Spec().editable,
|
|
links: sceneDash.links ?? defaultDashboardV2Spec().links,
|
|
tags: sceneDash.tags ?? defaultDashboardV2Spec().tags,
|
|
// EOF dashboard settings
|
|
|
|
// time settings
|
|
timeSettings: {
|
|
timezone: timeRange.timeZone,
|
|
from: timeRange.from,
|
|
to: timeRange.to,
|
|
autoRefresh: refreshPicker?.state.refresh || '',
|
|
autoRefreshIntervals: refreshPicker?.state.intervals,
|
|
hideTimepicker: controlsState?.hideTimeControls ?? false,
|
|
weekStart: timeRange.weekStart,
|
|
fiscalYearStartMonth: timeRange.fiscalYearStartMonth,
|
|
nowDelay: timeRange.UNSAFE_nowDelay,
|
|
quickRanges: controlsState?.timePicker.state.quickRanges,
|
|
},
|
|
// EOF time settings
|
|
|
|
// variables
|
|
variables: getVariables(sceneDash, dsReferencesMapping),
|
|
// EOF variables
|
|
|
|
// elements
|
|
elements: getElements(scene, dsReferencesMapping),
|
|
// EOF elements
|
|
|
|
// annotations
|
|
annotations: getAnnotations(sceneDash, dsReferencesMapping),
|
|
// EOF annotations
|
|
|
|
// layout
|
|
layout: sceneDash.body.serialize(),
|
|
// EOF layout
|
|
};
|
|
|
|
try {
|
|
// validateDashboardSchemaV2 will throw an error if the dashboard is not valid
|
|
if (validateDashboardSchemaV2(dashboardSchemaV2)) {
|
|
return sortedDeepCloneWithoutNulls(dashboardSchemaV2, true);
|
|
}
|
|
// should never reach this point, validation should throw an error
|
|
throw new Error('Error we could transform the dashboard to schema v2: ' + dashboardSchemaV2);
|
|
} catch (reason) {
|
|
console.error('Error transforming dashboard to schema v2: ' + reason, dashboardSchemaV2);
|
|
throw new Error('Error transforming dashboard to schema v2: ' + reason);
|
|
}
|
|
}
|
|
|
|
function getCursorSync(state: DashboardSceneState) {
|
|
const cursorSync = state.$behaviors?.find((b): b is behaviors.CursorSync => b instanceof behaviors.CursorSync)?.state
|
|
.sync;
|
|
|
|
return transformCursorSynctoEnum(cursorSync);
|
|
}
|
|
|
|
function getLiveNow(state: DashboardSceneState) {
|
|
const liveNow =
|
|
state.$behaviors?.find((b): b is behaviors.LiveNowTimer => b instanceof behaviors.LiveNowTimer)?.isEnabled ||
|
|
undefined;
|
|
// hack for validator
|
|
if (liveNow === undefined) {
|
|
return Boolean(defaultDashboardV2Spec().liveNow);
|
|
}
|
|
return Boolean(liveNow);
|
|
}
|
|
|
|
function getElements(scene: DashboardScene, dsReferencesMapping: DSReferencesMapping) {
|
|
const panels = scene.state.body.getVizPanels() ?? [];
|
|
const panelsArray = panels.map((vizPanel) => {
|
|
return vizPanelToSchemaV2(vizPanel, dsReferencesMapping);
|
|
});
|
|
return createElements(panelsArray, scene);
|
|
}
|
|
|
|
export function vizPanelToSchemaV2(
|
|
vizPanel: VizPanel,
|
|
dsReferencesMapping?: DSReferencesMapping
|
|
): PanelKind | LibraryPanelKind {
|
|
if (isLibraryPanel(vizPanel)) {
|
|
const behavior = getLibraryPanelBehavior(vizPanel)!;
|
|
const elementSpec: LibraryPanelKind = {
|
|
kind: 'LibraryPanel',
|
|
spec: {
|
|
id: getPanelIdForVizPanel(vizPanel),
|
|
title: vizPanel.state.title,
|
|
libraryPanel: {
|
|
uid: behavior.state.uid,
|
|
name: behavior.state.name,
|
|
},
|
|
},
|
|
};
|
|
return elementSpec;
|
|
}
|
|
|
|
// Handle type conversion for color mode
|
|
const rawColor = vizPanel.state.fieldConfig.defaults.color;
|
|
let color: FieldColor | undefined;
|
|
|
|
if (rawColor) {
|
|
const convertedMode = colorIdEnumToColorIdV2(rawColor.mode);
|
|
|
|
if (convertedMode) {
|
|
color = {
|
|
...rawColor,
|
|
mode: convertedMode,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Remove null from the defaults because schema V2 doesn't support null for these fields
|
|
const decimals = vizPanel.state.fieldConfig.defaults.decimals ?? undefined;
|
|
const min = vizPanel.state.fieldConfig.defaults.min ?? undefined;
|
|
const max = vizPanel.state.fieldConfig.defaults.max ?? undefined;
|
|
|
|
const defaults: FieldConfig = Object.fromEntries(
|
|
Object.entries({
|
|
...vizPanel.state.fieldConfig.defaults,
|
|
decimals,
|
|
min,
|
|
max,
|
|
color,
|
|
}).filter(([_, value]) => {
|
|
if (Array.isArray(value)) {
|
|
return value.length > 0;
|
|
}
|
|
return value !== undefined;
|
|
})
|
|
);
|
|
|
|
const vizFieldConfig: FieldConfigSource = {
|
|
...vizPanel.state.fieldConfig,
|
|
defaults,
|
|
};
|
|
|
|
const elementSpec: PanelKind = {
|
|
kind: 'Panel',
|
|
spec: {
|
|
id: getPanelIdForVizPanel(vizPanel),
|
|
title: vizPanel.state.title,
|
|
description: vizPanel.state.description ?? '',
|
|
links: getPanelLinks(vizPanel),
|
|
transparent: vizPanel.state.displayMode === 'transparent' ? true : undefined,
|
|
data: {
|
|
kind: 'QueryGroup',
|
|
spec: {
|
|
queries: getVizPanelQueries(vizPanel, dsReferencesMapping),
|
|
transformations: getVizPanelTransformations(vizPanel),
|
|
queryOptions: getVizPanelQueryOptions(vizPanel),
|
|
},
|
|
},
|
|
vizConfig: {
|
|
kind: vizPanel.state.pluginId,
|
|
spec: {
|
|
pluginVersion: vizPanel.state.pluginVersion ?? '',
|
|
options: vizPanel.state.options,
|
|
fieldConfig: vizFieldConfig ?? defaultFieldConfigSource(),
|
|
},
|
|
},
|
|
},
|
|
};
|
|
return elementSpec;
|
|
}
|
|
|
|
function getPanelLinks(panel: VizPanel): DataLink[] {
|
|
const vizLinks = dashboardSceneGraph.getPanelLinks(panel);
|
|
if (vizLinks) {
|
|
return vizLinks.state.rawLinks ?? [];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function getVizPanelQueries(vizPanel: VizPanel, dsReferencesMapping?: DSReferencesMapping): PanelQueryKind[] {
|
|
const queries: PanelQueryKind[] = [];
|
|
const queryRunner = getQueryRunnerFor(vizPanel);
|
|
const vizPanelQueries = queryRunner?.state.queries;
|
|
|
|
if (vizPanelQueries) {
|
|
vizPanelQueries.forEach((query) => {
|
|
const queryDatasource = getElementDatasource(vizPanel, query, 'panel', queryRunner, dsReferencesMapping);
|
|
const dataQuery: DataQueryKind = {
|
|
kind: getDataQueryKind(query, queryRunner),
|
|
spec: omit(query, 'datasource', 'refId', 'hide'),
|
|
};
|
|
const querySpec: PanelQuerySpec = {
|
|
datasource: queryDatasource,
|
|
query: dataQuery,
|
|
refId: query.refId,
|
|
hidden: Boolean(query.hide),
|
|
};
|
|
queries.push({
|
|
kind: 'PanelQuery',
|
|
spec: querySpec,
|
|
});
|
|
});
|
|
}
|
|
return queries;
|
|
}
|
|
|
|
export function getDataQueryKind(query: SceneDataQuery | string, queryRunner?: SceneQueryRunner): string {
|
|
// Query is a string - get default data source type
|
|
if (typeof query === 'string') {
|
|
const defaultDS = getDefaultDataSourceRef();
|
|
return defaultDS?.type || '';
|
|
}
|
|
|
|
// Query has explicit datasource with type
|
|
if (query.datasource?.type) {
|
|
return query.datasource.type;
|
|
}
|
|
|
|
// Get type from query runner's datasource
|
|
if (queryRunner?.state.datasource?.type) {
|
|
return queryRunner.state.datasource.type;
|
|
}
|
|
|
|
// Fall back to default datasource
|
|
const defaultDS = getDefaultDataSourceRef();
|
|
return defaultDS?.type || '';
|
|
}
|
|
|
|
export function getDataQuerySpec(query: SceneDataQuery): DataQueryKind['spec'] {
|
|
return query;
|
|
}
|
|
|
|
function getVizPanelTransformations(vizPanel: VizPanel): TransformationKind[] {
|
|
let transformations: TransformationKind[] = [];
|
|
const dataProvider = vizPanel.state.$data;
|
|
if (dataProvider instanceof SceneDataTransformer) {
|
|
const transformationList = dataProvider.state.transformations;
|
|
|
|
if (transformationList.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
for (const transformationItem of transformationList) {
|
|
const transformation = transformationItem;
|
|
|
|
if ('id' in transformation) {
|
|
// Transformation is a DataTransformerConfig
|
|
const transformationSpec: DataTransformerConfig = {
|
|
id: transformation.id,
|
|
disabled: transformation.disabled,
|
|
filter: transformation.filter,
|
|
...(transformation.topic && { topic: transformation.topic }),
|
|
options: transformation.options,
|
|
};
|
|
|
|
transformations.push({
|
|
kind: transformation.id,
|
|
spec: transformationSpec,
|
|
});
|
|
} else {
|
|
throw new Error('Unsupported transformation type');
|
|
}
|
|
}
|
|
}
|
|
return transformations;
|
|
}
|
|
|
|
function getVizPanelQueryOptions(vizPanel: VizPanel): QueryOptionsSpec {
|
|
let queryOptions: QueryOptionsSpec = {};
|
|
const queryRunner = getQueryRunnerFor(vizPanel);
|
|
|
|
if (queryRunner) {
|
|
queryOptions.maxDataPoints = queryRunner.state.maxDataPoints;
|
|
|
|
if (queryRunner.state.cacheTimeout) {
|
|
queryOptions.cacheTimeout = queryRunner.state.cacheTimeout;
|
|
}
|
|
|
|
if (queryRunner.state.queryCachingTTL) {
|
|
queryOptions.queryCachingTTL = queryRunner.state.queryCachingTTL;
|
|
}
|
|
if (queryRunner.state.minInterval) {
|
|
queryOptions.interval = queryRunner.state.minInterval;
|
|
}
|
|
}
|
|
|
|
const panelTime = vizPanel.state.$timeRange;
|
|
|
|
if (panelTime instanceof PanelTimeRange) {
|
|
queryOptions.timeFrom = panelTime.state.timeFrom;
|
|
queryOptions.timeShift = panelTime.state.timeShift;
|
|
queryOptions.hideTimeOverride = panelTime.state.hideTimeOverride;
|
|
}
|
|
return queryOptions;
|
|
}
|
|
|
|
export function createElements(panels: Element[], scene: DashboardScene): Record<string, Element> {
|
|
return panels.reduce<Record<string, Element>>((elements, panel) => {
|
|
let elementKey = scene.serializer.getElementIdForPanel(panel.spec.id);
|
|
elements[elementKey!] = panel;
|
|
return elements;
|
|
}, {});
|
|
}
|
|
|
|
function getVariables(oldDash: DashboardSceneState, dsReferencesMapping?: DSReferencesMapping) {
|
|
const variablesSet = oldDash.$variables;
|
|
|
|
// variables is an array of all variables kind (union)
|
|
let variables: Array<
|
|
| QueryVariableKind
|
|
| TextVariableKind
|
|
| IntervalVariableKind
|
|
| DatasourceVariableKind
|
|
| CustomVariableKind
|
|
| ConstantVariableKind
|
|
| GroupByVariableKind
|
|
| AdhocVariableKind
|
|
> = [];
|
|
|
|
if (variablesSet instanceof SceneVariableSet) {
|
|
variables = sceneVariablesSetToSchemaV2Variables(variablesSet, false, dsReferencesMapping);
|
|
}
|
|
|
|
return variables;
|
|
}
|
|
|
|
function getAnnotations(state: DashboardSceneState, dsReferencesMapping?: DSReferencesMapping): AnnotationQueryKind[] {
|
|
const data = state.$data;
|
|
if (!(data instanceof DashboardDataLayerSet)) {
|
|
return [];
|
|
}
|
|
const annotations: AnnotationQueryKind[] = [];
|
|
for (const layer of data.state.annotationLayers) {
|
|
if (!(layer instanceof dataLayers.AnnotationsDataLayer)) {
|
|
continue;
|
|
}
|
|
const result: AnnotationQueryKind = {
|
|
kind: 'AnnotationQuery',
|
|
spec: {
|
|
builtIn: Boolean(layer.state.query.builtIn),
|
|
name: layer.state.query.name,
|
|
datasource: getElementDatasource(layer, layer.state.query, 'annotation', undefined, dsReferencesMapping),
|
|
enable: Boolean(layer.state.isEnabled),
|
|
hide: Boolean(layer.state.isHidden),
|
|
iconColor: layer.state.query.iconColor,
|
|
},
|
|
};
|
|
|
|
// Transform v1 dashboard (using target) to v2 structure
|
|
// adds extra condition to prioritize query over target
|
|
// if query is defined, use it
|
|
if (layer.state.query.target && !layer.state.query.query) {
|
|
// Handle built-in annotations
|
|
if (layer.state.query.builtIn) {
|
|
result.spec.query = {
|
|
kind: 'grafana', // built-in annotations are always of type grafana
|
|
spec: {
|
|
...layer.state.query.target,
|
|
},
|
|
};
|
|
} else {
|
|
result.spec.query = {
|
|
kind: getAnnotationQueryKind(layer.state.query),
|
|
spec: {
|
|
...layer.state.query.target,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
// For annotations without query.query defined (e.g., grafana annotations without tags)
|
|
else if (layer.state.query.query?.kind) {
|
|
result.spec.query = {
|
|
kind: layer.state.query.query.kind,
|
|
spec: {
|
|
...layer.state.query.query.spec,
|
|
},
|
|
};
|
|
}
|
|
// Collect datasource-specific properties not in standard annotation spec
|
|
let otherProps = omit(
|
|
layer.state.query,
|
|
'type',
|
|
'target',
|
|
'builtIn',
|
|
'name',
|
|
'datasource',
|
|
'iconColor',
|
|
'enable',
|
|
'hide',
|
|
'filter',
|
|
'query'
|
|
);
|
|
|
|
// Store extra properties in the legacyOptions field instead of directly in the spec
|
|
if (Object.keys(otherProps).length > 0) {
|
|
// Extract options property and get the rest of the properties
|
|
const { legacyOptions, ...restProps } = otherProps;
|
|
// Merge options with the rest of the properties
|
|
result.spec.legacyOptions = { ...legacyOptions, ...restProps };
|
|
}
|
|
|
|
// If filter is an empty array, don't save it
|
|
if (layer.state.query.filter?.ids?.length) {
|
|
result.spec.filter = layer.state.query.filter;
|
|
}
|
|
|
|
annotations.push(result);
|
|
}
|
|
return annotations;
|
|
}
|
|
|
|
export function getAnnotationQueryKind(annotationQuery: AnnotationQuery): string {
|
|
if (annotationQuery.datasource?.type) {
|
|
return annotationQuery.datasource.type;
|
|
} else {
|
|
const ds = getDefaultDataSourceRef();
|
|
if (ds) {
|
|
return ds.type!; // in the datasource list from bootData "id" is the type
|
|
}
|
|
// if we can't find the default datasource, return grafana as default
|
|
return 'grafana';
|
|
}
|
|
}
|
|
|
|
export function getDefaultDataSourceRef(): DataSourceRef {
|
|
// we need to return the default datasource configured in the BootConfig
|
|
const defaultDatasource = config.bootData.settings.defaultDatasource;
|
|
|
|
// get default datasource type
|
|
const dsList = config.bootData.settings.datasources;
|
|
const ds = dsList[defaultDatasource];
|
|
|
|
return { type: ds.meta.id, uid: ds.name }; // in the datasource list from bootData "id" is the type
|
|
}
|
|
|
|
// Function to know if the dashboard transformed is a valid DashboardV2Spec
|
|
export function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec {
|
|
if (typeof dash !== 'object' || dash === null || Array.isArray(dash)) {
|
|
throw new Error('Dashboard is not an object or is null');
|
|
}
|
|
|
|
// Required properties
|
|
if (!('title' in dash) || typeof dash.title !== 'string') {
|
|
throw new Error('Title is not a string');
|
|
}
|
|
if (!('timeSettings' in dash) || typeof dash.timeSettings !== 'object' || dash.timeSettings === null) {
|
|
throw new Error('TimeSettings is not an object or is null');
|
|
}
|
|
if (!('variables' in dash) || !Array.isArray(dash.variables)) {
|
|
throw new Error('Variables is not an array');
|
|
}
|
|
if (!('elements' in dash) || typeof dash.elements !== 'object' || dash.elements === null) {
|
|
throw new Error('Elements is not an object or is null');
|
|
}
|
|
if (!('annotations' in dash) || !Array.isArray(dash.annotations)) {
|
|
throw new Error('Annotations is not an array');
|
|
}
|
|
if (!('layout' in dash) || typeof dash.layout !== 'object' || dash.layout === null) {
|
|
throw new Error('Layout is not an object or is null');
|
|
}
|
|
|
|
// Optional properties - only validate if present
|
|
if ('description' in dash && dash.description !== undefined && typeof dash.description !== 'string') {
|
|
throw new Error('Description is not a string');
|
|
}
|
|
if ('cursorSync' in dash && dash.cursorSync !== undefined) {
|
|
const validCursorSyncValues = ((): string[] => {
|
|
const typeValues: DashboardCursorSync[] = ['Off', 'Crosshair', 'Tooltip'];
|
|
return typeValues;
|
|
})();
|
|
|
|
if (typeof dash.cursorSync !== 'string' || !validCursorSyncValues.includes(dash.cursorSync)) {
|
|
throw new Error('CursorSync is not a valid value');
|
|
}
|
|
}
|
|
if ('liveNow' in dash && dash.liveNow !== undefined && typeof dash.liveNow !== 'boolean') {
|
|
throw new Error('LiveNow is not a boolean');
|
|
}
|
|
if ('preload' in dash && dash.preload !== undefined && typeof dash.preload !== 'boolean') {
|
|
throw new Error('Preload is not a boolean');
|
|
}
|
|
if ('editable' in dash && dash.editable !== undefined && typeof dash.editable !== 'boolean') {
|
|
throw new Error('Editable is not a boolean');
|
|
}
|
|
if ('links' in dash && dash.links !== undefined && !Array.isArray(dash.links)) {
|
|
throw new Error('Links is not an array');
|
|
}
|
|
if ('tags' in dash && dash.tags !== undefined && !Array.isArray(dash.tags)) {
|
|
throw new Error('Tags is not an array');
|
|
}
|
|
if ('id' in dash && dash.id !== undefined && typeof dash.id !== 'number') {
|
|
throw new Error('ID is not a number');
|
|
}
|
|
|
|
// Time settings validation
|
|
const timeSettings = dash.timeSettings;
|
|
|
|
// Required time settings
|
|
if (!('from' in timeSettings) || typeof timeSettings.from !== 'string') {
|
|
throw new Error('From is not a string');
|
|
}
|
|
if (!('to' in timeSettings) || typeof timeSettings.to !== 'string') {
|
|
throw new Error('To is not a string');
|
|
}
|
|
if (!('autoRefresh' in timeSettings) || typeof timeSettings.autoRefresh !== 'string') {
|
|
throw new Error('AutoRefresh is not a string');
|
|
}
|
|
if (!('hideTimepicker' in timeSettings) || typeof timeSettings.hideTimepicker !== 'boolean') {
|
|
throw new Error('HideTimepicker is not a boolean');
|
|
}
|
|
|
|
// Optional time settings with defaults
|
|
if (
|
|
'autoRefreshIntervals' in timeSettings &&
|
|
timeSettings.autoRefreshIntervals !== undefined &&
|
|
!Array.isArray(timeSettings.autoRefreshIntervals)
|
|
) {
|
|
throw new Error('AutoRefreshIntervals is not an array');
|
|
}
|
|
if ('timezone' in timeSettings && timeSettings.timezone !== undefined && typeof timeSettings.timezone !== 'string') {
|
|
throw new Error('Timezone is not a string');
|
|
}
|
|
if (
|
|
'quickRanges' in timeSettings &&
|
|
timeSettings.quickRanges !== undefined &&
|
|
!Array.isArray(timeSettings.quickRanges)
|
|
) {
|
|
throw new Error('QuickRanges is not an array');
|
|
}
|
|
if ('weekStart' in timeSettings && timeSettings.weekStart !== undefined) {
|
|
if (
|
|
typeof timeSettings.weekStart !== 'string' ||
|
|
!['saturday', 'sunday', 'monday'].includes(timeSettings.weekStart)
|
|
) {
|
|
throw new Error('WeekStart should be one of "saturday", "sunday" or "monday"');
|
|
}
|
|
}
|
|
if ('nowDelay' in timeSettings && timeSettings.nowDelay !== undefined && typeof timeSettings.nowDelay !== 'string') {
|
|
throw new Error('NowDelay is not a string');
|
|
}
|
|
if (
|
|
'fiscalYearStartMonth' in timeSettings &&
|
|
timeSettings.fiscalYearStartMonth !== undefined &&
|
|
typeof timeSettings.fiscalYearStartMonth !== 'number'
|
|
) {
|
|
throw new Error('FiscalYearStartMonth is not a number');
|
|
}
|
|
|
|
// Layout validation
|
|
if (!('kind' in dash.layout)) {
|
|
throw new Error('Layout kind is required');
|
|
}
|
|
|
|
if (dash.layout.kind === 'GridLayout') {
|
|
validateGridLayout(dash.layout);
|
|
} else if (dash.layout.kind === 'RowsLayout') {
|
|
validateRowsLayout(dash.layout);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function validateGridLayout(layout: unknown) {
|
|
if (typeof layout !== 'object' || layout === null) {
|
|
throw new Error('Layout is not an object or is null');
|
|
}
|
|
if (!('kind' in layout) || layout.kind !== 'GridLayout') {
|
|
throw new Error('Layout kind is not GridLayout');
|
|
}
|
|
if (!('spec' in layout) || typeof layout.spec !== 'object' || layout.spec === null) {
|
|
throw new Error('Layout spec is not an object or is null');
|
|
}
|
|
if (!('items' in layout.spec) || !Array.isArray(layout.spec.items)) {
|
|
throw new Error('Layout spec items is not an array');
|
|
}
|
|
}
|
|
|
|
function validateRowsLayout(layout: unknown) {
|
|
if (typeof layout !== 'object' || layout === null) {
|
|
throw new Error('Layout is not an object or is null');
|
|
}
|
|
if (!('kind' in layout) || layout.kind !== 'RowsLayout') {
|
|
throw new Error('Layout kind is not RowsLayout');
|
|
}
|
|
if (!('spec' in layout) || typeof layout.spec !== 'object' || layout.spec === null) {
|
|
throw new Error('Layout spec is not an object or is null');
|
|
}
|
|
if (!('rows' in layout.spec) || !Array.isArray(layout.spec.rows)) {
|
|
throw new Error('Layout spec items is not an array');
|
|
}
|
|
}
|
|
|
|
export function getAutoAssignedDSRef(
|
|
element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer,
|
|
type: 'panels' | 'variables' | 'annotations',
|
|
elementMapReferences?: DSReferencesMapping
|
|
): Set<string> {
|
|
if (!elementMapReferences) {
|
|
return new Set();
|
|
}
|
|
if (type === 'panels' && isVizPanel(element)) {
|
|
const elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(element);
|
|
return elementMapReferences.panels.get(elementKey) || new Set();
|
|
}
|
|
|
|
if (type === 'variables') {
|
|
return elementMapReferences.variables;
|
|
}
|
|
|
|
if (type === 'annotations') {
|
|
return elementMapReferences.annotations;
|
|
}
|
|
|
|
// if type is not panels, annotations, or variables, throw error
|
|
throw new Error(`Invalid type ${type} for getAutoAssignedDSRef`);
|
|
}
|
|
|
|
/**
|
|
* Determines if a data source reference should be persisted for a query or variable
|
|
*/
|
|
export function getPersistedDSFor<T extends SceneDataQuery | QueryVariable | AnnotationQuery>(
|
|
element: T,
|
|
autoAssignedDsRef: Set<string>,
|
|
type: 'query' | 'variable' | 'annotation',
|
|
context?: SceneQueryRunner
|
|
): DataSourceRef | undefined {
|
|
// Get the element identifier - refId for queries, name for variables
|
|
const elementId = getElementIdentifier(element, type);
|
|
|
|
// If the element is in the auto-assigned set, it didn't have a datasource specified
|
|
if (autoAssignedDsRef?.has(elementId)) {
|
|
return undefined;
|
|
}
|
|
|
|
// Return appropriate datasource reference based on element type
|
|
if (type === 'query') {
|
|
if ('datasource' in element && element.datasource) {
|
|
// If element has its own datasource, use that
|
|
return element.datasource;
|
|
}
|
|
|
|
// For queries missing a datasource but not in auto-assigned set, use datasource from context (queryRunner)
|
|
return context?.state?.datasource;
|
|
}
|
|
|
|
if (type === 'variable' && 'state' in element && 'datasource' in element.state) {
|
|
return element.state.datasource || {};
|
|
}
|
|
|
|
if (type === 'annotation' && 'datasource' in element) {
|
|
return element.datasource || {};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Helper function to extract which identifier to use from a query or variable element
|
|
* @returns refId for queries, name for variables
|
|
* TODO: we will add annotations in the future
|
|
*/
|
|
function getElementIdentifier<T extends SceneDataQuery | QueryVariable | AnnotationQuery>(
|
|
element: T,
|
|
type: 'query' | 'variable' | 'annotation'
|
|
): string {
|
|
// when is type query look for refId
|
|
if (type === 'query') {
|
|
return 'refId' in element ? element.refId : '';
|
|
}
|
|
|
|
if (type === 'variable') {
|
|
// when is type variable look for the name of the variable
|
|
return 'state' in element && 'name' in element.state ? element.state.name : '';
|
|
}
|
|
|
|
// when is type annotation look for annotation name
|
|
if (type === 'annotation') {
|
|
return 'name' in element ? element.name : '';
|
|
}
|
|
|
|
throw new Error(`Invalid type ${type} for getElementIdentifier`);
|
|
}
|
|
|
|
function isVizPanel(element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer): element is VizPanel {
|
|
// FIXME: is there another way to do this?
|
|
return 'pluginId' in element.state;
|
|
}
|
|
|
|
function isSceneVariables(
|
|
element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer
|
|
): element is SceneVariables {
|
|
// Check for properties unique to SceneVariables but not in VizPanel
|
|
return !('pluginId' in element.state) && ('variables' in element.state || 'getValue' in element);
|
|
}
|
|
|
|
function isSceneDataQuery(query: SceneDataQuery | QueryVariable | AnnotationQuery): query is SceneDataQuery {
|
|
return 'refId' in query && !('state' in query);
|
|
}
|
|
|
|
function isAnnotationQuery(query: SceneDataQuery | QueryVariable | AnnotationQuery): query is AnnotationQuery {
|
|
return 'datasource' in query && 'name' in query;
|
|
}
|
|
|
|
function isQueryVariable(query: SceneDataQuery | QueryVariable | AnnotationQuery): query is QueryVariable {
|
|
return 'state' in query && 'name' in query.state;
|
|
}
|
|
|
|
/**
|
|
* Get the persisted datasource for a query or variable
|
|
* When a query or variable is created it could not have a datasource set
|
|
* we want to respect that and not overwrite it with the auto assigned datasources
|
|
* resolved in runtime
|
|
*
|
|
*/
|
|
export function getElementDatasource(
|
|
element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer,
|
|
queryElement: SceneDataQuery | QueryVariable | AnnotationQuery,
|
|
type: 'panel' | 'variable' | 'annotation',
|
|
queryRunner?: SceneQueryRunner,
|
|
dsReferencesMapping?: DSReferencesMapping
|
|
): DataSourceRef | undefined {
|
|
let result: DataSourceRef | undefined;
|
|
if (type === 'panel') {
|
|
if (!queryRunner || !isVizPanel(element) || !isSceneDataQuery(queryElement)) {
|
|
return undefined;
|
|
}
|
|
// Get datasource for panel query
|
|
const autoAssignedRefs = getAutoAssignedDSRef(element, 'panels', dsReferencesMapping);
|
|
result = getPersistedDSFor(queryElement, autoAssignedRefs, 'query', queryRunner);
|
|
}
|
|
|
|
if (type === 'variable') {
|
|
if (!isSceneVariables(element) || !isQueryVariable(queryElement)) {
|
|
return undefined;
|
|
}
|
|
// Get datasource for variable
|
|
const autoAssignedRefs = getAutoAssignedDSRef(element, 'variables', dsReferencesMapping);
|
|
|
|
result = getPersistedDSFor(queryElement, autoAssignedRefs, 'variable');
|
|
}
|
|
|
|
if (type === 'annotation') {
|
|
if (!isAnnotationQuery(queryElement)) {
|
|
return undefined;
|
|
}
|
|
// Get datasource for annotation
|
|
const autoAssignedRefs = getAutoAssignedDSRef(element, 'annotations', dsReferencesMapping);
|
|
result = getPersistedDSFor(queryElement, autoAssignedRefs, 'annotation');
|
|
}
|
|
// Important: Only return the datasource if it's not in auto-assigned refs
|
|
// and if the result would not be an empty object
|
|
return Object.keys(result || {}).length > 0 ? result : undefined;
|
|
}
|
|
|