Schema V2 to DashboardScene: Settings + Panels (#96074)
* Transform Save mode to Scene: Settings * Transform Save mode to Scene: Panels --------- Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> Co-authored-by: alexandra vargas <alexa1866@gmail.com> Co-authored-by: Haris Rozajac <haris.rozajac12@gmail.com>
This commit is contained in:
co-authored by
Dominik Prokop
alexandra vargas
Haris Rozajac
parent
16c78f6a98
commit
d18cdae3e2
@@ -124,6 +124,14 @@ export interface DataSourceRef {
|
||||
export const defaultDataSourceRef = (): DataSourceRef => ({
|
||||
});
|
||||
|
||||
export enum DataTopic {
|
||||
AlertStates = "alertStates",
|
||||
Annotations = "annotations",
|
||||
Series = "series",
|
||||
}
|
||||
|
||||
export const defaultDataTopic = (): DataTopic => (DataTopic.AlertStates);
|
||||
|
||||
// Transformations allow to manipulate data returned by a query before the system applies a visualization.
|
||||
// Using transformations you can: rename fields, join time series data, perform mathematical operations across queries,
|
||||
// use the output of one transformation as the input to another transformation, etc.
|
||||
@@ -135,8 +143,7 @@ export interface DataTransformerConfig {
|
||||
// Optional frame matcher. When missing it will be applied to all results
|
||||
filter?: MatcherConfig;
|
||||
// Where to pull DataFrames from as input to transformation
|
||||
// replaced with common.DataTopic
|
||||
topic?: "series" | "annotations" | "alertStates";
|
||||
topic?: DataTopic;
|
||||
// Options to be passed to the transformer
|
||||
// Valid options depend on the transformer id
|
||||
options: any;
|
||||
@@ -247,7 +254,7 @@ export const defaultMatcherConfig = (): MatcherConfig => ({
|
||||
});
|
||||
|
||||
export interface Threshold {
|
||||
value: number | null;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
@@ -283,13 +290,13 @@ export const defaultValueMapping = (): ValueMapping => (defaultValueMap());
|
||||
// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.
|
||||
// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.
|
||||
export enum MappingType {
|
||||
Value = "value",
|
||||
Range = "range",
|
||||
Regex = "regex",
|
||||
Special = "special",
|
||||
ValueToText = "value",
|
||||
RangeToText = "range",
|
||||
RegexToText = "regex",
|
||||
SpecialValue = "special",
|
||||
}
|
||||
|
||||
export const defaultMappingType = (): MappingType => (MappingType.Value);
|
||||
export const defaultMappingType = (): MappingType => (MappingType.ValueToText);
|
||||
|
||||
// Maps text values to a color or different display text and color.
|
||||
// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
|
||||
@@ -376,7 +383,7 @@ export enum SpecialValueMatch {
|
||||
False = "false",
|
||||
Null = "null",
|
||||
NotANumber = "nan",
|
||||
NullNan = "null+nan",
|
||||
NullAndNaN = "null+nan",
|
||||
Empty = "empty",
|
||||
}
|
||||
|
||||
@@ -542,6 +549,7 @@ export interface QueryOptionsSpec {
|
||||
queryCachingTTL?: number;
|
||||
interval?: string;
|
||||
cacheTimeout?: string;
|
||||
hideTimeOverride?: boolean;
|
||||
}
|
||||
|
||||
export const defaultQueryOptionsSpec = (): QueryOptionsSpec => ({
|
||||
|
||||
@@ -102,6 +102,8 @@ DataSourceRef: {
|
||||
uid?: string
|
||||
}
|
||||
|
||||
DataTopic: "alertStates" | "annotations" | "series"
|
||||
|
||||
// Transformations allow to manipulate data returned by a query before the system applies a visualization.
|
||||
// Using transformations you can: rename fields, join time series data, perform mathematical operations across queries,
|
||||
// use the output of one transformation as the input to another transformation, etc.
|
||||
@@ -113,7 +115,7 @@ DataTransformerConfig: {
|
||||
// Optional frame matcher. When missing it will be applied to all results
|
||||
filter?: MatcherConfig
|
||||
// Where to pull DataFrames from as input to transformation
|
||||
topic?: "series" | "annotations" | "alertStates" // replaced with common.DataTopic
|
||||
topic?: DataTopic
|
||||
// Options to be passed to the transformer
|
||||
// Valid options depend on the transformer id
|
||||
options: _
|
||||
@@ -217,7 +219,7 @@ MatcherConfig: {
|
||||
}
|
||||
|
||||
Threshold: {
|
||||
value: number | null
|
||||
value: number
|
||||
color: string
|
||||
}
|
||||
|
||||
@@ -235,7 +237,7 @@ ValueMapping: ValueMap | RangeMap | RegexMap | SpecialValueMap
|
||||
// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.
|
||||
// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.
|
||||
// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.
|
||||
MappingType: "value" | "range" | "regex" | "special"
|
||||
MappingType: "value" | "range" | "regex" | "special" @cog(kind="enum",memberNames="ValueToText|RangeToText|RegexToText|SpecialValue")
|
||||
|
||||
// Maps text values to a color or different display text and color.
|
||||
// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
|
||||
@@ -287,7 +289,7 @@ SpecialValueMap: {
|
||||
}
|
||||
|
||||
// Special value types supported by the `SpecialValueMap`
|
||||
SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty"
|
||||
SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" @cog(kind="enum",memberNames="True|False|Null|NaN|NullAndNaN|Empty")
|
||||
|
||||
// Result used as replacement with text and color when the value matches
|
||||
ValueMappingResult: {
|
||||
@@ -383,6 +385,7 @@ QueryOptionsSpec: {
|
||||
queryCachingTTL?: int
|
||||
interval?: string
|
||||
cacheTimeout?: string
|
||||
hideTimeOverride?: bool
|
||||
}
|
||||
|
||||
DataQueryKind: {
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
import { DashboardCursorSync, DashboardV2Spec } from './dashboard.gen';
|
||||
import {
|
||||
DashboardCursorSync,
|
||||
DashboardLinkType,
|
||||
DashboardV2Spec,
|
||||
VariableHide,
|
||||
VariableRefresh,
|
||||
VariableSort,
|
||||
} from './dashboard.gen';
|
||||
|
||||
export const handyTestingSchema: DashboardV2Spec = {
|
||||
id: 1,
|
||||
title: 'Default Dashboard',
|
||||
description: 'This is a default dashboard',
|
||||
cursorSync: DashboardCursorSync.Off,
|
||||
liveNow: false,
|
||||
preload: false,
|
||||
title: 'Test Dashboard',
|
||||
description: 'Test Description',
|
||||
editable: true,
|
||||
links: [],
|
||||
tags: [],
|
||||
schemaVersion: 39,
|
||||
preload: true,
|
||||
schemaVersion: 40,
|
||||
tags: ['tag1', 'tag2'],
|
||||
liveNow: true,
|
||||
cursorSync: DashboardCursorSync.Crosshair,
|
||||
timeSettings: {
|
||||
timezone: 'browser',
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
autoRefresh: '10s',
|
||||
autoRefreshIntervals: ['10s', '1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d'],
|
||||
quickRanges: ['now/d', 'now/w', 'now/M', 'now/y'],
|
||||
hideTimepicker: false,
|
||||
weekStart: 'sunday',
|
||||
autoRefresh: '5s',
|
||||
autoRefreshIntervals: ['5s', '10s', '30s'],
|
||||
fiscalYearStartMonth: 1,
|
||||
from: 'now-1h',
|
||||
hideTimepicker: false,
|
||||
nowDelay: '1m',
|
||||
quickRanges: [],
|
||||
timezone: 'UTC',
|
||||
to: 'now',
|
||||
weekStart: 'monday',
|
||||
},
|
||||
|
||||
annotations: [],
|
||||
elements: {
|
||||
timeSeriesTest: {
|
||||
'test-panel-uid': {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
title: 'Time Series Test',
|
||||
description: 'This is a test panel',
|
||||
uid: 'timeSeriesTest',
|
||||
links: [],
|
||||
data: {
|
||||
kind: 'QueryGroup',
|
||||
spec: {
|
||||
@@ -38,44 +40,63 @@ export const handyTestingSchema: DashboardV2Spec = {
|
||||
{
|
||||
kind: 'PanelQuery',
|
||||
spec: {
|
||||
refId: 'A',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'datasource1',
|
||||
},
|
||||
query: {
|
||||
kind: 'prometheus',
|
||||
spec: {
|
||||
query: 'up',
|
||||
expr: 'test-query',
|
||||
},
|
||||
},
|
||||
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
|
||||
hidden: false,
|
||||
refId: 'A',
|
||||
},
|
||||
},
|
||||
],
|
||||
queryOptions: {
|
||||
timeFrom: '1h',
|
||||
maxDataPoints: 100,
|
||||
timeShift: '1h',
|
||||
queryCachingTTL: 60,
|
||||
interval: '1m',
|
||||
cacheTimeout: '1m',
|
||||
hideTimeOverride: false,
|
||||
},
|
||||
transformations: [
|
||||
{
|
||||
kind: 'limit',
|
||||
spec: {
|
||||
id: 'limit', // id is competing w/ kind
|
||||
id: 'limit',
|
||||
disabled: false,
|
||||
filter: {
|
||||
id: 'byValue',
|
||||
options: {
|
||||
reducer: 'sum',
|
||||
},
|
||||
},
|
||||
options: {
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
queryOptions: {
|
||||
maxDataPoints: 100,
|
||||
cacheTimeout: '1m',
|
||||
},
|
||||
},
|
||||
},
|
||||
description: 'Test Description',
|
||||
links: [],
|
||||
title: 'Test Panel',
|
||||
uid: 'test-panel-uid',
|
||||
vizConfig: {
|
||||
kind: 'timeseries',
|
||||
spec: {
|
||||
pluginVersion: '11.0.0',
|
||||
options: {},
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
options: {},
|
||||
pluginVersion: '7.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -88,62 +109,252 @@ export const handyTestingSchema: DashboardV2Spec = {
|
||||
{
|
||||
kind: 'GridLayoutItem',
|
||||
spec: {
|
||||
element: { kind: 'ElementReference', name: 'timeSeriesTest' },
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: 'test-panel-uid',
|
||||
},
|
||||
height: 0,
|
||||
width: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 12,
|
||||
height: 6,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
variables: [],
|
||||
annotations: [
|
||||
links: [
|
||||
{
|
||||
kind: 'AnnotationQuery',
|
||||
asDropdown: false,
|
||||
icon: '',
|
||||
includeVars: false,
|
||||
keepTime: false,
|
||||
tags: [],
|
||||
targetBlank: false,
|
||||
title: 'Test Link',
|
||||
tooltip: '',
|
||||
type: DashboardLinkType.Dashboards,
|
||||
url: 'http://test.com',
|
||||
},
|
||||
],
|
||||
|
||||
variables: [
|
||||
{
|
||||
kind: 'QueryVariable',
|
||||
spec: {
|
||||
datasource: { type: 'datasource', uid: 'grafana' },
|
||||
query: {
|
||||
kind: 'grafana',
|
||||
spec: {
|
||||
queryType: 'timeRegions',
|
||||
matchAny: false,
|
||||
timeRegion: {
|
||||
from: '12:27',
|
||||
fromDayOfWeek: 2,
|
||||
timezone: 'browser',
|
||||
to: '11:30',
|
||||
toDayOfWeek: 2,
|
||||
},
|
||||
},
|
||||
allValue: '*',
|
||||
current: {
|
||||
text: 'text1',
|
||||
value: 'value1',
|
||||
},
|
||||
enable: true,
|
||||
filter: {
|
||||
ids: [],
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'datasource1',
|
||||
},
|
||||
hide: false,
|
||||
iconColor: 'blue',
|
||||
name: 'Grafana annotations',
|
||||
definition: 'definition1',
|
||||
description: 'A query variable',
|
||||
hide: VariableHide.DontHide,
|
||||
includeAll: true,
|
||||
label: 'Query Variable',
|
||||
multi: true,
|
||||
name: 'queryVar',
|
||||
options: [],
|
||||
query: 'query1',
|
||||
refresh: VariableRefresh.OnDashboardLoad,
|
||||
regex: 'regex1',
|
||||
skipUrlSync: false,
|
||||
sort: VariableSort.Disabled,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'AnnotationQuery',
|
||||
kind: 'CustomVariable',
|
||||
spec: {
|
||||
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
|
||||
query: {
|
||||
kind: 'prometheus',
|
||||
spec: {
|
||||
query: 'up',
|
||||
allValue: 'All',
|
||||
current: {
|
||||
text: 'option1',
|
||||
value: 'option1',
|
||||
},
|
||||
description: 'A custom variable',
|
||||
hide: VariableHide.DontHide,
|
||||
includeAll: true,
|
||||
label: 'Custom Variable',
|
||||
multi: true,
|
||||
name: 'customVar',
|
||||
options: [
|
||||
{
|
||||
selected: true,
|
||||
text: 'option1',
|
||||
value: 'option1',
|
||||
},
|
||||
{
|
||||
selected: false,
|
||||
text: 'option2',
|
||||
value: 'option2',
|
||||
},
|
||||
],
|
||||
query: 'option1, option2',
|
||||
skipUrlSync: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'DatasourceVariable',
|
||||
spec: {
|
||||
allValue: undefined,
|
||||
current: {
|
||||
text: 'text1',
|
||||
value: 'value1',
|
||||
},
|
||||
enable: true,
|
||||
filter: {
|
||||
ids: [],
|
||||
defaultOptionEnabled: true,
|
||||
description: 'A datasource variable',
|
||||
hide: VariableHide.DontHide,
|
||||
includeAll: false,
|
||||
label: 'Datasource Variable',
|
||||
multi: false,
|
||||
name: 'datasourceVar',
|
||||
options: [],
|
||||
pluginId: 'datasource1',
|
||||
refresh: VariableRefresh.OnDashboardLoad,
|
||||
regex: 'regex1',
|
||||
skipUrlSync: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ConstantVariable',
|
||||
spec: {
|
||||
current: {
|
||||
text: 'value4',
|
||||
value: 'value4',
|
||||
},
|
||||
hide: false,
|
||||
iconColor: 'red',
|
||||
name: 'Prometheus annotations',
|
||||
description: 'A constant variable',
|
||||
hide: VariableHide.DontHide,
|
||||
label: 'Constant Variable',
|
||||
name: 'constantVar',
|
||||
query: 'value4',
|
||||
skipUrlSync: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'IntervalVariable',
|
||||
spec: {
|
||||
auto: false,
|
||||
auto_count: 10,
|
||||
auto_min: '1m',
|
||||
current: {
|
||||
text: '1m',
|
||||
value: '1m',
|
||||
},
|
||||
description: 'An interval variable',
|
||||
hide: VariableHide.DontHide,
|
||||
label: 'Interval Variable',
|
||||
name: 'intervalVar',
|
||||
options: [
|
||||
{
|
||||
selected: true,
|
||||
text: '1m',
|
||||
value: '1m',
|
||||
},
|
||||
{
|
||||
selected: false,
|
||||
text: '5m',
|
||||
value: '5m',
|
||||
},
|
||||
{
|
||||
selected: false,
|
||||
text: '10m',
|
||||
value: '10m',
|
||||
},
|
||||
],
|
||||
query: '1m,5m,10m',
|
||||
refresh: VariableRefresh.OnDashboardLoad,
|
||||
skipUrlSync: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'TextVariable',
|
||||
spec: {
|
||||
current: {
|
||||
text: 'value6',
|
||||
value: 'value6',
|
||||
},
|
||||
description: 'A text variable',
|
||||
hide: VariableHide.DontHide,
|
||||
label: 'Text Variable',
|
||||
name: 'textVar',
|
||||
query: 'value6',
|
||||
skipUrlSync: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'GroupByVariable',
|
||||
spec: {
|
||||
current: {
|
||||
text: 'text7',
|
||||
value: 'value7',
|
||||
},
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'datasource2',
|
||||
},
|
||||
description: 'A group by variable',
|
||||
hide: VariableHide.DontHide,
|
||||
includeAll: false,
|
||||
label: 'Group By Variable',
|
||||
multi: false,
|
||||
name: 'groupByVar',
|
||||
options: [
|
||||
{
|
||||
text: 'option1',
|
||||
value: 'option1',
|
||||
},
|
||||
{
|
||||
text: 'option2',
|
||||
value: 'option2',
|
||||
},
|
||||
],
|
||||
skipUrlSync: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'AdhocVariable',
|
||||
spec: {
|
||||
baseFilters: [
|
||||
{
|
||||
condition: 'AND',
|
||||
key: 'key1',
|
||||
operator: '=',
|
||||
value: 'value1',
|
||||
},
|
||||
{
|
||||
condition: 'OR',
|
||||
key: 'key2',
|
||||
operator: '=',
|
||||
value: 'value2',
|
||||
},
|
||||
],
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'datasource3',
|
||||
},
|
||||
defaultKeys: [
|
||||
{
|
||||
expandable: true,
|
||||
group: 'defaultGroup1',
|
||||
text: 'defaultKey1',
|
||||
value: 'defaultKey1',
|
||||
},
|
||||
],
|
||||
description: 'An adhoc variable',
|
||||
filters: [
|
||||
{
|
||||
condition: 'AND',
|
||||
key: 'key3',
|
||||
operator: '=',
|
||||
value: 'value3',
|
||||
},
|
||||
],
|
||||
hide: VariableHide.DontHide,
|
||||
label: 'Adhoc Variable',
|
||||
name: 'adhocVar',
|
||||
skipUrlSync: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { behaviors, sceneGraph, SceneQueryRunner } from '@grafana/scenes';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
|
||||
import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/examples';
|
||||
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/dashboard_api';
|
||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||
|
||||
import { DashboardLayoutManager } from '../scene/types';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { getQueryRunnerFor } from '../utils/utils';
|
||||
|
||||
import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene';
|
||||
import { transformCursorSynctoEnum } from './transformToV2TypesUtils';
|
||||
|
||||
const defaultDashboard: DashboardWithAccessInfo<DashboardV2Spec> = {
|
||||
kind: 'DashboardWithAccessInfo',
|
||||
metadata: {
|
||||
name: 'dashboard-uid',
|
||||
namespace: 'default',
|
||||
labels: {},
|
||||
resourceVersion: '',
|
||||
creationTimestamp: '',
|
||||
},
|
||||
spec: handyTestingSchema,
|
||||
access: {},
|
||||
apiVersion: 'v2',
|
||||
};
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: () => ({
|
||||
getInstanceSettings: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('transformSaveModelSchemaV2ToScene', () => {
|
||||
beforeAll(() => {
|
||||
config.featureToggles.groupByVariable = true;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.featureToggles.groupByVariable = false;
|
||||
});
|
||||
|
||||
it('should initialize the DashboardScene with the model state', () => {
|
||||
const scene = transformSaveModelSchemaV2ToScene(defaultDashboard);
|
||||
const dashboardControls = scene.state.controls!;
|
||||
const dash = defaultDashboard.spec;
|
||||
|
||||
expect(scene.state.uid).toEqual(defaultDashboard.metadata.name);
|
||||
expect(scene.state.title).toEqual(dash.title);
|
||||
expect(scene.state.description).toEqual(dash.description);
|
||||
expect(scene.state.editable).toEqual(dash.editable);
|
||||
expect(scene.state.preload).toEqual(true);
|
||||
expect(scene.state.version).toEqual(dash.schemaVersion);
|
||||
expect(scene.state.tags).toEqual(dash.tags);
|
||||
|
||||
const liveNow = scene.state.$behaviors?.find((b) => b instanceof behaviors.LiveNowTimer);
|
||||
expect(liveNow?.state.enabled).toEqual(dash.liveNow);
|
||||
|
||||
const cursorSync = scene.state.$behaviors?.find((b) => b instanceof behaviors.CursorSync);
|
||||
expect(transformCursorSynctoEnum(cursorSync?.state.sync)).toEqual(dash.cursorSync);
|
||||
|
||||
// Dashboard links
|
||||
expect(scene.state.links).toHaveLength(dash.links.length);
|
||||
expect(scene.state.links![0].title).toBe(dash.links[0].title);
|
||||
|
||||
// Time settings
|
||||
const time = dash.timeSettings;
|
||||
const refreshPicker = dashboardSceneGraph.getRefreshPicker(scene)!;
|
||||
const timeRange = sceneGraph.getTimeRange(scene)!;
|
||||
|
||||
// Time settings
|
||||
expect(refreshPicker.state.refresh).toEqual(time.autoRefresh);
|
||||
expect(refreshPicker.state.intervals).toEqual(time.autoRefreshIntervals);
|
||||
expect(timeRange?.state.fiscalYearStartMonth).toEqual(dash.timeSettings.fiscalYearStartMonth);
|
||||
expect(timeRange?.state.value.raw).toEqual({ from: dash.timeSettings.from, to: dash.timeSettings.to });
|
||||
expect(dashboardControls.state.hideTimeControls).toEqual(dash.timeSettings.hideTimepicker);
|
||||
expect(timeRange?.state.UNSAFE_nowDelay).toEqual(dash.timeSettings.nowDelay);
|
||||
expect(timeRange?.state.timeZone).toEqual(dash.timeSettings.timezone);
|
||||
expect(timeRange?.state.weekStart).toEqual(dash.timeSettings.weekStart);
|
||||
expect(dashboardControls).toBeDefined();
|
||||
expect(dashboardControls.state.refreshPicker.state.intervals).toEqual(time.autoRefreshIntervals);
|
||||
expect(dashboardControls.state.hideTimeControls).toBe(time.hideTimepicker);
|
||||
|
||||
// TODO: Variables
|
||||
// expect(scene.state?.$variables?.state.variables).toHaveLength(dash.variables.length);
|
||||
// expect(scene.state?.$variables?.getByName(dash.variables[0].spec.name)).toBeInstanceOf(QueryVariable);
|
||||
// expect(scene.state?.$variables?.getByName(dash.variables[1].spec.name)).toBeInstanceOf(TextBoxVariable); ...
|
||||
|
||||
// TODO: Annotations
|
||||
// expect(scene.state.annotations).toHaveLength(dash.annotations.length);
|
||||
// expect(scene.state.annotations[0].text).toBe(dash.annotations[0].text); ...
|
||||
|
||||
// To be implemented
|
||||
// expect(timePicker.state.ranges).toEqual(dash.timeSettings.quickRanges);
|
||||
|
||||
// VizPanel
|
||||
const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels();
|
||||
expect(vizPanels).toHaveLength(1);
|
||||
const vizPanel = vizPanels[0];
|
||||
expect(vizPanel.state.title).toBe(dash.elements['test-panel-uid'].spec.title);
|
||||
expect(vizPanel.state.description).toBe(dash.elements['test-panel-uid'].spec.description);
|
||||
expect(vizPanel.state.pluginId).toBe(dash.elements['test-panel-uid'].spec.vizConfig.kind);
|
||||
expect(vizPanel.state.pluginVersion).toBe(dash.elements['test-panel-uid'].spec.vizConfig.spec.pluginVersion);
|
||||
expect(vizPanel.state.options).toEqual(dash.elements['test-panel-uid'].spec.vizConfig.spec.options);
|
||||
expect(vizPanel.state.fieldConfig).toEqual(dash.elements['test-panel-uid'].spec.vizConfig.spec.fieldConfig);
|
||||
|
||||
// FIXME: There is an error of data being undefined
|
||||
// expect(vizPanel.state.$data).toBeInstanceOf(SceneDataTransformer);
|
||||
// const dataTransformer = vizPanel.state.$data as SceneDataTransformer;
|
||||
// expect(dataTransformer.state.transformations).toEqual([{ id: 'transform1', options: {} }]);
|
||||
|
||||
// expect(dataTransformer.state.$data).toBeInstanceOf(SceneQueryRunner);
|
||||
const queryRunner = getQueryRunnerFor(vizPanel);
|
||||
expect(queryRunner).toBeInstanceOf(SceneQueryRunner);
|
||||
expect(queryRunner?.state.datasource).toBeUndefined();
|
||||
// expect(queryRunner.state.queries).toEqual([{ query: 'test-query', datasource: { uid: 'datasource1', type: 'prometheus' } }]);
|
||||
// expect(queryRunner.state.maxDataPoints).toBe(100);
|
||||
// expect(queryRunner.state.cacheTimeout).toBe('1m');
|
||||
// expect(queryRunner.state.queryCachingTTL).toBe(60);
|
||||
// expect(queryRunner.state.minInterval).toBe('1m');
|
||||
// expect(queryRunner.state.dataLayerFilter?.panelId).toBe(1);
|
||||
|
||||
// FIXME: Fix the key incompatibility since panel is not numeric anymore
|
||||
// expect(vizPanel.state.key).toBe(dash.elements['test-panel-uid'].spec.uid);
|
||||
|
||||
// FIXME: Tests for layout
|
||||
});
|
||||
|
||||
it('should set panel ds if it is mixed DS', () => {
|
||||
const dashboard = cloneDeep(defaultDashboard);
|
||||
dashboard.spec.elements['test-panel-uid'].spec.data.spec.queries.push({
|
||||
kind: 'PanelQuery',
|
||||
spec: {
|
||||
refId: 'A',
|
||||
datasource: {
|
||||
type: 'graphite',
|
||||
uid: 'datasource1',
|
||||
},
|
||||
hidden: false,
|
||||
query: {
|
||||
kind: 'prometheus',
|
||||
spec: {
|
||||
expr: 'test-query',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const scene = transformSaveModelSchemaV2ToScene(dashboard);
|
||||
|
||||
const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels();
|
||||
expect(vizPanels.length).toBe(1);
|
||||
expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.type).toBe('mixed');
|
||||
expect(getQueryRunnerFor(vizPanels[0])?.state.datasource?.uid).toBe(MIXED_DATASOURCE_NAME);
|
||||
});
|
||||
|
||||
it('should set panel ds as undefined if it is not mixed DS', () => {
|
||||
const dashboard = cloneDeep(defaultDashboard);
|
||||
dashboard.spec.elements['test-panel-uid'].spec.data.spec.queries.push({
|
||||
kind: 'PanelQuery',
|
||||
spec: {
|
||||
refId: 'A',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'datasource1',
|
||||
},
|
||||
hidden: false,
|
||||
query: {
|
||||
kind: 'prometheus',
|
||||
spec: {
|
||||
expr: 'test-query',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const scene = transformSaveModelSchemaV2ToScene(dashboard);
|
||||
|
||||
const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels();
|
||||
expect(vizPanels.length).toBe(1);
|
||||
expect(getQueryRunnerFor(vizPanels[0])?.state.datasource).toBeUndefined();
|
||||
});
|
||||
|
||||
// Skipping the test because the schema doesn't accept the ds to be undefined.
|
||||
// In future PR, we will mark it as optional so, this test should pass and the runtime code should be updated.
|
||||
it.skip('should set panel ds as undefined if it is not mixed DS', () => {
|
||||
const dashboard = cloneDeep(defaultDashboard);
|
||||
dashboard.spec.elements['test-panel-uid'].spec.data.spec.queries.push({
|
||||
kind: 'PanelQuery',
|
||||
// @ts-expect-error TODO: When marking DS as optional, this should be fixed
|
||||
spec: {
|
||||
refId: 'A',
|
||||
hidden: false,
|
||||
query: {
|
||||
kind: 'prometheus',
|
||||
spec: {
|
||||
expr: 'test-query',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const scene = transformSaveModelSchemaV2ToScene(dashboard);
|
||||
|
||||
const vizPanels = (scene.state.body as DashboardLayoutManager).getVizPanels();
|
||||
expect(vizPanels.length).toBe(1);
|
||||
expect(getQueryRunnerFor(vizPanels[0])?.state.datasource).toBeUndefined();
|
||||
});
|
||||
});
|
||||
+631
@@ -0,0 +1,631 @@
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||
import {
|
||||
AdHocFiltersVariable,
|
||||
behaviors,
|
||||
ConstantVariable,
|
||||
CustomVariable,
|
||||
DataSourceVariable,
|
||||
GroupByVariable,
|
||||
IntervalVariable,
|
||||
QueryVariable,
|
||||
SceneDataLayerControls,
|
||||
SceneDataProvider,
|
||||
SceneDataQuery,
|
||||
SceneDataTransformer,
|
||||
SceneGridItemLike,
|
||||
SceneGridLayout,
|
||||
SceneObject,
|
||||
SceneQueryRunner,
|
||||
SceneRefreshPicker,
|
||||
SceneTimePicker,
|
||||
SceneTimeRange,
|
||||
SceneVariable,
|
||||
SceneVariableSet,
|
||||
TextBoxVariable,
|
||||
VariableValueSelectors,
|
||||
VizPanel,
|
||||
VizPanelMenu,
|
||||
VizPanelState,
|
||||
} from '@grafana/scenes';
|
||||
import { DataSourceRef } from '@grafana/schema/dist/esm/index.gen';
|
||||
import {
|
||||
AdhocVariableKind,
|
||||
ConstantVariableKind,
|
||||
CustomVariableKind,
|
||||
DashboardV2Spec,
|
||||
DatasourceVariableKind,
|
||||
defaultAdhocVariableKind,
|
||||
defaultConstantVariableKind,
|
||||
defaultCustomVariableKind,
|
||||
defaultDatasourceVariableKind,
|
||||
defaultGroupByVariableKind,
|
||||
defaultIntervalVariableKind,
|
||||
defaultQueryVariableKind,
|
||||
defaultTextVariableKind,
|
||||
GroupByVariableKind,
|
||||
IntervalVariableKind,
|
||||
PanelKind,
|
||||
PanelQueryKind,
|
||||
QueryVariableKind,
|
||||
TextVariableKind,
|
||||
} from '@grafana/schema/src/schema/dashboard/v2alpha0/dashboard.gen';
|
||||
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/dashboard_api';
|
||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||
|
||||
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
|
||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||
import { DashboardControls } from '../scene/DashboardControls';
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardDatasourceBehaviour } from '../scene/DashboardDatasourceBehaviour';
|
||||
import { registerDashboardMacro } from '../scene/DashboardMacro';
|
||||
import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { DashboardScopesFacade } from '../scene/DashboardScopesFacade';
|
||||
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
|
||||
import { PanelNotices } from '../scene/PanelNotices';
|
||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||
import { AngularDeprecation } from '../scene/angular/AngularDeprecation';
|
||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
|
||||
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
|
||||
import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState';
|
||||
import { getDashboardSceneFor, getIntervalsFromQueryString } from '../utils/utils';
|
||||
|
||||
import { SnapshotVariable } from './custom-variables/SnapshotVariable';
|
||||
import { registerPanelInteractionsReporter } from './transformSaveModelToScene';
|
||||
import {
|
||||
transformCursorSyncV2ToV1,
|
||||
transformSortVariableToEnumV1,
|
||||
transformValueMappingsToV1,
|
||||
transformVariableHideToEnumV1,
|
||||
transformVariableRefreshToEnumV1,
|
||||
} from './transformToV1TypesUtils';
|
||||
|
||||
const DEFAULT_DATASOURCE = 'default';
|
||||
|
||||
type TypedVariableModelv2 =
|
||||
| QueryVariableKind
|
||||
| TextVariableKind
|
||||
| ConstantVariableKind
|
||||
| DatasourceVariableKind
|
||||
| IntervalVariableKind
|
||||
| CustomVariableKind
|
||||
| GroupByVariableKind
|
||||
| AdhocVariableKind;
|
||||
|
||||
export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<DashboardV2Spec>): DashboardScene {
|
||||
const { spec: dashboard, metadata } = dto;
|
||||
|
||||
const annotationLayers = dashboard.annotations.map((annotation) => {
|
||||
return new DashboardAnnotationsDataLayer({
|
||||
key: uniqueId('annotations-'),
|
||||
query: annotation.spec,
|
||||
name: annotation.spec.name,
|
||||
isEnabled: Boolean(annotation.spec.enable),
|
||||
isHidden: Boolean(annotation.spec.hide),
|
||||
});
|
||||
});
|
||||
|
||||
const dashboardScene = new DashboardScene({
|
||||
description: dashboard.description,
|
||||
editable: dashboard.editable,
|
||||
preload: dashboard.preload,
|
||||
id: dashboard.id,
|
||||
isDirty: false,
|
||||
links: dashboard.links,
|
||||
// TODO: Combine access and metadata to compose the V1 meta object
|
||||
meta: {},
|
||||
tags: dashboard.tags,
|
||||
title: dashboard.title,
|
||||
uid: metadata.name,
|
||||
version: dashboard.schemaVersion,
|
||||
body: new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
isLazy: dashboard.preload ? false : true,
|
||||
children: createSceneGridLayoutForItems(dashboard),
|
||||
$behaviors: [trackIfEmpty],
|
||||
}),
|
||||
}),
|
||||
$timeRange: new SceneTimeRange({
|
||||
from: dashboard.timeSettings.from,
|
||||
to: dashboard.timeSettings.to,
|
||||
fiscalYearStartMonth: dashboard.timeSettings.fiscalYearStartMonth,
|
||||
timeZone: dashboard.timeSettings.timezone,
|
||||
weekStart: dashboard.timeSettings.weekStart,
|
||||
UNSAFE_nowDelay: dashboard.timeSettings.nowDelay,
|
||||
}),
|
||||
$variables: getVariables(dashboard),
|
||||
$behaviors: [
|
||||
new behaviors.CursorSync({
|
||||
sync: transformCursorSyncV2ToV1(dashboard.cursorSync),
|
||||
}),
|
||||
new behaviors.SceneQueryController(),
|
||||
registerDashboardMacro,
|
||||
registerPanelInteractionsReporter,
|
||||
new behaviors.LiveNowTimer({ enabled: dashboard.liveNow }),
|
||||
preserveDashboardSceneStateInLocalStorage,
|
||||
addPanelsOnLoadBehavior,
|
||||
new DashboardScopesFacade({
|
||||
reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange,
|
||||
uid: dashboard.id?.toString(),
|
||||
}),
|
||||
new DashboardReloadBehavior({
|
||||
reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange,
|
||||
uid: dashboard.id?.toString(),
|
||||
version: 1,
|
||||
}),
|
||||
],
|
||||
$data: new DashboardDataLayerSet({
|
||||
annotationLayers,
|
||||
}),
|
||||
controls: new DashboardControls({
|
||||
variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()],
|
||||
timePicker: new SceneTimePicker({}),
|
||||
refreshPicker: new SceneRefreshPicker({
|
||||
refresh: dashboard.timeSettings.autoRefresh,
|
||||
intervals: dashboard.timeSettings.autoRefreshIntervals,
|
||||
withText: true,
|
||||
}),
|
||||
hideTimeControls: dashboard.timeSettings.hideTimepicker,
|
||||
}),
|
||||
});
|
||||
|
||||
return dashboardScene;
|
||||
}
|
||||
|
||||
function createSceneGridLayoutForItems(dashboard: DashboardV2Spec): SceneGridItemLike[] {
|
||||
const gridElements = dashboard.layout.spec.items;
|
||||
|
||||
return gridElements.map((element) => {
|
||||
if (element.kind === 'GridLayoutItem') {
|
||||
const panel = dashboard.elements[element.spec.element.name];
|
||||
|
||||
if (!panel) {
|
||||
throw new Error(`Panel with uid ${element.spec.element.name} not found in the dashboard elements`);
|
||||
}
|
||||
|
||||
if (panel.kind === 'Panel') {
|
||||
const vizPanel = buildVizPanel(panel);
|
||||
|
||||
return new DashboardGridItem({
|
||||
key: `grid-item-${panel.spec.uid}`,
|
||||
x: element.spec.x,
|
||||
y: element.spec.y,
|
||||
width: element.spec.width,
|
||||
height: element.spec.height,
|
||||
itemHeight: element.spec.height,
|
||||
body: vizPanel,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unknown element kind: ${element.kind}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown layout element kind: ${element.kind}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildVizPanel(panel: PanelKind): VizPanel {
|
||||
const titleItems: SceneObject[] = [];
|
||||
|
||||
if (config.featureToggles.angularDeprecationUI) {
|
||||
titleItems.push(new AngularDeprecation());
|
||||
}
|
||||
|
||||
// FIXME: Links in a panel are DashboardLinks not DataLinks
|
||||
// titleItems.push(
|
||||
// new VizPanelLinks({
|
||||
// rawLinks: panel.spec.links,
|
||||
// menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }),
|
||||
// })
|
||||
// );
|
||||
|
||||
titleItems.push(new PanelNotices());
|
||||
|
||||
const queryOptions = panel.spec.data.spec.queryOptions;
|
||||
const timeOverrideShown = (queryOptions.timeFrom || queryOptions.timeShift) && !queryOptions.hideTimeOverride;
|
||||
|
||||
const vizPanelState: VizPanelState = {
|
||||
key: panel.spec.uid,
|
||||
title: panel.spec.title,
|
||||
description: panel.spec.description,
|
||||
pluginId: panel.spec.vizConfig.kind,
|
||||
options: panel.spec.vizConfig.spec.options,
|
||||
fieldConfig: transformValueMappingsToV1(panel.spec.vizConfig.spec.fieldConfig),
|
||||
pluginVersion: panel.spec.vizConfig.spec.pluginVersion,
|
||||
// FIXME: Transparent is not added to the schema yet
|
||||
// displayMode: panel.spec.transparent ? 'transparent' : undefined,
|
||||
hoverHeader: !panel.spec.title && !timeOverrideShown,
|
||||
hoverHeaderOffset: 0,
|
||||
$data: createPanelDataProvider(panel),
|
||||
titleItems,
|
||||
$behaviors: [],
|
||||
extendPanelContext: setDashboardPanelContext,
|
||||
// _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), //FIXME: Angular Migration
|
||||
};
|
||||
|
||||
// FIXME: Library Panel
|
||||
// if (panel.spec.libraryPanel) {
|
||||
// vizPanelState.$behaviors!.push(
|
||||
// new LibraryPanelBehavior({ uid: panel.spec.libraryPanel.uid, name: panel.spec.libraryPanel.name })
|
||||
// );
|
||||
// vizPanelState.pluginId = LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID;
|
||||
// vizPanelState.$data = undefined;
|
||||
// }
|
||||
|
||||
if (!config.publicDashboardAccessToken) {
|
||||
vizPanelState.menu = new VizPanelMenu({
|
||||
$behaviors: [panelMenuBehavior],
|
||||
});
|
||||
}
|
||||
|
||||
if (queryOptions.timeFrom || queryOptions.timeShift) {
|
||||
vizPanelState.$timeRange = new PanelTimeRange({
|
||||
timeFrom: queryOptions.timeFrom,
|
||||
timeShift: queryOptions.timeShift,
|
||||
hideTimeOverride: queryOptions.hideTimeOverride,
|
||||
});
|
||||
}
|
||||
|
||||
return new VizPanel(vizPanelState);
|
||||
}
|
||||
|
||||
function trackIfEmpty(grid: SceneGridLayout) {
|
||||
getDashboardSceneFor(grid).setState({ isEmpty: grid.state.children.length === 0 });
|
||||
|
||||
const sub = grid.subscribeToState((n, p) => {
|
||||
if (n.children.length !== p.children.length || n.children !== p.children) {
|
||||
getDashboardSceneFor(grid).setState({ isEmpty: n.children.length === 0 });
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined {
|
||||
if (!panel.spec.data?.spec.queries?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let datasource: DataSourceRef | undefined = undefined;
|
||||
let isMixedDatasource = false;
|
||||
|
||||
panel.spec.data.spec.queries.forEach((query) => {
|
||||
if (!datasource) {
|
||||
datasource = query.spec.datasource;
|
||||
} else if (datasource.uid !== query.spec.datasource.uid || datasource.type !== query.spec.datasource.type) {
|
||||
isMixedDatasource = true;
|
||||
}
|
||||
});
|
||||
|
||||
return isMixedDatasource ? { type: 'mixed', uid: MIXED_DATASOURCE_NAME } : undefined;
|
||||
}
|
||||
|
||||
function panelQueryKindToSceneQuery(query: PanelQueryKind): SceneDataQuery {
|
||||
return {
|
||||
refId: query.spec.refId,
|
||||
datasource: query.spec.datasource,
|
||||
hide: query.spec.hidden,
|
||||
...query.spec.query.spec,
|
||||
};
|
||||
}
|
||||
|
||||
export function createPanelDataProvider(panelKind: PanelKind): SceneDataProvider | undefined {
|
||||
const panel = panelKind.spec;
|
||||
const targets = panel.data?.spec.queries ?? [];
|
||||
// Skip setting query runner for panels without queries
|
||||
if (!targets?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Skip setting query runner for panel plugins with skipDataQuery
|
||||
if (config.panels[panel.vizConfig.kind]?.skipDataQuery) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let dataProvider: SceneDataProvider | undefined = undefined;
|
||||
const datasource = getPanelDataSource(panelKind);
|
||||
|
||||
dataProvider = new SceneQueryRunner({
|
||||
datasource,
|
||||
queries: targets.map(panelQueryKindToSceneQuery),
|
||||
maxDataPoints: panel.data.spec.queryOptions.maxDataPoints ?? undefined,
|
||||
maxDataPointsFromWidth: true,
|
||||
cacheTimeout: panel.data.spec.queryOptions.cacheTimeout,
|
||||
queryCachingTTL: panel.data.spec.queryOptions.queryCachingTTL,
|
||||
minInterval: panel.data.spec.queryOptions.interval ?? undefined,
|
||||
dataLayerFilter: {
|
||||
// FIXME: This is asking for a number as panel ID but here the uid of a panel is string
|
||||
panelId: Number.isNaN(parseInt(panel.uid, 10)) ? 0 : parseInt(panel.uid, 10),
|
||||
},
|
||||
$behaviors: [new DashboardDatasourceBehaviour({})],
|
||||
});
|
||||
|
||||
// Wrap inner data provider in a data transformer
|
||||
return new SceneDataTransformer({
|
||||
$data: dataProvider,
|
||||
transformations: panel.data.spec.transformations.map((transformation) => transformation.spec),
|
||||
});
|
||||
}
|
||||
|
||||
function getVariables(dashboard: DashboardV2Spec): SceneVariableSet | undefined {
|
||||
let variables: SceneVariableSet | undefined;
|
||||
|
||||
if (dashboard.variables.length) {
|
||||
if (false) {
|
||||
// FIXME: isSnapshot is not added to the schema yet
|
||||
//if (dashboard.meta?.isSnapshot) {
|
||||
// in the old model we use .meta.isSnapshot but meta is not persisted
|
||||
// variables = createVariablesForSnapshot(dashboard);
|
||||
} else {
|
||||
variables = createVariablesForDashboard(dashboard);
|
||||
}
|
||||
} else {
|
||||
// Create empty variable set
|
||||
variables = new SceneVariableSet({
|
||||
variables: [],
|
||||
});
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
function createVariablesForDashboard(dashboard: DashboardV2Spec) {
|
||||
const variableObjects = dashboard.variables
|
||||
.map((v) => {
|
||||
try {
|
||||
return createSceneVariableFromVariableModel(v);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
// TODO: Remove filter
|
||||
// Added temporarily to allow skipping non-compatible variables
|
||||
.filter((v): v is SceneVariable => Boolean(v));
|
||||
|
||||
return new SceneVariableSet({
|
||||
variables: variableObjects,
|
||||
});
|
||||
}
|
||||
|
||||
function createSceneVariableFromVariableModel(variable: TypedVariableModelv2): SceneVariable {
|
||||
const commonProperties = {
|
||||
name: variable.spec.name,
|
||||
label: variable.spec.label,
|
||||
description: variable.spec.description,
|
||||
};
|
||||
if (variable.kind === defaultAdhocVariableKind().kind) {
|
||||
return new AdHocFiltersVariable({
|
||||
...commonProperties,
|
||||
description: variable.spec.description,
|
||||
skipUrlSync: variable.spec.skipUrlSync,
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
datasource: variable.spec.datasource,
|
||||
applyMode: 'auto',
|
||||
filters: variable.spec.filters ?? [],
|
||||
baseFilters: variable.spec.baseFilters ?? [],
|
||||
defaultKeys: variable.spec.defaultKeys,
|
||||
useQueriesAsFilterForOptions: true,
|
||||
layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined,
|
||||
supportsMultiValueOperators: Boolean(
|
||||
getDataSourceSrv().getInstanceSettings(variable.spec.datasource)?.meta.multiValueFilterOperators
|
||||
),
|
||||
});
|
||||
}
|
||||
if (variable.kind === defaultCustomVariableKind().kind) {
|
||||
return new CustomVariable({
|
||||
...commonProperties,
|
||||
value: variable.spec.current?.value ?? '',
|
||||
text: variable.spec.current?.text ?? '',
|
||||
|
||||
query: variable.spec.query,
|
||||
isMulti: variable.spec.multi,
|
||||
allValue: variable.spec.allValue || undefined,
|
||||
includeAll: variable.spec.includeAll,
|
||||
defaultToAll: Boolean(variable.spec.includeAll),
|
||||
skipUrlSync: variable.spec.skipUrlSync,
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
});
|
||||
} else if (variable.kind === defaultQueryVariableKind().kind) {
|
||||
return new QueryVariable({
|
||||
...commonProperties,
|
||||
value: variable.spec.current?.value ?? '',
|
||||
text: variable.spec.current?.text ?? '',
|
||||
query: getDataQueryForVariable(variable),
|
||||
datasource: variable.spec.datasource,
|
||||
sort: transformSortVariableToEnumV1(variable.spec.sort),
|
||||
refresh: transformVariableRefreshToEnumV1(variable.spec.refresh),
|
||||
regex: variable.spec.regex,
|
||||
allValue: variable.spec.allValue || undefined,
|
||||
includeAll: variable.spec.includeAll,
|
||||
defaultToAll: Boolean(variable.spec.includeAll),
|
||||
isMulti: variable.spec.multi,
|
||||
skipUrlSync: variable.spec.skipUrlSync,
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
definition: variable.spec.definition,
|
||||
});
|
||||
} else if (variable.kind === defaultDatasourceVariableKind().kind) {
|
||||
return new DataSourceVariable({
|
||||
...commonProperties,
|
||||
value: variable.spec.current?.value ?? '',
|
||||
text: variable.spec.current?.text ?? '',
|
||||
regex: variable.spec.regex,
|
||||
pluginId: variable.spec.pluginId,
|
||||
allValue: variable.spec.allValue || undefined,
|
||||
includeAll: variable.spec.includeAll,
|
||||
defaultToAll: Boolean(variable.spec.includeAll),
|
||||
skipUrlSync: variable.spec.skipUrlSync,
|
||||
isMulti: variable.spec.multi,
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
defaultOptionEnabled:
|
||||
variable.spec.current?.value === DEFAULT_DATASOURCE && variable.spec.current?.text === 'default',
|
||||
});
|
||||
} else if (variable.kind === defaultIntervalVariableKind().kind) {
|
||||
const intervals = getIntervalsFromQueryString(variable.spec.query);
|
||||
const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals);
|
||||
return new IntervalVariable({
|
||||
...commonProperties,
|
||||
value: currentInterval,
|
||||
intervals: intervals,
|
||||
autoEnabled: variable.spec.auto,
|
||||
autoStepCount: variable.spec.auto_count,
|
||||
autoMinInterval: variable.spec.auto_min,
|
||||
refresh: transformVariableRefreshToEnumV1(variable.spec.refresh),
|
||||
skipUrlSync: variable.spec.skipUrlSync,
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
});
|
||||
} else if (variable.kind === defaultConstantVariableKind().kind) {
|
||||
return new ConstantVariable({
|
||||
...commonProperties,
|
||||
value: variable.spec.query,
|
||||
skipUrlSync: variable.spec.skipUrlSync,
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
});
|
||||
} else if (variable.kind === defaultTextVariableKind().kind) {
|
||||
let val;
|
||||
if (!variable?.spec.current?.value) {
|
||||
val = variable.spec.query;
|
||||
} else {
|
||||
if (typeof variable.spec.current.value === 'string') {
|
||||
val = variable.spec.current.value;
|
||||
} else {
|
||||
val = variable.spec.current.value[0];
|
||||
}
|
||||
}
|
||||
|
||||
return new TextBoxVariable({
|
||||
...commonProperties,
|
||||
value: val,
|
||||
skipUrlSync: variable.spec.skipUrlSync,
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
});
|
||||
} else if (config.featureToggles.groupByVariable && variable.kind === defaultGroupByVariableKind().kind) {
|
||||
return new GroupByVariable({
|
||||
...commonProperties,
|
||||
datasource: variable.spec.datasource,
|
||||
value: variable.spec.current?.value || [],
|
||||
text: variable.spec.current?.text || [],
|
||||
skipUrlSync: variable.spec.skipUrlSync,
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
// @ts-expect-error
|
||||
defaultOptions: variable.options,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Scenes: Unsupported variable type ${variable.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getDataQueryForVariable(variable: QueryVariableKind) {
|
||||
return typeof variable.spec.query !== 'string'
|
||||
? {
|
||||
...variable.spec.query.spec,
|
||||
refId: variable.spec.query.spec.refId ?? 'A',
|
||||
}
|
||||
: (variable.spec.query ?? '');
|
||||
}
|
||||
|
||||
export function getCurrentValueForOldIntervalModel(variable: IntervalVariableKind, intervals: string[]): string {
|
||||
const selectedInterval = Array.isArray(variable.spec.current.value)
|
||||
? variable.spec.current.value[0]
|
||||
: variable.spec.current.value;
|
||||
|
||||
// If the interval is the old auto format, return the new auto interval from scenes.
|
||||
if (selectedInterval.startsWith('$__auto_interval_')) {
|
||||
return '$__auto';
|
||||
}
|
||||
|
||||
// Check if the selected interval is valid.
|
||||
if (intervals.includes(selectedInterval)) {
|
||||
return selectedInterval;
|
||||
}
|
||||
|
||||
// If the selected interval is not valid, return the first valid interval.
|
||||
return intervals[0];
|
||||
}
|
||||
|
||||
export function createVariablesForSnapshot(dashboard: DashboardV2Spec): SceneVariableSet {
|
||||
const variableObjects = dashboard.variables
|
||||
.map((v) => {
|
||||
try {
|
||||
// for adhoc we are using the AdHocFiltersVariable from scenes becuase of its complexity
|
||||
if (v.kind === 'AdhocVariable') {
|
||||
return new AdHocFiltersVariable({
|
||||
name: v.spec.name,
|
||||
label: v.spec.label,
|
||||
readOnly: true,
|
||||
description: v.spec.description,
|
||||
skipUrlSync: v.spec.skipUrlSync,
|
||||
hide: transformVariableHideToEnumV1(v.spec.hide),
|
||||
datasource: v.spec.datasource,
|
||||
applyMode: 'auto',
|
||||
filters: v.spec.filters ?? [],
|
||||
baseFilters: v.spec.baseFilters ?? [],
|
||||
defaultKeys: v.spec.defaultKeys,
|
||||
useQueriesAsFilterForOptions: true,
|
||||
layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined,
|
||||
supportsMultiValueOperators: Boolean(
|
||||
getDataSourceSrv().getInstanceSettings(v.spec.datasource)?.meta.multiValueFilterOperators
|
||||
),
|
||||
});
|
||||
}
|
||||
// for other variable types we are using the SnapshotVariable
|
||||
return createSnapshotVariable(v);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
// TODO: Remove filter
|
||||
// Added temporarily to allow skipping non-compatible variables
|
||||
.filter((v): v is SceneVariable => Boolean(v));
|
||||
|
||||
return new SceneVariableSet({
|
||||
variables: variableObjects,
|
||||
});
|
||||
}
|
||||
|
||||
/** Snapshots variables are read-only and should not be updated */
|
||||
export function createSnapshotVariable(variable: TypedVariableModelv2): SceneVariable {
|
||||
let snapshotVariable: SnapshotVariable;
|
||||
let current: { value: string | string[]; text: string | string[] };
|
||||
if (variable.kind === 'IntervalVariable') {
|
||||
const intervals = getIntervalsFromQueryString(variable.spec.query);
|
||||
const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals);
|
||||
snapshotVariable = new SnapshotVariable({
|
||||
name: variable.spec.name,
|
||||
label: variable.spec.label,
|
||||
description: variable.spec.description,
|
||||
value: currentInterval,
|
||||
text: currentInterval,
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
});
|
||||
return snapshotVariable;
|
||||
}
|
||||
|
||||
if (variable.kind === 'ConstantVariable' || variable.kind === 'AdhocVariable') {
|
||||
current = {
|
||||
value: '',
|
||||
text: '',
|
||||
};
|
||||
} else {
|
||||
current = {
|
||||
value: variable.spec.current?.value ?? '',
|
||||
text: variable.spec.current?.text ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
snapshotVariable = new SnapshotVariable({
|
||||
name: variable.spec.name,
|
||||
label: variable.spec.label,
|
||||
description: variable.spec.description,
|
||||
value: current?.value ?? '',
|
||||
text: current?.text ?? '',
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
});
|
||||
return snapshotVariable;
|
||||
}
|
||||
@@ -359,7 +359,7 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
|
||||
});
|
||||
}
|
||||
|
||||
function registerPanelInteractionsReporter(scene: DashboardScene) {
|
||||
export function registerPanelInteractionsReporter(scene: DashboardScene) {
|
||||
// Subscriptions set with subscribeToEvent are automatically unsubscribed when the scene deactivated
|
||||
scene.subscribeToEvent(UserActionEvent, (e) => {
|
||||
const { interaction } = e.payload;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { FieldConfigSource as FieldConfigSourceV1, SpecialValueMatch as SpecialValueMatchV1 } from '@grafana/data';
|
||||
import {
|
||||
VariableHide as VariableHideV1,
|
||||
VariableRefresh as VariableRefreshV1,
|
||||
VariableSort as VariableSortV1,
|
||||
DashboardCursorSync as DashboardCursorSyncV1,
|
||||
defaultDashboardCursorSync,
|
||||
} from '@grafana/schema';
|
||||
import {
|
||||
DashboardCursorSync,
|
||||
MappingType,
|
||||
VariableHide,
|
||||
VariableRefresh,
|
||||
VariableSort,
|
||||
FieldConfigSource,
|
||||
SpecialValueMatch,
|
||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0/dashboard.gen';
|
||||
|
||||
export function transformVariableRefreshToEnumV1(refresh?: VariableRefresh): VariableRefreshV1 {
|
||||
switch (refresh) {
|
||||
case VariableRefresh.Never:
|
||||
return VariableRefreshV1.never;
|
||||
case VariableRefresh.OnDashboardLoad:
|
||||
return VariableRefreshV1.onDashboardLoad;
|
||||
case VariableRefresh.OnTimeRangeChanged:
|
||||
return VariableRefreshV1.onTimeRangeChanged;
|
||||
default:
|
||||
return VariableRefreshV1.never;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformVariableHideToEnumV1(hide?: VariableHide): VariableHideV1 {
|
||||
switch (hide) {
|
||||
case VariableHide.DontHide:
|
||||
return VariableHideV1.dontHide;
|
||||
case VariableHide.HideLabel:
|
||||
return VariableHideV1.hideLabel;
|
||||
case VariableHide.HideVariable:
|
||||
return VariableHideV1.hideVariable;
|
||||
default:
|
||||
return VariableHideV1.dontHide;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformSortVariableToEnumV1(sort?: VariableSort): VariableSortV1 {
|
||||
switch (sort) {
|
||||
case VariableSort.Disabled:
|
||||
return VariableSortV1.disabled;
|
||||
case VariableSort.NumericalAsc:
|
||||
return VariableSortV1.numericalAsc;
|
||||
case VariableSort.NumericalDesc:
|
||||
return VariableSortV1.numericalDesc;
|
||||
case VariableSort.AlphabeticalAsc:
|
||||
return VariableSortV1.alphabeticalAsc;
|
||||
case VariableSort.AlphabeticalDesc:
|
||||
return VariableSortV1.alphabeticalDesc;
|
||||
default:
|
||||
return VariableSortV1.disabled;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformCursorSyncV2ToV1(cursorSync: DashboardCursorSync): DashboardCursorSyncV1 {
|
||||
switch (cursorSync) {
|
||||
case DashboardCursorSync.Crosshair:
|
||||
return DashboardCursorSyncV1.Crosshair;
|
||||
case DashboardCursorSync.Tooltip:
|
||||
return DashboardCursorSyncV1.Tooltip;
|
||||
case DashboardCursorSync.Off:
|
||||
return DashboardCursorSyncV1.Off;
|
||||
default:
|
||||
return defaultDashboardCursorSync;
|
||||
}
|
||||
}
|
||||
|
||||
function transformSpecialValueMatchToV1(match: SpecialValueMatch): SpecialValueMatchV1 {
|
||||
switch (match) {
|
||||
case SpecialValueMatch.True:
|
||||
return SpecialValueMatchV1.True;
|
||||
case SpecialValueMatch.False:
|
||||
return SpecialValueMatchV1.False;
|
||||
case SpecialValueMatch.Null:
|
||||
return SpecialValueMatchV1.Null;
|
||||
case SpecialValueMatch.NotANumber:
|
||||
return SpecialValueMatchV1.NaN;
|
||||
case SpecialValueMatch.NullAndNaN:
|
||||
return SpecialValueMatchV1.NullAndNaN;
|
||||
case SpecialValueMatch.Empty:
|
||||
return SpecialValueMatchV1.Empty;
|
||||
default:
|
||||
throw new Error(`Unknown match type: ${match}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function transformValueMappingsToV1(fieldConfig: FieldConfigSource): FieldConfigSourceV1 {
|
||||
return {
|
||||
...fieldConfig,
|
||||
defaults: {
|
||||
...fieldConfig.defaults,
|
||||
mappings: fieldConfig.defaults.mappings?.map((mapping) => {
|
||||
switch (mapping.type) {
|
||||
case 'value':
|
||||
return {
|
||||
...mapping,
|
||||
type: MappingType.ValueToText,
|
||||
};
|
||||
case 'range':
|
||||
return {
|
||||
...mapping,
|
||||
type: MappingType.RangeToText,
|
||||
};
|
||||
case 'regex':
|
||||
return {
|
||||
...mapping,
|
||||
type: MappingType.RegexToText,
|
||||
};
|
||||
case 'special':
|
||||
return {
|
||||
...mapping,
|
||||
options: {
|
||||
...mapping.options,
|
||||
match: transformSpecialValueMatchToV1(mapping.options.match),
|
||||
},
|
||||
type: MappingType.SpecialValue,
|
||||
};
|
||||
default:
|
||||
return mapping;
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class LegacyDashboardAPI implements DashboardAPI {
|
||||
}
|
||||
}
|
||||
|
||||
interface DashboardWithAccessInfo extends Resource<DashboardDataDTO, 'DashboardWithAccessInfo'> {
|
||||
export interface DashboardWithAccessInfo<T = DashboardDataDTO> extends Resource<T, 'DashboardWithAccessInfo'> {
|
||||
access: Object; // TODO...
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user