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.
1136 lines
34 KiB
1136 lines
34 KiB
import { TypedVariableModel } from '@grafana/data';
|
|
import { config } from '@grafana/runtime';
|
|
import {
|
|
AnnotationQuery,
|
|
DataQuery,
|
|
DataSourceRef,
|
|
Panel,
|
|
RowPanel,
|
|
VariableModel,
|
|
VariableType,
|
|
FieldConfigSource as FieldConfigSourceV1,
|
|
FieldColorModeId as FieldColorModeIdV1,
|
|
ThresholdsMode as ThresholdsModeV1,
|
|
MappingType as MappingTypeV1,
|
|
SpecialValueMatch as SpecialValueMatchV1,
|
|
} from '@grafana/schema';
|
|
import {
|
|
AnnotationQueryKind,
|
|
DashboardV2Spec,
|
|
DataLink,
|
|
DatasourceVariableKind,
|
|
defaultDashboardV2Spec,
|
|
defaultFieldConfigSource,
|
|
defaultTimeSettingsSpec,
|
|
PanelQueryKind,
|
|
QueryVariableKind,
|
|
TransformationKind,
|
|
FieldColorModeId,
|
|
FieldConfigSource,
|
|
ThresholdsMode,
|
|
SpecialValueMatch,
|
|
AdhocVariableKind,
|
|
CustomVariableKind,
|
|
ConstantVariableKind,
|
|
IntervalVariableKind,
|
|
TextVariableKind,
|
|
GroupByVariableKind,
|
|
LibraryPanelKind,
|
|
PanelKind,
|
|
GridLayoutRowKind,
|
|
GridLayoutItemKind,
|
|
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
|
import { DashboardLink, DataTransformerConfig } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen';
|
|
import { isWeekStart, WeekStart } from '@grafana/ui';
|
|
import {
|
|
AnnoKeyCreatedBy,
|
|
AnnoKeyDashboardGnetId,
|
|
AnnoKeyDashboardIsSnapshot,
|
|
AnnoKeyDashboardSnapshotOriginalUrl,
|
|
AnnoKeyFolder,
|
|
AnnoKeySlug,
|
|
AnnoKeyUpdatedBy,
|
|
AnnoKeyUpdatedTimestamp,
|
|
DeprecatedInternalId,
|
|
} from 'app/features/apiserver/types';
|
|
import { GRID_ROW_HEIGHT } from 'app/features/dashboard-scene/serialization/const';
|
|
import { TypedVariableModelV2 } from 'app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene';
|
|
import { getDefaultDataSourceRef } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2';
|
|
import {
|
|
transformCursorSyncV2ToV1,
|
|
transformSortVariableToEnumV1,
|
|
transformVariableHideToEnumV1,
|
|
transformVariableRefreshToEnumV1,
|
|
} from 'app/features/dashboard-scene/serialization/transformToV1TypesUtils';
|
|
import {
|
|
LEGACY_STRING_VALUE_KEY,
|
|
transformCursorSynctoEnum,
|
|
transformDataTopic,
|
|
transformSortVariableToEnum,
|
|
transformVariableHideToEnum,
|
|
transformVariableRefreshToEnum,
|
|
} from 'app/features/dashboard-scene/serialization/transformToV2TypesUtils';
|
|
import { DashboardDataDTO, DashboardDTO } from 'app/types';
|
|
|
|
import { DashboardWithAccessInfo } from './types';
|
|
import { isDashboardResource, isDashboardV0Spec, isDashboardV2Resource } from './utils';
|
|
|
|
export function ensureV2Response(
|
|
dto: DashboardDTO | DashboardWithAccessInfo<DashboardDataDTO> | DashboardWithAccessInfo<DashboardV2Spec>
|
|
): DashboardWithAccessInfo<DashboardV2Spec> {
|
|
if (isDashboardV2Resource(dto)) {
|
|
return dto;
|
|
}
|
|
let dashboard: DashboardDataDTO;
|
|
|
|
if (isDashboardResource(dto)) {
|
|
dashboard = dto.spec;
|
|
} else {
|
|
dashboard = dto.dashboard;
|
|
}
|
|
|
|
const timeSettingsDefaults = defaultTimeSettingsSpec();
|
|
const dashboardDefaults = defaultDashboardV2Spec();
|
|
const [elements, layout] = getElementsFromPanels(dashboard.panels || []);
|
|
// @ts-expect-error - dashboard.templating.list is VariableModel[] and we need TypedVariableModel[] here
|
|
// that would allow accessing unique properties for each variable type that the API returns
|
|
const variables = getVariables(dashboard.templating?.list || []);
|
|
const annotations = getAnnotations(dashboard.annotations?.list || []);
|
|
|
|
let accessMeta: DashboardWithAccessInfo<DashboardV2Spec>['access'];
|
|
let annotationsMeta: DashboardWithAccessInfo<DashboardV2Spec>['metadata']['annotations'];
|
|
let labelsMeta: DashboardWithAccessInfo<DashboardV2Spec>['metadata']['labels'];
|
|
let creationTimestamp;
|
|
|
|
if (isDashboardResource(dto)) {
|
|
accessMeta = dto.access;
|
|
annotationsMeta = {
|
|
...dto.metadata.annotations,
|
|
[AnnoKeyDashboardGnetId]: dashboard.gnetId ?? undefined,
|
|
};
|
|
creationTimestamp = dto.metadata.creationTimestamp;
|
|
labelsMeta = {
|
|
[DeprecatedInternalId]: dto.metadata.labels?.[DeprecatedInternalId],
|
|
};
|
|
} else {
|
|
accessMeta = {
|
|
url: dto.meta.url,
|
|
slug: dto.meta.slug,
|
|
canSave: dto.meta.canSave,
|
|
canEdit: dto.meta.canEdit,
|
|
canDelete: dto.meta.canDelete,
|
|
canShare: dto.meta.canShare,
|
|
canStar: dto.meta.canStar,
|
|
canAdmin: dto.meta.canAdmin,
|
|
annotationsPermissions: dto.meta.annotationsPermissions,
|
|
};
|
|
annotationsMeta = {
|
|
[AnnoKeyCreatedBy]: dto.meta.createdBy,
|
|
[AnnoKeyUpdatedBy]: dto.meta.updatedBy,
|
|
[AnnoKeyUpdatedTimestamp]: dto.meta.updated,
|
|
[AnnoKeyFolder]: dto.meta.folderUid,
|
|
[AnnoKeySlug]: dto.meta.slug,
|
|
[AnnoKeyDashboardGnetId]: dashboard.gnetId ?? undefined,
|
|
[AnnoKeyDashboardIsSnapshot]: dto.meta.isSnapshot,
|
|
};
|
|
creationTimestamp = dto.meta.created;
|
|
labelsMeta = {
|
|
[DeprecatedInternalId]: dashboard.id?.toString() ?? undefined,
|
|
};
|
|
}
|
|
|
|
if (annotationsMeta?.[AnnoKeyDashboardIsSnapshot]) {
|
|
annotationsMeta[AnnoKeyDashboardSnapshotOriginalUrl] = dashboard.snapshot?.originalUrl;
|
|
}
|
|
|
|
const spec: DashboardV2Spec = {
|
|
title: dashboard.title,
|
|
description: dashboard.description,
|
|
tags: dashboard.tags ?? [],
|
|
cursorSync: transformCursorSynctoEnum(dashboard.graphTooltip),
|
|
preload: dashboard.preload || dashboardDefaults.preload,
|
|
liveNow: dashboard.liveNow,
|
|
editable: dashboard.editable,
|
|
revision: dashboard.revision,
|
|
timeSettings: {
|
|
from: dashboard.time?.from || timeSettingsDefaults.from,
|
|
to: dashboard.time?.to || timeSettingsDefaults.to,
|
|
timezone: dashboard.timezone || timeSettingsDefaults.timezone,
|
|
autoRefresh: dashboard.refresh || timeSettingsDefaults.autoRefresh,
|
|
autoRefreshIntervals: dashboard.timepicker?.refresh_intervals || timeSettingsDefaults.autoRefreshIntervals,
|
|
fiscalYearStartMonth: dashboard.fiscalYearStartMonth || timeSettingsDefaults.fiscalYearStartMonth,
|
|
hideTimepicker: dashboard.timepicker?.hidden || timeSettingsDefaults.hideTimepicker,
|
|
quickRanges: dashboard.timepicker?.quick_ranges,
|
|
weekStart: getWeekStart(dashboard.weekStart, timeSettingsDefaults.weekStart),
|
|
nowDelay: dashboard.timepicker?.nowDelay || timeSettingsDefaults.nowDelay,
|
|
},
|
|
links: dashboard.links || [],
|
|
annotations,
|
|
variables,
|
|
elements,
|
|
layout,
|
|
};
|
|
|
|
return {
|
|
apiVersion: 'v2alpha1',
|
|
kind: 'DashboardWithAccessInfo',
|
|
metadata: {
|
|
creationTimestamp: creationTimestamp || '', // TODO verify this empty string is valid
|
|
name: dashboard.uid,
|
|
resourceVersion: dashboard.version?.toString() || '0',
|
|
annotations: annotationsMeta,
|
|
labels: labelsMeta,
|
|
},
|
|
spec,
|
|
access: accessMeta,
|
|
};
|
|
}
|
|
|
|
export function ensureV1Response(
|
|
dashboard: DashboardDTO | DashboardWithAccessInfo<DashboardV2Spec> | DashboardWithAccessInfo<DashboardDataDTO>
|
|
): DashboardDTO {
|
|
// if dashboard is not on v1 schema or v2 schema, return as is
|
|
if (!isDashboardResource(dashboard)) {
|
|
return dashboard;
|
|
}
|
|
|
|
const spec = dashboard.spec;
|
|
// if dashboard is on v1 schema
|
|
if (isDashboardV0Spec(spec)) {
|
|
return {
|
|
meta: {
|
|
...dashboard.access,
|
|
isNew: false,
|
|
isFolder: false,
|
|
uid: dashboard.metadata.name,
|
|
k8s: dashboard.metadata,
|
|
version: dashboard.metadata.generation,
|
|
},
|
|
dashboard: spec,
|
|
};
|
|
} else {
|
|
// if dashboard is on v2 schema convert to v1 schema
|
|
const annotations = getAnnotationsV1(spec.annotations);
|
|
const variables = getVariablesV1(spec.variables);
|
|
const panels = getPanelsV1(spec.elements, spec.layout);
|
|
return {
|
|
meta: {
|
|
created: dashboard.metadata.creationTimestamp,
|
|
createdBy: dashboard.metadata.annotations?.[AnnoKeyCreatedBy] ?? '',
|
|
updated: dashboard.metadata.annotations?.[AnnoKeyUpdatedTimestamp],
|
|
updatedBy: dashboard.metadata.annotations?.[AnnoKeyUpdatedBy],
|
|
folderUid: dashboard.metadata.annotations?.[AnnoKeyFolder],
|
|
slug: dashboard.metadata.annotations?.[AnnoKeySlug],
|
|
url: dashboard.access.url,
|
|
canAdmin: dashboard.access.canAdmin,
|
|
canDelete: dashboard.access.canDelete,
|
|
canEdit: dashboard.access.canEdit,
|
|
canSave: dashboard.access.canSave,
|
|
canShare: dashboard.access.canShare,
|
|
canStar: dashboard.access.canStar,
|
|
annotationsPermissions: dashboard.access.annotationsPermissions,
|
|
},
|
|
dashboard: {
|
|
uid: dashboard.metadata.name,
|
|
title: spec.title,
|
|
description: spec.description,
|
|
tags: spec.tags,
|
|
schemaVersion: 40,
|
|
graphTooltip: transformCursorSyncV2ToV1(spec.cursorSync),
|
|
preload: spec.preload,
|
|
liveNow: spec.liveNow,
|
|
editable: spec.editable,
|
|
gnetId: dashboard.metadata.annotations?.[AnnoKeyDashboardGnetId],
|
|
revision: spec.revision,
|
|
time: {
|
|
from: spec.timeSettings.from,
|
|
to: spec.timeSettings.to,
|
|
},
|
|
timezone: spec.timeSettings.timezone,
|
|
refresh: spec.timeSettings.autoRefresh,
|
|
timepicker: {
|
|
refresh_intervals: spec.timeSettings.autoRefreshIntervals,
|
|
hidden: spec.timeSettings.hideTimepicker,
|
|
quick_ranges: spec.timeSettings.quickRanges,
|
|
nowDelay: spec.timeSettings.nowDelay,
|
|
},
|
|
fiscalYearStartMonth: spec.timeSettings.fiscalYearStartMonth,
|
|
weekStart: spec.timeSettings.weekStart,
|
|
version: dashboard.metadata.generation,
|
|
links: spec.links,
|
|
annotations: { list: annotations },
|
|
panels,
|
|
templating: { list: variables },
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
export const ResponseTransformers = {
|
|
ensureV2Response,
|
|
ensureV1Response,
|
|
};
|
|
|
|
function getElementsFromPanels(
|
|
panels: Array<Panel | RowPanel>
|
|
): [DashboardV2Spec['elements'], DashboardV2Spec['layout']] {
|
|
const elements: DashboardV2Spec['elements'] = {};
|
|
const layout: DashboardV2Spec['layout'] = {
|
|
kind: 'GridLayout',
|
|
spec: {
|
|
items: [],
|
|
},
|
|
};
|
|
|
|
if (!panels) {
|
|
return [elements, layout];
|
|
}
|
|
|
|
let currentRow: GridLayoutRowKind | null = null;
|
|
|
|
// iterate over panels
|
|
for (const p of panels) {
|
|
if (isRowPanel(p)) {
|
|
if (currentRow) {
|
|
// Flush current row to layout before we create a new one
|
|
layout.spec.items.push(currentRow);
|
|
}
|
|
|
|
const rowElements = [];
|
|
|
|
for (const panel of p.panels || []) {
|
|
const [element, name] = buildElement(panel);
|
|
elements[name] = element;
|
|
rowElements.push(buildGridItemKind(panel, name, yOffsetInRows(panel, p.gridPos!.y)));
|
|
}
|
|
|
|
currentRow = buildRowKind(p, rowElements);
|
|
} else {
|
|
const [element, elementName] = buildElement(p);
|
|
|
|
elements[elementName] = element;
|
|
|
|
if (currentRow) {
|
|
// Collect panels to current layout row
|
|
currentRow.spec.elements.push(buildGridItemKind(p, elementName, yOffsetInRows(p, currentRow.spec.y)));
|
|
} else {
|
|
layout.spec.items.push(buildGridItemKind(p, elementName));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentRow) {
|
|
// Flush last row to layout
|
|
layout.spec.items.push(currentRow);
|
|
}
|
|
|
|
return [elements, layout];
|
|
}
|
|
|
|
function isRowPanel(panel: Panel | RowPanel): panel is RowPanel {
|
|
return panel.type === 'row';
|
|
}
|
|
|
|
function getWeekStart(weekStart?: string, defaultWeekStart?: WeekStart): WeekStart | undefined {
|
|
if (!weekStart || !isWeekStart(weekStart)) {
|
|
return defaultWeekStart;
|
|
}
|
|
return weekStart;
|
|
}
|
|
|
|
function buildRowKind(p: RowPanel, elements: GridLayoutItemKind[]): GridLayoutRowKind {
|
|
return {
|
|
kind: 'GridLayoutRow',
|
|
spec: {
|
|
collapsed: p.collapsed,
|
|
title: p.title ?? '',
|
|
repeat: p.repeat ? { value: p.repeat, mode: 'variable' } : undefined,
|
|
y: p.gridPos?.y ?? 0,
|
|
elements,
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildGridItemKind(p: Panel, elementName: string, yOverride?: number): GridLayoutItemKind {
|
|
return {
|
|
kind: 'GridLayoutItem',
|
|
spec: {
|
|
x: p.gridPos!.x,
|
|
y: yOverride ?? p.gridPos!.y,
|
|
width: p.gridPos!.w,
|
|
height: p.gridPos!.h,
|
|
repeat: p.repeat
|
|
? { value: p.repeat, mode: 'variable', direction: p.repeatDirection, maxPerRow: p.maxPerRow }
|
|
: undefined,
|
|
element: {
|
|
kind: 'ElementReference',
|
|
name: elementName!,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function yOffsetInRows(p: Panel, rowY: number): number {
|
|
return p.gridPos!.y - rowY - GRID_ROW_HEIGHT;
|
|
}
|
|
|
|
function buildElement(p: Panel): [PanelKind | LibraryPanelKind, string] {
|
|
const element_identifier = `panel-${p.id}`;
|
|
|
|
if (p.libraryPanel) {
|
|
// LibraryPanelKind
|
|
const panelKind: LibraryPanelKind = {
|
|
kind: 'LibraryPanel',
|
|
spec: {
|
|
libraryPanel: {
|
|
uid: p.libraryPanel.uid,
|
|
name: p.libraryPanel.name,
|
|
},
|
|
id: p.id!,
|
|
title: p.title ?? '',
|
|
},
|
|
};
|
|
|
|
return [panelKind, element_identifier];
|
|
} else {
|
|
// PanelKind
|
|
|
|
const queries = getPanelQueries(
|
|
(p.targets as unknown as DataQuery[]) || [],
|
|
p.datasource || getDefaultDatasource()
|
|
);
|
|
|
|
const transformations = getPanelTransformations(p.transformations || []);
|
|
|
|
const panelKind: PanelKind = {
|
|
kind: 'Panel',
|
|
spec: {
|
|
title: p.title || '',
|
|
description: p.description || '',
|
|
vizConfig: {
|
|
kind: p.type,
|
|
spec: {
|
|
fieldConfig: (p.fieldConfig as any) || defaultFieldConfigSource(),
|
|
options: p.options as any,
|
|
pluginVersion: p.pluginVersion!,
|
|
},
|
|
},
|
|
links:
|
|
p.links?.map<DataLink>((l) => ({
|
|
title: l.title,
|
|
url: l.url || '',
|
|
targetBlank: l.targetBlank,
|
|
})) || [],
|
|
id: p.id!,
|
|
data: {
|
|
kind: 'QueryGroup',
|
|
spec: {
|
|
queries,
|
|
transformations,
|
|
queryOptions: {
|
|
cacheTimeout: p.cacheTimeout,
|
|
maxDataPoints: p.maxDataPoints,
|
|
interval: p.interval,
|
|
hideTimeOverride: p.hideTimeOverride,
|
|
queryCachingTTL: p.queryCachingTTL,
|
|
timeFrom: p.timeFrom,
|
|
timeShift: p.timeShift,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
return [panelKind, element_identifier];
|
|
}
|
|
}
|
|
|
|
function getDefaultDatasourceType() {
|
|
// if there is no default datasource, return 'grafana' as default
|
|
return getDefaultDataSourceRef()?.type ?? 'grafana';
|
|
}
|
|
|
|
export function getDefaultDatasource(): DataSourceRef {
|
|
const configDefaultDS = getDefaultDataSourceRef() ?? { type: 'grafana', uid: '-- Grafana --' };
|
|
|
|
if (configDefaultDS.uid && !configDefaultDS.apiVersion) {
|
|
// get api version from config
|
|
const dsInstance = config.bootData.settings.datasources[configDefaultDS.uid];
|
|
configDefaultDS.apiVersion = dsInstance.apiVersion ?? undefined;
|
|
}
|
|
|
|
return {
|
|
apiVersion: configDefaultDS.apiVersion,
|
|
type: configDefaultDS.type,
|
|
uid: configDefaultDS.uid,
|
|
};
|
|
}
|
|
|
|
export function getPanelQueries(targets: DataQuery[], panelDatasource: DataSourceRef): PanelQueryKind[] {
|
|
return targets.map((t) => {
|
|
const { refId, hide, datasource, ...query } = t;
|
|
const q: PanelQueryKind = {
|
|
kind: 'PanelQuery',
|
|
spec: {
|
|
refId: t.refId,
|
|
hidden: t.hide ?? false,
|
|
datasource: t.datasource ? t.datasource : panelDatasource,
|
|
query: {
|
|
kind: t.datasource?.type || panelDatasource.type!,
|
|
spec: {
|
|
...query,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
return q;
|
|
});
|
|
}
|
|
|
|
function getPanelTransformations(transformations: DataTransformerConfig[]): TransformationKind[] {
|
|
return transformations.map((t) => {
|
|
return {
|
|
kind: t.id,
|
|
spec: {
|
|
...t,
|
|
topic: transformDataTopic(t.topic),
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables'] {
|
|
const variables: DashboardV2Spec['variables'] = [];
|
|
for (const v of vars) {
|
|
const commonProperties = {
|
|
name: v.name,
|
|
label: v.label,
|
|
...(v.description && { description: v.description }),
|
|
skipUrlSync: Boolean(v.skipUrlSync),
|
|
hide: transformVariableHideToEnum(v.hide),
|
|
};
|
|
|
|
switch (v.type) {
|
|
case 'query':
|
|
let query = v.query || {};
|
|
|
|
if (typeof query === 'string') {
|
|
console.warn(
|
|
'Query variable query is a string which is deprecated in the schema v2. It should extend DataQuery'
|
|
);
|
|
query = {
|
|
[LEGACY_STRING_VALUE_KEY]: query,
|
|
};
|
|
}
|
|
|
|
const qv: QueryVariableKind = {
|
|
kind: 'QueryVariable',
|
|
spec: {
|
|
...commonProperties,
|
|
multi: Boolean(v.multi),
|
|
includeAll: Boolean(v.includeAll),
|
|
...(v.allValue && { allValue: v.allValue }),
|
|
current: {
|
|
value: v.current.value,
|
|
text: v.current.text,
|
|
},
|
|
options: v.options || [],
|
|
refresh: transformVariableRefreshToEnum(v.refresh),
|
|
...(v.datasource && { datasource: v.datasource }),
|
|
regex: v.regex || '',
|
|
sort: transformSortVariableToEnum(v.sort),
|
|
query: {
|
|
kind: v.datasource?.type || getDefaultDatasourceType(),
|
|
spec: query,
|
|
},
|
|
},
|
|
};
|
|
variables.push(qv);
|
|
break;
|
|
case 'datasource':
|
|
let pluginId = getDefaultDatasourceType();
|
|
|
|
if (v.query && typeof v.query === 'string') {
|
|
pluginId = v.query;
|
|
}
|
|
|
|
const dv: DatasourceVariableKind = {
|
|
kind: 'DatasourceVariable',
|
|
spec: {
|
|
...commonProperties,
|
|
multi: Boolean(v.multi),
|
|
includeAll: Boolean(v.includeAll),
|
|
...(v.allValue && { allValue: v.allValue }),
|
|
current: {
|
|
value: v.current.value,
|
|
text: v.current.text,
|
|
},
|
|
options: v.options || [],
|
|
refresh: transformVariableRefreshToEnum(v.refresh),
|
|
pluginId,
|
|
regex: v.regex || '',
|
|
},
|
|
};
|
|
variables.push(dv);
|
|
break;
|
|
case 'custom':
|
|
const cv: CustomVariableKind = {
|
|
kind: 'CustomVariable',
|
|
spec: {
|
|
...commonProperties,
|
|
query: v.query,
|
|
current: {
|
|
value: v.current.value,
|
|
text: v.current.text,
|
|
},
|
|
options: v.options,
|
|
multi: v.multi,
|
|
includeAll: v.includeAll,
|
|
...(v.allValue && { allValue: v.allValue }),
|
|
},
|
|
};
|
|
variables.push(cv);
|
|
break;
|
|
case 'adhoc':
|
|
const av: AdhocVariableKind = {
|
|
kind: 'AdhocVariable',
|
|
spec: {
|
|
...commonProperties,
|
|
datasource: v.datasource || getDefaultDatasource(),
|
|
baseFilters: v.baseFilters || [],
|
|
filters: v.filters || [],
|
|
defaultKeys: v.defaultKeys || [],
|
|
},
|
|
};
|
|
variables.push(av);
|
|
break;
|
|
case 'constant':
|
|
const cnts: ConstantVariableKind = {
|
|
kind: 'ConstantVariable',
|
|
spec: {
|
|
...commonProperties,
|
|
current: {
|
|
value: v.current.value,
|
|
// Constant variable doesn't use text state
|
|
text: v.current.value,
|
|
},
|
|
query: v.query,
|
|
},
|
|
};
|
|
variables.push(cnts);
|
|
break;
|
|
case 'interval':
|
|
const intrv: IntervalVariableKind = {
|
|
kind: 'IntervalVariable',
|
|
spec: {
|
|
...commonProperties,
|
|
current: {
|
|
value: v.current.value,
|
|
// Interval variable doesn't use text state
|
|
text: v.current.value,
|
|
},
|
|
query: v.query,
|
|
refresh: 'onTimeRangeChanged',
|
|
options: v.options,
|
|
auto: v.auto,
|
|
auto_min: v.auto_min,
|
|
auto_count: v.auto_count,
|
|
},
|
|
};
|
|
variables.push(intrv);
|
|
break;
|
|
case 'textbox':
|
|
const tx: TextVariableKind = {
|
|
kind: 'TextVariable',
|
|
spec: {
|
|
...commonProperties,
|
|
current: {
|
|
value: v.current.value,
|
|
// Text variable doesn't use text state
|
|
text: v.current.value,
|
|
},
|
|
query: v.query,
|
|
},
|
|
};
|
|
variables.push(tx);
|
|
break;
|
|
case 'groupby':
|
|
const gb: GroupByVariableKind = {
|
|
kind: 'GroupByVariable',
|
|
spec: {
|
|
...commonProperties,
|
|
datasource: v.datasource || getDefaultDatasource(),
|
|
options: v.options,
|
|
current: {
|
|
value: v.current.value,
|
|
text: v.current.text,
|
|
},
|
|
multi: v.multi,
|
|
},
|
|
};
|
|
variables.push(gb);
|
|
break;
|
|
default:
|
|
// do not throw error, just log it
|
|
console.error(`Variable transformation not implemented: ${v.type}`);
|
|
}
|
|
}
|
|
return variables;
|
|
}
|
|
|
|
function getAnnotations(annotations: AnnotationQuery[]): DashboardV2Spec['annotations'] {
|
|
return annotations.map((a) => {
|
|
const aq: AnnotationQueryKind = {
|
|
kind: 'AnnotationQuery',
|
|
spec: {
|
|
name: a.name,
|
|
...(a.datasource && { datasource: a.datasource }),
|
|
enable: a.enable,
|
|
hide: Boolean(a.hide),
|
|
iconColor: a.iconColor,
|
|
builtIn: Boolean(a.builtIn),
|
|
query: {
|
|
kind: a.datasource?.type || getDefaultDatasourceType(),
|
|
spec: {
|
|
...a.target,
|
|
},
|
|
},
|
|
filter: a.filter,
|
|
},
|
|
};
|
|
return aq;
|
|
});
|
|
}
|
|
|
|
function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] {
|
|
const variables: VariableModel[] = [];
|
|
|
|
for (const v of vars) {
|
|
const commonProperties = {
|
|
name: v.spec.name,
|
|
label: v.spec.label,
|
|
...(v.spec.description && { description: v.spec.description }),
|
|
skipUrlSync: v.spec.skipUrlSync,
|
|
hide: transformVariableHideToEnumV1(v.spec.hide),
|
|
type: transformToV1VariableTypes(v),
|
|
};
|
|
|
|
switch (v.kind) {
|
|
case 'QueryVariable':
|
|
const qv: VariableModel = {
|
|
...commonProperties,
|
|
current: v.spec.current,
|
|
options: v.spec.options,
|
|
query:
|
|
LEGACY_STRING_VALUE_KEY in v.spec.query.spec
|
|
? v.spec.query.spec[LEGACY_STRING_VALUE_KEY]
|
|
: v.spec.query.spec,
|
|
datasource: v.spec.datasource,
|
|
sort: transformSortVariableToEnumV1(v.spec.sort),
|
|
refresh: transformVariableRefreshToEnumV1(v.spec.refresh),
|
|
regex: v.spec.regex,
|
|
allValue: v.spec.allValue,
|
|
includeAll: v.spec.includeAll,
|
|
multi: v.spec.multi,
|
|
// @ts-expect-error - definition is not part of v1 VariableModel
|
|
definition: v.spec.definition,
|
|
};
|
|
variables.push(qv);
|
|
break;
|
|
case 'DatasourceVariable':
|
|
const dv: VariableModel = {
|
|
...commonProperties,
|
|
current: v.spec.current,
|
|
options: [],
|
|
regex: v.spec.regex,
|
|
refresh: transformVariableRefreshToEnumV1(v.spec.refresh),
|
|
query: v.spec.pluginId,
|
|
multi: v.spec.multi,
|
|
allValue: v.spec.allValue,
|
|
includeAll: v.spec.includeAll,
|
|
};
|
|
variables.push(dv);
|
|
break;
|
|
case 'CustomVariable':
|
|
const cv: VariableModel = {
|
|
...commonProperties,
|
|
current: {
|
|
text: v.spec.current.value,
|
|
value: v.spec.current.value,
|
|
},
|
|
options: v.spec.options,
|
|
query: v.spec.query,
|
|
multi: v.spec.multi,
|
|
allValue: v.spec.allValue,
|
|
includeAll: v.spec.includeAll,
|
|
};
|
|
variables.push(cv);
|
|
break;
|
|
case 'ConstantVariable':
|
|
const constant: VariableModel = {
|
|
...commonProperties,
|
|
current: {
|
|
text: v.spec.current.value,
|
|
value: v.spec.current.value,
|
|
},
|
|
hide: transformVariableHideToEnumV1(v.spec.hide),
|
|
// @ts-expect-error
|
|
query: v.spec.current.value,
|
|
};
|
|
variables.push(constant);
|
|
break;
|
|
case 'IntervalVariable':
|
|
const iv: VariableModel = {
|
|
...commonProperties,
|
|
current: {
|
|
text: v.spec.current.value,
|
|
value: v.spec.current.value,
|
|
},
|
|
hide: transformVariableHideToEnumV1(v.spec.hide),
|
|
query: v.spec.query,
|
|
refresh: transformVariableRefreshToEnumV1(v.spec.refresh),
|
|
options: v.spec.options,
|
|
// @ts-expect-error
|
|
auto: v.spec.auto,
|
|
auto_min: v.spec.auto_min,
|
|
auto_count: v.spec.auto_count,
|
|
};
|
|
variables.push(iv);
|
|
break;
|
|
case 'TextVariable':
|
|
const current = {
|
|
text: v.spec.current.value,
|
|
value: v.spec.current.value,
|
|
};
|
|
|
|
const tv: VariableModel = {
|
|
...commonProperties,
|
|
current: {
|
|
text: v.spec.current.value,
|
|
value: v.spec.current.value,
|
|
},
|
|
options: [{ ...current, selected: true }],
|
|
query: v.spec.query,
|
|
};
|
|
variables.push(tv);
|
|
break;
|
|
case 'GroupByVariable':
|
|
const gv: VariableModel = {
|
|
...commonProperties,
|
|
datasource: v.spec.datasource,
|
|
current: v.spec.current,
|
|
options: v.spec.options,
|
|
};
|
|
variables.push(gv);
|
|
break;
|
|
case 'AdhocVariable':
|
|
const av: VariableModel = {
|
|
...commonProperties,
|
|
datasource: v.spec.datasource,
|
|
// @ts-expect-error
|
|
baseFilters: v.spec.baseFilters,
|
|
filters: v.spec.filters,
|
|
defaultKeys: v.spec.defaultKeys,
|
|
};
|
|
variables.push(av);
|
|
break;
|
|
default:
|
|
// do not throw error, just log it
|
|
console.error(`Variable transformation not implemented: ${v}`);
|
|
}
|
|
}
|
|
return variables;
|
|
}
|
|
|
|
function getAnnotationsV1(annotations: DashboardV2Spec['annotations']): AnnotationQuery[] {
|
|
// @ts-expect-error - target v2 query is not compatible with v1 target
|
|
return annotations.map((a) => {
|
|
return {
|
|
name: a.spec.name,
|
|
datasource: a.spec.datasource,
|
|
enable: a.spec.enable,
|
|
hide: a.spec.hide,
|
|
iconColor: a.spec.iconColor,
|
|
builtIn: a.spec.builtIn ? 1 : 0,
|
|
target: a.spec.query?.spec,
|
|
filter: a.spec.filter,
|
|
};
|
|
});
|
|
}
|
|
|
|
interface LibraryPanelDTO extends Pick<Panel, 'libraryPanel' | 'id' | 'title' | 'gridPos' | 'type'> {}
|
|
|
|
function getPanelsV1(
|
|
panels: DashboardV2Spec['elements'],
|
|
layout: DashboardV2Spec['layout']
|
|
): Array<Panel | LibraryPanelDTO> {
|
|
const panelsV1: Array<Panel | LibraryPanelDTO | RowPanel> = [];
|
|
|
|
let maxPanelId = 0;
|
|
|
|
if (layout.kind !== 'GridLayout') {
|
|
throw new Error('Cannot convert non-GridLayout layout to v1');
|
|
}
|
|
|
|
for (const item of layout.spec.items) {
|
|
if (item.kind === 'GridLayoutItem') {
|
|
const panel = panels[item.spec.element.name];
|
|
const v1Panel = transformV2PanelToV1Panel(panel, item);
|
|
panelsV1.push(v1Panel);
|
|
if (v1Panel.id ?? 0 > maxPanelId) {
|
|
maxPanelId = v1Panel.id ?? 0;
|
|
}
|
|
} else if (item.kind === 'GridLayoutRow') {
|
|
const row: RowPanel = {
|
|
id: -1, // Temporarily set to -1, updated later to be unique
|
|
type: 'row',
|
|
title: item.spec.title,
|
|
collapsed: item.spec.collapsed,
|
|
repeat: item.spec.repeat ? item.spec.repeat.value : undefined,
|
|
gridPos: {
|
|
x: 0,
|
|
y: item.spec.y,
|
|
w: 24,
|
|
h: GRID_ROW_HEIGHT,
|
|
},
|
|
panels: [],
|
|
};
|
|
|
|
const rowPanels = [];
|
|
for (const panel of item.spec.elements) {
|
|
const panelElement = panels[panel.spec.element.name];
|
|
const v1Panel = transformV2PanelToV1Panel(panelElement, panel, item.spec.y + GRID_ROW_HEIGHT + panel.spec.y);
|
|
rowPanels.push(v1Panel);
|
|
if (v1Panel.id ?? 0 > maxPanelId) {
|
|
maxPanelId = v1Panel.id ?? 0;
|
|
}
|
|
}
|
|
if (item.spec.collapsed) {
|
|
// When a row is collapsed, panels inside it are stored in the panels property.
|
|
row.panels = rowPanels;
|
|
panelsV1.push(row);
|
|
} else {
|
|
panelsV1.push(row);
|
|
panelsV1.push(...rowPanels);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update row panel ids to be unique
|
|
for (const panel of panelsV1) {
|
|
if (panel.type === 'row' && panel.id === -1) {
|
|
panel.id = ++maxPanelId;
|
|
}
|
|
}
|
|
return panelsV1;
|
|
}
|
|
|
|
function transformV2PanelToV1Panel(
|
|
p: PanelKind | LibraryPanelKind,
|
|
layoutElement: GridLayoutItemKind,
|
|
yOverride?: number
|
|
): Panel | LibraryPanelDTO {
|
|
const { x, y, width, height, repeat } = layoutElement?.spec || { x: 0, y: 0, width: 0, height: 0 };
|
|
const gridPos = { x, y: yOverride ?? y, w: width, h: height };
|
|
if (p.kind === 'Panel') {
|
|
const panel = p.spec;
|
|
return {
|
|
id: panel.id,
|
|
type: panel.vizConfig.kind,
|
|
title: panel.title,
|
|
description: panel.description,
|
|
fieldConfig: transformMappingsToV1(panel.vizConfig.spec.fieldConfig),
|
|
options: panel.vizConfig.spec.options,
|
|
pluginVersion: panel.vizConfig.spec.pluginVersion,
|
|
links:
|
|
// @ts-expect-error - Panel link is wrongly typed as DashboardLink
|
|
panel.links?.map<DashboardLink>((l) => ({
|
|
title: l.title,
|
|
url: l.url,
|
|
...(l.targetBlank && { targetBlank: l.targetBlank }),
|
|
})) || [],
|
|
targets: panel.data.spec.queries.map((q) => {
|
|
return {
|
|
refId: q.spec.refId,
|
|
hide: q.spec.hidden,
|
|
datasource: q.spec.datasource,
|
|
...q.spec.query.spec,
|
|
};
|
|
}),
|
|
transformations: panel.data.spec.transformations.map((t) => t.spec),
|
|
gridPos,
|
|
cacheTimeout: panel.data.spec.queryOptions.cacheTimeout,
|
|
maxDataPoints: panel.data.spec.queryOptions.maxDataPoints,
|
|
interval: panel.data.spec.queryOptions.interval,
|
|
hideTimeOverride: panel.data.spec.queryOptions.hideTimeOverride,
|
|
queryCachingTTL: panel.data.spec.queryOptions.queryCachingTTL,
|
|
timeFrom: panel.data.spec.queryOptions.timeFrom,
|
|
timeShift: panel.data.spec.queryOptions.timeShift,
|
|
transparent: panel.transparent,
|
|
...(repeat?.value && { repeat: repeat.value }),
|
|
...(repeat?.direction && { repeatDirection: repeat.direction }),
|
|
...(repeat?.maxPerRow && { maxPerRow: repeat.maxPerRow }),
|
|
};
|
|
} else if (p.kind === 'LibraryPanel') {
|
|
const panel = p.spec;
|
|
return {
|
|
id: panel.id,
|
|
title: panel.title,
|
|
gridPos,
|
|
libraryPanel: {
|
|
uid: panel.libraryPanel.uid,
|
|
name: panel.libraryPanel.name,
|
|
},
|
|
type: 'library-panel-ref',
|
|
};
|
|
} else {
|
|
throw new Error(`Unknown element kind: ${p}`);
|
|
}
|
|
}
|
|
|
|
export function transformMappingsToV1(fieldConfig: FieldConfigSource): FieldConfigSourceV1 {
|
|
const getThresholdsMode = (mode: ThresholdsMode): ThresholdsModeV1 => {
|
|
switch (mode) {
|
|
case 'absolute':
|
|
return ThresholdsModeV1.Absolute;
|
|
case 'percentage':
|
|
return ThresholdsModeV1.Percentage;
|
|
default:
|
|
return ThresholdsModeV1.Absolute;
|
|
}
|
|
};
|
|
|
|
const transformedDefaults: any = {
|
|
...fieldConfig.defaults,
|
|
};
|
|
|
|
if (fieldConfig.defaults.mappings) {
|
|
transformedDefaults.mappings = fieldConfig.defaults.mappings.map((mapping) => {
|
|
switch (mapping.type) {
|
|
case 'value':
|
|
return {
|
|
...mapping,
|
|
type: MappingTypeV1.ValueToText,
|
|
};
|
|
case 'range':
|
|
return {
|
|
...mapping,
|
|
type: MappingTypeV1.RangeToText,
|
|
};
|
|
case 'regex':
|
|
return {
|
|
...mapping,
|
|
type: MappingTypeV1.RegexToText,
|
|
};
|
|
case 'special':
|
|
return {
|
|
...mapping,
|
|
options: {
|
|
...mapping.options,
|
|
match: transformSpecialValueMatchToV1(mapping.options.match),
|
|
},
|
|
type: MappingTypeV1.SpecialValue,
|
|
};
|
|
default:
|
|
return mapping;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (fieldConfig.defaults.thresholds) {
|
|
transformedDefaults.thresholds = {
|
|
...fieldConfig.defaults.thresholds,
|
|
mode: getThresholdsMode(fieldConfig.defaults.thresholds.mode),
|
|
};
|
|
}
|
|
|
|
if (fieldConfig.defaults.color?.mode) {
|
|
transformedDefaults.color = {
|
|
...fieldConfig.defaults.color,
|
|
mode: colorIdToEnumv1(fieldConfig.defaults.color.mode),
|
|
};
|
|
}
|
|
|
|
return {
|
|
...fieldConfig,
|
|
defaults: transformedDefaults,
|
|
};
|
|
}
|
|
|
|
function colorIdToEnumv1(colorId: FieldColorModeId): FieldColorModeIdV1 {
|
|
switch (colorId) {
|
|
case 'thresholds':
|
|
return FieldColorModeIdV1.Thresholds;
|
|
case 'palette-classic':
|
|
return FieldColorModeIdV1.PaletteClassic;
|
|
case 'palette-classic-by-name':
|
|
return FieldColorModeIdV1.PaletteClassicByName;
|
|
case 'continuous-GrYlRd':
|
|
return FieldColorModeIdV1.ContinuousGrYlRd;
|
|
case 'continuous-RdYlGr':
|
|
return FieldColorModeIdV1.ContinuousRdYlGr;
|
|
case 'continuous-BlYlRd':
|
|
return FieldColorModeIdV1.ContinuousBlYlRd;
|
|
case 'continuous-YlRd':
|
|
return FieldColorModeIdV1.ContinuousYlRd;
|
|
case 'continuous-BlPu':
|
|
return FieldColorModeIdV1.ContinuousBlPu;
|
|
case 'continuous-YlBl':
|
|
return FieldColorModeIdV1.ContinuousYlBl;
|
|
case 'continuous-blues':
|
|
return FieldColorModeIdV1.ContinuousBlues;
|
|
case 'continuous-reds':
|
|
return FieldColorModeIdV1.ContinuousReds;
|
|
case 'continuous-greens':
|
|
return FieldColorModeIdV1.ContinuousGreens;
|
|
case 'continuous-purples':
|
|
return FieldColorModeIdV1.ContinuousPurples;
|
|
case 'fixed':
|
|
return FieldColorModeIdV1.Fixed;
|
|
case 'shades':
|
|
return FieldColorModeIdV1.Shades;
|
|
default:
|
|
return FieldColorModeIdV1.Thresholds;
|
|
}
|
|
}
|
|
|
|
function transformSpecialValueMatchToV1(match: SpecialValueMatch): SpecialValueMatchV1 {
|
|
switch (match) {
|
|
case 'true':
|
|
return SpecialValueMatchV1.True;
|
|
case 'false':
|
|
return SpecialValueMatchV1.False;
|
|
case 'null':
|
|
return SpecialValueMatchV1.Null;
|
|
case 'nan':
|
|
return SpecialValueMatchV1.NaN;
|
|
case 'null+nan':
|
|
return SpecialValueMatchV1.NullAndNan;
|
|
case 'empty':
|
|
return SpecialValueMatchV1.Empty;
|
|
default:
|
|
throw new Error(`Unknown match type: ${match}`);
|
|
}
|
|
}
|
|
|
|
function transformToV1VariableTypes(variable: TypedVariableModelV2): VariableType {
|
|
switch (variable.kind) {
|
|
case 'QueryVariable':
|
|
return 'query';
|
|
case 'DatasourceVariable':
|
|
return 'datasource';
|
|
case 'CustomVariable':
|
|
return 'custom';
|
|
case 'ConstantVariable':
|
|
return 'constant';
|
|
case 'IntervalVariable':
|
|
return 'interval';
|
|
case 'TextVariable':
|
|
return 'textbox';
|
|
case 'GroupByVariable':
|
|
return 'groupby';
|
|
case 'AdhocVariable':
|
|
return 'adhoc';
|
|
default:
|
|
throw new Error(`Unknown variable type: ${variable}`);
|
|
}
|
|
}
|
|
|