Compare commits

...

1 Commits

Author SHA1 Message Date
Levente Balogh
007010e8cd feat: allow datasources to register default links and variables
s
2025-12-19 13:01:26 +01:00
34 changed files with 2667 additions and 75 deletions

View File

@@ -127,6 +127,8 @@ DashboardLink: {
keepTime: bool | *false
// Placement can be used to display the link somewhere else on the dashboard other than above the visualisations.
placement?: DashboardLinkPlacement
// The source that registered the link (if any)
source?: ControlSourceRef
}
// Dashboard Link placement. Defines where the link should be displayed.
@@ -792,6 +794,13 @@ VariableOption: {
value: string | [...string]
}
// Source information for controls (e.g. variables or links)
ControlSourceRef: {
uid: string
sourceId: string // E.g. "prometheus"
sourceType: string // E.g. "datasource"
}
// Query variable specification
QueryVariableSpec: {
name: string | *""

View File

@@ -127,6 +127,8 @@ DashboardLink: {
keepTime: bool | *false
// Placement can be used to display the link somewhere else on the dashboard other than above the visualisations.
placement?: DashboardLinkPlacement
// The source that registered the link (if any)
source?: ControlSourceRef
}
// Dashboard Link placement. Defines where the link should be displayed.
@@ -796,6 +798,14 @@ VariableOption: {
value: string | [...string]
}
// Source information for controls (e.g. variables or links)
ControlSourceRef: {
uid: string
sourceId: string // E.g. "prometheus"
sourceType: string // E.g. "datasource"
}
// Query variable specification
QueryVariableSpec: {
name: string | *""

View File

@@ -277,6 +277,14 @@ lineage: schemas: [{
uid?: string
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// Source information for controls (e.g. variables or links)
#ControlSourceRef: {
uid: string
sourceId: string // E.g. "prometheus"
sourceType: string // E.g. "datasource"
}
// Links with references to other dashboards or external resources
#DashboardLink: {
// Title to display with the link
@@ -301,6 +309,8 @@ lineage: schemas: [{
includeVars: bool | *false
// If true, includes current time range in the link as query params
keepTime: bool | *false
// The source that registered the link (if any)
source?: #ControlSourceRef
} @cuetsy(kind="interface")

View File

@@ -277,6 +277,14 @@ lineage: schemas: [{
uid?: string
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// Source information for controls (e.g. variables or links)
#ControlSourceRef: {
uid: string
sourceId: string // E.g. "prometheus"
sourceType: string // E.g. "datasource"
}
// Links with references to other dashboards or external resources
#DashboardLink: {
// Title to display with the link
@@ -301,6 +309,8 @@ lineage: schemas: [{
includeVars: bool | *false
// If true, includes current time range in the link as query params
keepTime: bool | *false
// The source that registered the link (if any)
source?: #ControlSourceRef
} @cuetsy(kind="interface")

View File

@@ -131,6 +131,8 @@ DashboardLink: {
keepTime: bool | *false
// Placement can be used to display the link somewhere else on the dashboard other than above the visualisations.
placement?: DashboardLinkPlacement
// The source that registered the link (if any)
source?: ControlSourceRef
}
// Dashboard Link placement. Defines where the link should be displayed.
@@ -796,6 +798,13 @@ VariableOption: {
value: string | [...string]
}
// Source information for controls (e.g. variables or links)
ControlSourceRef: {
uid: string
sourceId: string // E.g. "prometheus"
sourceType: string // E.g. "datasource"
}
// Query variable specification
QueryVariableSpec: {
name: string | *""

View File

@@ -1238,6 +1238,8 @@ type DashboardDashboardLink struct {
KeepTime bool `json:"keepTime"`
// Placement can be used to display the link somewhere else on the dashboard other than above the visualisations.
Placement *string `json:"placement,omitempty"`
// The source that registered the link (if any)
Source *DashboardControlSourceRef `json:"source,omitempty"`
}
// NewDashboardDashboardLink creates a new DashboardDashboardLink object.
@@ -1266,6 +1268,21 @@ const (
// +k8s:openapi-gen=true
const DashboardDashboardLinkPlacement = "inControlsMenu"
// Source information for controls (e.g. variables or links)
// +k8s:openapi-gen=true
type DashboardControlSourceRef struct {
Uid string `json:"uid"`
// E.g. "prometheus"
SourceId string `json:"sourceId"`
// E.g. "datasource"
SourceType string `json:"sourceType"`
}
// NewDashboardControlSourceRef creates a new DashboardControlSourceRef object.
func NewDashboardControlSourceRef() *DashboardControlSourceRef {
return &DashboardControlSourceRef{}
}
// Time configuration
// It defines the default time config for the time picker, the refresh picker for the specific dashboard.
// +k8s:openapi-gen=true

View File

@@ -44,6 +44,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableSpec(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableKind(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableSpec(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardControlSourceRef": schema_pkg_apis_dashboard_v2alpha1_DashboardControlSourceRef(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConversionStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardConversionStatus(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableKind(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableSpec(ref),
@@ -1383,6 +1384,43 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableSpec(ref common
}
}
func schema_pkg_apis_dashboard_v2alpha1_DashboardControlSourceRef(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Source information for controls (e.g. variables or links)",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"uid": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"sourceId": {
SchemaProps: spec.SchemaProps{
Description: "E.g. \"prometheus\"",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"sourceType": {
SchemaProps: spec.SchemaProps{
Description: "E.g. \"datasource\"",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"uid", "sourceId", "sourceType"},
},
},
}
}
func schema_pkg_apis_dashboard_v2alpha1_DashboardConversionStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -1657,10 +1695,18 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardDashboardLink(ref common.Refere
Format: "",
},
},
"source": {
SchemaProps: spec.SchemaProps{
Description: "The source that registered the link (if any)",
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardControlSourceRef"),
},
},
},
Required: []string{"title", "type", "icon", "tooltip", "tags", "asDropdown", "targetBlank", "includeVars", "keepTime"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardControlSourceRef"},
}
}

View File

@@ -131,6 +131,8 @@ DashboardLink: {
keepTime: bool | *false
// Placement can be used to display the link somewhere else on the dashboard other than above the visualisations.
placement?: DashboardLinkPlacement
// The source that registered the link (if any)
source?: ControlSourceRef
}
// Dashboard Link placement. Defines where the link should be displayed.
@@ -800,6 +802,14 @@ VariableOption: {
value: string | [...string]
}
// Source information for controls (e.g. variables or links)
ControlSourceRef: {
uid: string
sourceId: string // E.g. "prometheus"
sourceType: string // E.g. "datasource"
}
// Query variable specification
QueryVariableSpec: {
name: string | *""

View File

@@ -1242,6 +1242,8 @@ type DashboardDashboardLink struct {
KeepTime bool `json:"keepTime"`
// Placement can be used to display the link somewhere else on the dashboard other than above the visualisations.
Placement *string `json:"placement,omitempty"`
// The source that registered the link (if any)
Source *DashboardControlSourceRef `json:"source,omitempty"`
}
// NewDashboardDashboardLink creates a new DashboardDashboardLink object.
@@ -1270,6 +1272,21 @@ const (
// +k8s:openapi-gen=true
const DashboardDashboardLinkPlacement = "inControlsMenu"
// Source information for controls (e.g. variables or links)
// +k8s:openapi-gen=true
type DashboardControlSourceRef struct {
Uid string `json:"uid"`
// E.g. "prometheus"
SourceId string `json:"sourceId"`
// E.g. "datasource"
SourceType string `json:"sourceType"`
}
// NewDashboardControlSourceRef creates a new DashboardControlSourceRef object.
func NewDashboardControlSourceRef() *DashboardControlSourceRef {
return &DashboardControlSourceRef{}
}
// Time configuration
// It defines the default time config for the time picker, the refresh picker for the specific dashboard.
// +k8s:openapi-gen=true

View File

@@ -44,6 +44,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardConditionalRenderingVariableSpec(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardConstantVariableKind(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardConstantVariableSpec(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardControlSourceRef": schema_pkg_apis_dashboard_v2beta1_DashboardControlSourceRef(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardConversionStatus": schema_pkg_apis_dashboard_v2beta1_DashboardConversionStatus(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardCustomVariableKind": schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableKind(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardCustomVariableSpec": schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableSpec(ref),
@@ -1395,6 +1396,43 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardConstantVariableSpec(ref common.
}
}
func schema_pkg_apis_dashboard_v2beta1_DashboardControlSourceRef(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Source information for controls (e.g. variables or links)",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"uid": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"sourceId": {
SchemaProps: spec.SchemaProps{
Description: "E.g. \"prometheus\"",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"sourceType": {
SchemaProps: spec.SchemaProps{
Description: "E.g. \"datasource\"",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"uid", "sourceId", "sourceType"},
},
},
}
}
func schema_pkg_apis_dashboard_v2beta1_DashboardConversionStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -1669,10 +1707,18 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardDashboardLink(ref common.Referen
Format: "",
},
},
"source": {
SchemaProps: spec.SchemaProps{
Description: "The source that registered the link (if any)",
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardControlSourceRef"),
},
},
},
Required: []string{"title", "type", "icon", "tooltip", "tags", "asDropdown", "targetBlank", "includeVars", "keepTime"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardControlSourceRef"},
}
}

File diff suppressed because one or more lines are too long

View File

@@ -273,6 +273,14 @@ lineage: schemas: [{
uid?: string
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// Source information for controls (e.g. variables or links)
#ControlSourceRef: {
uid: string
sourceId: string // E.g. "prometheus"
sourceType: string // E.g. "datasource"
}
// Links with references to other dashboards or external resources
#DashboardLink: {
// Title to display with the link
@@ -297,6 +305,8 @@ lineage: schemas: [{
includeVars: bool | *false
// If true, includes current time range in the link as query params
keepTime: bool | *false
// The source that registered the link (if any)
source?: #ControlSourceRef
} @cuetsy(kind="interface")

View File

@@ -295,8 +295,8 @@
"@grafana/plugin-ui": "^0.11.1",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "6.52.0",
"@grafana/scenes-react": "6.52.0",
"@grafana/scenes": "6.53.0--canary.1315.20365173522.0",
"@grafana/scenes-react": "6.53.0--canary.1315.20365173522.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",

View File

@@ -1,7 +1,7 @@
import { ComponentType } from 'react';
import { Observable } from 'rxjs';
import { DataSourceRef } from '@grafana/schema';
import { DashboardLink, DataSourceRef } from '@grafana/schema';
import { deprecationWarning } from '../utils/deprecationWarning';
import { makeClassES5Compatible } from '../utils/makeClassES5Compatible';
@@ -17,7 +17,7 @@ import { PanelData } from './panel';
import { GrafanaPlugin, PluginMeta } from './plugin';
import { DataQuery } from './query';
import { Scope } from './scopes';
import { AdHocVariableFilter } from './templateVars';
import { AdHocVariableFilter, TypedVariableModel } from './templateVars';
import { RawTimeRange, TimeRange } from './time';
import { UserStorage } from './userStorage';
import { CustomVariableSupport, DataSourceVariableSupport, StandardVariableSupport } from './variables';
@@ -330,6 +330,16 @@ abstract class DataSourceApi<
*/
getTagValues?(options: DataSourceGetTagValuesOptions<TQuery>): Promise<GetTagResponse> | Promise<MetricFindValue[]>;
/**
* Get default variables that will be added to the dashboard
*/
getDefaultVariables?(): TypedVariableModel[];
/**
* Get default dashboard links that should be added when this datasource is used.
*/
getDefaultLinks?(): DashboardLink[];
/**
* Set after constructor call, as the data source instance is the most common thing to pass around
* we attach the components to this instance for easy access

View File

@@ -195,6 +195,13 @@ export interface BaseVariableModel {
error: any | null;
description: string | null;
usedInRepeat?: boolean;
source?: ControlSourceRef;
}
export interface ControlSourceRef {
uid: string;
sourceId: string;
sourceType: string;
}
export interface SnapshotVariableModel extends VariableWithOptions {

View File

@@ -332,6 +332,14 @@ export interface DashboardLink {
* Placement can be used to display the link somewhere else on the dashboard other than above the visualisations.
*/
placement?: DashboardLinkPlacement;
/**
* The source that registered the link (if any)
*/
source?: {
uid: string;
sourceId: string; // E.g. "prometheus"
sourceType: string; // E.g. "datasource"
};
/**
* List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards
*/

View File

@@ -287,6 +287,8 @@ type DashboardLink struct {
IncludeVars bool `json:"includeVars"`
// If true, includes current time range in the link as query params
KeepTime bool `json:"keepTime"`
// The source that registered the link (if any)
Source *ControlSourceRef `json:"source,omitempty"`
}
// NewDashboardLink creates a new DashboardLink object.
@@ -313,6 +315,20 @@ const (
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
const DashboardLinkPlacement = "inControlsMenu"
// Source information for controls (e.g. variables or links)
type ControlSourceRef struct {
Uid string `json:"uid"`
// E.g. "prometheus"
SourceId string `json:"sourceId"`
// E.g. "datasource"
SourceType string `json:"sourceType"`
}
// NewControlSourceRef creates a new ControlSourceRef object.
func NewControlSourceRef() *ControlSourceRef {
return &ControlSourceRef{}
}
// 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.

View File

@@ -1683,6 +1683,29 @@
},
"additionalProperties": false
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardControlSourceRef": {
"description": "Source information for controls (e.g. variables or links)",
"type": "object",
"required": [
"uid",
"sourceId",
"sourceType"
],
"properties": {
"sourceId": {
"description": "E.g. \"prometheus\"",
"type": "string"
},
"sourceType": {
"description": "E.g. \"datasource\"",
"type": "string"
},
"uid": {
"type": "string"
}
},
"additionalProperties": false
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConversionStatus": {
"description": "ConversionStatus is the status of the conversion of the dashboard.",
"type": "object",
@@ -1836,6 +1859,9 @@
"placement": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLinkPlacement"
},
"source": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardControlSourceRef"
},
"tags": {
"description": "List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards",
"type": "array",
@@ -2274,7 +2300,7 @@
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsConfig"
},
"unit": {
"description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:<suffix>` for custom unit that should go after value.\n`prefix:<prefix>` for custom unit that should go before value.\n`time:<format>` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:<base scale><unit characters>` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n`count:<unit>` for a custom count unit.\n`currency:<unit>` for custom a currency unit.",
"description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:\u003csuffix\u003e` for custom unit that should go after value.\n`prefix:\u003cprefix\u003e` for custom unit that should go before value.\n`time:\u003cformat\u003e` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:\u003cbase scale\u003e\u003cunit characters\u003e` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n`count:\u003cunit\u003e` for a custom count unit.\n`currency:\u003cunit\u003e` for custom a currency unit.",
"type": "string"
},
"writeable": {
@@ -3790,7 +3816,7 @@
],
"properties": {
"options": {
"description": "Map with <value_to_match>: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }",
"description": "Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }",
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult"
@@ -4214,7 +4240,7 @@
}
},
"io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": {
"description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:<name>', where <name> is the name of a field in a struct, or key in a map 'v:<value>', where <value> is the exact json formatted value of a list item 'i:<index>', where <index> is position of a item in a list 'k:<keys>', where <keys> is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff",
"description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff",
"type": "object"
},
"io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": {
@@ -4568,4 +4594,4 @@
}
}
}
}
}

View File

@@ -1698,6 +1698,29 @@
},
"additionalProperties": false
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardControlSourceRef": {
"description": "Source information for controls (e.g. variables or links)",
"type": "object",
"required": [
"uid",
"sourceId",
"sourceType"
],
"properties": {
"sourceId": {
"description": "E.g. \"prometheus\"",
"type": "string"
},
"sourceType": {
"description": "E.g. \"datasource\"",
"type": "string"
},
"uid": {
"type": "string"
}
},
"additionalProperties": false
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConversionStatus": {
"description": "ConversionStatus is the status of the conversion of the dashboard.",
"type": "object",
@@ -1851,6 +1874,9 @@
"placement": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLinkPlacement"
},
"source": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardControlSourceRef"
},
"tags": {
"description": "List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards",
"type": "array",
@@ -2293,7 +2319,7 @@
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThresholdsConfig"
},
"unit": {
"description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:<suffix>` for custom unit that should go after value.\n`prefix:<prefix>` for custom unit that should go before value.\n`time:<format>` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:<base scale><unit characters>` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n`count:<unit>` for a custom count unit.\n`currency:<unit>` for custom a currency unit.",
"description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:\u003csuffix\u003e` for custom unit that should go after value.\n`prefix:\u003cprefix\u003e` for custom unit that should go before value.\n`time:\u003cformat\u003e` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:\u003cbase scale\u003e\u003cunit characters\u003e` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n`count:\u003cunit\u003e` for a custom count unit.\n`currency:\u003cunit\u003e` for custom a currency unit.",
"type": "string"
},
"writeable": {
@@ -3816,7 +3842,7 @@
],
"properties": {
"options": {
"description": "Map with <value_to_match>: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }",
"description": "Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }",
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult"
@@ -4245,7 +4271,7 @@
}
},
"io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": {
"description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:<name>', where <name> is the name of a field in a struct, or key in a map 'v:<value>', where <value> is the exact json formatted value of a list item 'i:<index>', where <index> is position of a item in a list 'k:<keys>', where <keys> is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff",
"description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff",
"type": "object"
},
"io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": {
@@ -4599,4 +4625,4 @@
}
}
}
}
}

View File

@@ -94,6 +94,14 @@ jest.mock('app/features/playlist/PlaylistSrv', () => ({
},
}));
jest.mock('../utils/dashboardControls', () => ({
...jest.requireActual('../utils/dashboardControls'),
loadDefaultControlsFromDatasources: jest.fn().mockResolvedValue({
defaultVariables: [],
defaultLinks: [],
}),
}));
const createTestStore = () =>
configureStore({
reducer: {

View File

@@ -1,7 +1,8 @@
import { locationUtil, UrlQueryMap } from '@grafana/data';
import { locationUtil, TypedVariableModel, UrlQueryMap } from '@grafana/data';
import { t } from '@grafana/i18n';
import { config, getBackendSrv, getDataSourceSrv, isFetchError, locationService } from '@grafana/runtime';
import { sceneGraph } from '@grafana/scenes';
import { DashboardLink } from '@grafana/schema';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { GetRepositoryFilesWithPathApiResponse, provisioningAPIv0alpha1 } from 'app/api/clients/provisioning/v0alpha1';
import { StateManagerBase } from 'app/core/services/StateManagerBase';
@@ -41,6 +42,11 @@ import { DashboardScene } from '../scene/DashboardScene';
import { buildNewDashboardSaveModel, buildNewDashboardSaveModelV2 } from '../serialization/buildNewDashboardSaveModel';
import { transformSaveModelSchemaV2ToScene } from '../serialization/transformSaveModelSchemaV2ToScene';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import {
getDsRefsFromV1Dashboard,
getDsRefsFromV2Dashboard,
loadDefaultControlsFromDatasources,
} from '../utils/dashboardControls';
import { restoreDashboardStateFromLocalStorage } from '../utils/dashboardSessionState';
import { processQueryParamsForDashboardLoad, updateNavModel } from './utils';
@@ -85,6 +91,8 @@ export interface LoadDashboardOptions {
slug?: string;
type?: string;
urlFolderUid?: string;
defaultVariables?: TypedVariableModel[];
defaultLinks?: DashboardLink[];
}
export type HomeDashboardDTO = DashboardDTO & {
@@ -114,6 +122,9 @@ abstract class DashboardScenePageStateManagerBase<T>
abstract reloadDashboard(queryParams: UrlQueryMap): Promise<void>;
abstract transformResponseToScene(rsp: T | null, options: LoadDashboardOptions): DashboardScene | null;
abstract loadSnapshotScene(slug: string): Promise<DashboardScene>;
abstract getDefaultControls(
rsp: T
): Promise<{ defaultVariables: TypedVariableModel[]; defaultLinks: DashboardLink[] }>;
protected cache: Record<string, DashboardScene> = {};
@@ -370,6 +381,10 @@ abstract class DashboardScenePageStateManagerBase<T>
return null;
}
const { defaultVariables, defaultLinks } = await this.getDefaultControls(rsp);
options.defaultVariables = defaultVariables;
options.defaultLinks = defaultLinks;
return this.transformResponseToScene(rsp, options);
}
@@ -610,6 +625,14 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
return this.buildDashboardDTOFromInterpolated(interpolatedDashboard);
}
public getDefaultControls(
rsp: DashboardDTO
): Promise<{ defaultVariables: TypedVariableModel[]; defaultLinks: DashboardLink[] }> {
const datasourceRefs = getDsRefsFromV1Dashboard(rsp);
return loadDefaultControlsFromDatasources(datasourceRefs);
}
public async fetchDashboard({
type,
slug,
@@ -816,7 +839,7 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
}
if (rsp) {
const scene = transformSaveModelSchemaV2ToScene(rsp);
const scene = transformSaveModelSchemaV2ToScene(rsp, options);
// Cache scene only if not coming from Explore, we don't want to cache temporary dashboard
if (options.uid) {
@@ -829,6 +852,14 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
throw new Error('Dashboard not found');
}
public async getDefaultControls(
rsp: DashboardWithAccessInfo<DashboardV2Spec>
): Promise<{ defaultVariables: TypedVariableModel[]; defaultLinks: DashboardLink[] }> {
const datasourceRefs = getDsRefsFromV2Dashboard(rsp);
return loadDefaultControlsFromDatasources(datasourceRefs);
}
public async fetchDashboard({
type,
slug,
@@ -1101,6 +1132,15 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta
public resetActiveManager() {
this.activeManager = shouldForceV2API() ? this.v2Manager : this.v1Manager;
}
public async getDefaultControls(
rsp: DashboardDTO | DashboardWithAccessInfo<DashboardV2Spec>
): Promise<{ defaultVariables: TypedVariableModel[]; defaultLinks: DashboardLink[] }> {
if (isDashboardV2Resource(rsp)) {
return this.v2Manager.getDefaultControls(rsp);
}
return this.v1Manager.getDefaultControls(rsp);
}
}
const managers: {

View File

@@ -5,6 +5,7 @@ import { SceneDataLayerProvider, SceneVariable } from '@grafana/scenes';
import { DashboardLink } from '@grafana/schema';
import { Box, Menu, useStyles2 } from '@grafana/ui';
import { sortDefaultLinksFirst, sortDefaultVarsFirst } from '../../utils/dashboardControls';
import { DashboardLinkRenderer } from '../DashboardLinkRenderer';
import { DataLayerControl } from '../DataLayerControl';
import { VariableValueSelectWrapper } from '../VariableControls';
@@ -36,7 +37,7 @@ export function DashboardControlsMenu({ variables, links, annotations, dashboard
}}
>
{/* Variables */}
{variables.map((variable, index) => (
{sortDefaultVarsFirst(variables).map((variable, index) => (
<div key={variable.state.key}>
<VariableValueSelectWrapper variable={variable} inMenu />
</div>
@@ -54,7 +55,7 @@ export function DashboardControlsMenu({ variables, links, annotations, dashboard
{links.length > 0 && dashboardUID && (
<>
{(variables.length > 0 || annotations.length > 0) && <MenuDivider />}
{links.map((link, index) => (
{sortDefaultLinksFirst(links).map((link, index) => (
<div key={`${link.title}-${index}`}>
<DashboardLinkRenderer link={link} dashboardUID={dashboardUID} inMenu />
</div>

View File

@@ -714,6 +714,396 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
}
`;
exports[`transformSceneToSaveModel Given a simple scene with custom settings Should not transform back links that are registered by a datasource 1`] = `
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana",
},
"enable": true,
"hide": false,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard",
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": true,
"hide": false,
"iconColor": "red",
"name": "Enabled",
"target": {
"lines": 4,
"refId": "Anno",
"scenarioId": "annotations",
},
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": false,
"hide": false,
"iconColor": "yellow",
"name": "Disabled",
"target": {
"lines": 5,
"refId": "Anno",
"scenarioId": "annotations",
},
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": true,
"hide": true,
"iconColor": "dark-purple",
"name": "Hidden",
"target": {
"lines": 6,
"refId": "Anno",
"scenarioId": "annotations",
},
},
],
},
"description": "My custom description",
"editable": false,
"fiscalYearStartMonth": 1,
"graphTooltip": 1,
"id": 1351,
"links": [
{
"asDropdown": false,
"icon": "external link",
"includeVars": false,
"keepTime": false,
"tags": [],
"targetBlank": false,
"title": "Link 1",
"tooltip": "",
"type": "dashboards",
"url": "",
},
{
"asDropdown": false,
"icon": "external link",
"includeVars": false,
"keepTime": false,
"source": {
"sourceId": "prometheus",
"sourceType": "datasource",
"uid": "123456",
},
"tags": [],
"targetBlank": false,
"title": "Link 2",
"tooltip": "",
"type": "dashboards",
"url": "",
},
],
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"description": "This is a simple time series graph",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic",
},
"custom": {
"fillOpacity": 0,
"gradientMode": "none",
"lineWidth": 2,
},
},
"overrides": [],
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0,
},
"id": 28,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true,
},
"tooltip": {
"mode": "single",
"sort": "none",
},
},
"targets": [
{
"alias": "series",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 1,
},
],
"title": "Simple time series graph ",
"type": "timeseries",
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 8,
},
"id": 5,
"panels": [],
"title": "Row title",
"type": "row",
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"fieldConfig": {
"defaults": {},
"overrides": [],
},
"gridPos": {
"h": 10,
"w": 12,
"x": 0,
"y": 9,
},
"id": 29,
"options": {},
"targets": [
{
"alias": "series",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 1,
},
],
"title": "panel inside row",
"type": "timeseries",
},
{
"fieldConfig": {
"defaults": {},
"overrides": [],
},
"gridPos": {
"h": 10,
"w": 11,
"x": 12,
"y": 9,
},
"id": 25,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false,
},
"content": "content",
"mode": "markdown",
},
"pluginVersion": "10.2.0-pre",
"title": "Transparent text panel",
"transparent": true,
"type": "text",
},
],
"preload": false,
"refresh": "5m",
"schemaVersion": 42,
"tags": [
"tag1",
"tag2",
],
"templating": {
"list": [
{
"baseFilters": [],
"datasource": {
"type": "prometheus",
"uid": "wc2AL7L7k",
},
"filters": [],
"name": "Filters",
"type": "adhoc",
},
{
"auto": true,
"auto_count": 30,
"auto_min": "10s",
"current": {
"text": "1m",
"value": "1m",
},
"name": "intervalVar",
"options": [
{
"selected": true,
"text": "1m",
"value": "1m",
},
{
"selected": false,
"text": "10m",
"value": "10m",
},
{
"selected": false,
"text": "30m",
"value": "30m",
},
{
"selected": false,
"text": "1h",
"value": "1h",
},
{
"selected": false,
"text": "6h",
"value": "6h",
},
{
"selected": false,
"text": "12h",
"value": "12h",
},
{
"selected": false,
"text": "1d",
"value": "1d",
},
{
"selected": false,
"text": "7d",
"value": "7d",
},
{
"selected": false,
"text": "14d",
"value": "14d",
},
{
"selected": false,
"text": "30d",
"value": "30d",
},
],
"query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
"refresh": 2,
"type": "interval",
},
{
"current": {
"text": [
"a",
],
"value": [
"a",
],
},
"includeAll": true,
"multi": true,
"name": "customVar",
"options": [],
"query": "a, b, c",
"type": "custom",
},
{
"current": {
"text": "gdev-testdata",
"value": "PD8C576611E62080A",
},
"includeAll": false,
"name": "dsVar",
"options": [],
"query": "grafana-testdata-datasource",
"refresh": 1,
"regex": "",
"type": "datasource",
},
{
"current": {
"text": "A",
"value": "A",
},
"definition": "*",
"includeAll": false,
"name": "query0",
"options": [],
"query": {
"query": "*",
"refId": "StandardVariableQuery",
},
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"type": "query",
},
{
"current": {
"text": "test",
"value": "test",
},
"hide": 2,
"name": "constant",
"query": "test",
"skipUrlSync": true,
"type": "constant",
},
],
},
"time": {
"from": "now-5m",
"to": "now",
},
"timepicker": {
"hidden": true,
"refresh_intervals": [
"5m",
"15m",
"30m",
"1h",
],
},
"timezone": "America/New_York",
"title": "My custom title",
"uid": "nP8rcffGkasd",
"version": 2,
"weekStart": "monday",
}
`;
exports[`transformSceneToSaveModel Given a simple scene with custom settings Should transform back to persisted model 1`] = `
{
"annotations": {
@@ -1087,6 +1477,370 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
}
`;
exports[`transformSceneToSaveModel Given a simple scene with variables Should not transform back variables registered by a datasource 1`] = `
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana",
},
"enable": true,
"hide": false,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard",
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": true,
"hide": false,
"iconColor": "red",
"name": "Enabled",
"target": {
"lines": 4,
"refId": "Anno",
"scenarioId": "annotations",
},
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": false,
"hide": false,
"iconColor": "yellow",
"name": "Disabled",
"target": {
"lines": 5,
"refId": "Anno",
"scenarioId": "annotations",
},
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": true,
"hide": true,
"iconColor": "dark-purple",
"name": "Hidden",
"target": {
"lines": 6,
"refId": "Anno",
"scenarioId": "annotations",
},
},
],
},
"editable": true,
"fiscalYearStartMonth": 1,
"graphTooltip": 1,
"id": 1351,
"links": [],
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"description": "This is a simple time series graph",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic",
},
"custom": {
"fillOpacity": 0,
"gradientMode": "none",
"lineWidth": 2,
},
},
"overrides": [],
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0,
},
"id": 28,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true,
},
"tooltip": {
"mode": "single",
"sort": "none",
},
},
"targets": [
{
"alias": "series",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 1,
},
],
"title": "Simple time series graph ",
"type": "timeseries",
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 8,
},
"id": 5,
"panels": [],
"title": "Row title",
"type": "row",
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"fieldConfig": {
"defaults": {},
"overrides": [],
},
"gridPos": {
"h": 10,
"w": 12,
"x": 0,
"y": 9,
},
"id": 29,
"options": {},
"targets": [
{
"alias": "series",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 1,
},
],
"title": "panel inside row",
"type": "timeseries",
},
{
"fieldConfig": {
"defaults": {},
"overrides": [],
},
"gridPos": {
"h": 10,
"w": 11,
"x": 12,
"y": 9,
},
"id": 25,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false,
},
"content": "content",
"mode": "markdown",
},
"pluginVersion": "10.2.0-pre",
"title": "Transparent text panel",
"transparent": true,
"type": "text",
},
],
"preload": false,
"refresh": "",
"schemaVersion": 42,
"tags": [
"gdev",
"graph-ng",
"demo",
],
"templating": {
"list": [
{
"baseFilters": [],
"datasource": {
"type": "prometheus",
"uid": "wc2AL7L7k",
},
"filters": [],
"name": "Filters",
"type": "adhoc",
},
{
"auto": true,
"auto_count": 30,
"auto_min": "10s",
"current": {
"text": "1m",
"value": "1m",
},
"name": "intervalVar",
"options": [
{
"selected": true,
"text": "1m",
"value": "1m",
},
{
"selected": false,
"text": "10m",
"value": "10m",
},
{
"selected": false,
"text": "30m",
"value": "30m",
},
{
"selected": false,
"text": "1h",
"value": "1h",
},
{
"selected": false,
"text": "6h",
"value": "6h",
},
{
"selected": false,
"text": "12h",
"value": "12h",
},
{
"selected": false,
"text": "1d",
"value": "1d",
},
{
"selected": false,
"text": "7d",
"value": "7d",
},
{
"selected": false,
"text": "14d",
"value": "14d",
},
{
"selected": false,
"text": "30d",
"value": "30d",
},
],
"query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
"refresh": 2,
"type": "interval",
},
{
"current": {
"text": [
"a",
],
"value": [
"a",
],
},
"includeAll": true,
"multi": true,
"name": "customVar",
"options": [],
"query": "a, b, c",
"type": "custom",
},
{
"current": {
"text": "gdev-testdata",
"value": "PD8C576611E62080A",
},
"includeAll": false,
"name": "dsVar",
"options": [],
"query": "grafana-testdata-datasource",
"refresh": 1,
"regex": "",
"type": "datasource",
},
{
"current": {
"text": "A",
"value": "A",
},
"definition": "*",
"includeAll": false,
"name": "query0",
"options": [],
"query": {
"query": "*",
"refId": "StandardVariableQuery",
},
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"type": "query",
},
{
"current": {
"text": "test",
"value": "test",
},
"hide": 2,
"name": "constant",
"query": "test",
"skipUrlSync": true,
"type": "constant",
},
],
},
"time": {
"from": "now-5m",
"to": "now",
},
"timepicker": {
"refresh_intervals": [
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d",
],
},
"timezone": "America/New_York",
"title": "Dashboard to load1",
"uid": "nP8rcffGkasd",
"version": 2,
"weekStart": "saturday",
}
`;
exports[`transformSceneToSaveModel Given a simple scene with variables Should transform back to persisted model 1`] = `
{
"annotations": {

View File

@@ -55,6 +55,12 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
const variables: VariableModel[] = [];
for (const variable of set.state.variables) {
// Skipping default variables
// (Default variables don't get persisted to the JSON schema.)
if (variable.state.source !== undefined) {
continue;
}
const commonProperties = {
name: variable.state.name,
label: variable.state.label,
@@ -312,6 +318,12 @@ export function sceneVariablesSetToSchemaV2Variables(
> = [];
for (const variable of set.state.variables) {
// Skipping default variables
// (Default variables don't get persisted to the JSON schema.)
if (variable.state.source !== undefined) {
continue;
}
const commonProperties = {
name: variable.state.name,
label: variable.state.label,

View File

@@ -1,5 +1,6 @@
import { uniqueId } from 'lodash';
import { TypedVariableModel } from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
import {
AdHocFiltersVariable,
@@ -65,6 +66,7 @@ import { DashboardMeta } from 'app/types/dashboard';
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
import { dashboardAnalyticsInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior';
import { LoadDashboardOptions } from '../pages/DashboardScenePageStateManager';
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls';
@@ -74,6 +76,7 @@ import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior';
import { DashboardScene } from '../scene/DashboardScene';
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
import { getIntervalsFromQueryString } from '../utils/utils';
import { createSceneVariableFromVariableModel as createSceneVariableFromVariableModelV1 } from '../utils/variables';
import { transformV2ToV1AnnotationQuery } from './annotations';
import { SnapshotVariable } from './custom-variables/SnapshotVariable';
@@ -101,7 +104,10 @@ export type TypedVariableModelV2 =
| AdhocVariableKind
| SwitchVariableKind;
export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<DashboardV2Spec>): DashboardScene {
export function transformSaveModelSchemaV2ToScene(
dto: DashboardWithAccessInfo<DashboardV2Spec>,
options?: LoadDashboardOptions
): DashboardScene {
const { spec: dashboard, metadata, apiVersion } = dto;
const found = dashboard.annotations.some((item) => item.spec.builtIn);
@@ -179,8 +185,6 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
.get(dashboard.layout.kind)
.deserialize(dashboard.layout, dashboard.elements, dashboard.preload);
//createLayoutManager(dashboard);
// Create profiler once and reuse to avoid duplicate metadata setting
const dashboardProfiler = getDashboardSceneProfilerWithMetadata(metadata.name, dashboard.title);
@@ -208,7 +212,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
preload: dashboard.preload,
id: dashboardId,
isDirty: false,
links: dashboard.links,
links: [...dashboard.links, ...(options?.defaultLinks ?? [])],
meta,
tags: dashboard.tags,
title: dashboard.title,
@@ -224,7 +228,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
weekStart: dashboard.timeSettings.weekStart,
UNSAFE_nowDelay: dashboard.timeSettings.nowDelay,
}),
$variables: getVariables(dashboard, meta.isSnapshot ?? false),
$variables: getVariables(dashboard, meta.isSnapshot ?? false, options?.defaultVariables),
$behaviors: [
new behaviors.CursorSync({
sync: transformCursorSyncV2ToV1(dashboard.cursorSync),
@@ -269,19 +273,24 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
return dashboardScene;
}
function getVariables(dashboard: DashboardV2Spec, isSnapshot: boolean): SceneVariableSet | undefined {
function getVariables(
dashboard: DashboardV2Spec,
isSnapshot: boolean,
defaultVariables?: TypedVariableModel[]
): SceneVariableSet | undefined {
let variables: SceneVariableSet | undefined;
if (isSnapshot) {
variables = createVariablesForSnapshot(dashboard);
} else {
variables = createVariablesForDashboard(dashboard);
variables = createVariablesForDashboard(dashboard, defaultVariables);
}
return variables;
}
function createVariablesForDashboard(dashboard: DashboardV2Spec) {
function createVariablesForDashboard(dashboard: DashboardV2Spec, defaultVariables: TypedVariableModel[] = []) {
const isDefined = (v: SceneVariable | null): v is SceneVariable => Boolean(v);
const variableObjects = dashboard.variables
.map((v) => {
try {
@@ -293,7 +302,21 @@ function createVariablesForDashboard(dashboard: DashboardV2Spec) {
})
// TODO: Remove filter
// Added temporarily to allow skipping non-compatible variables
.filter((v): v is SceneVariable => Boolean(v));
.filter(isDefined);
// Default variables are defined using the `TypedVariableModel` type, so we are still using the V1 transformer to create a scene variable from them.
const defaultVariableObjects = defaultVariables
? defaultVariables
.map((v) => {
try {
return createSceneVariableFromVariableModelV1(v);
} catch (err) {
console.error(err);
return null;
}
})
.filter(isDefined)
: [];
// Explicitly disable scopes for public dashboards
if (config.featureToggles.scopeFilters && !config.publicDashboardAccessToken) {
@@ -301,7 +324,7 @@ function createVariablesForDashboard(dashboard: DashboardV2Spec) {
}
return new SceneVariableSet({
variables: variableObjects,
variables: [...variableObjects, ...defaultVariableObjects],
});
}

View File

@@ -271,7 +271,7 @@ export function createDashboardSceneFromDashboardModel(
if (oldModel.meta.isSnapshot) {
variables = createVariablesForSnapshot(oldModel);
} else {
variables = createVariablesForDashboard(oldModel);
variables = createVariablesForDashboard(oldModel, options?.defaultVariables);
}
if (oldModel.annotations?.list?.length && !oldModel.isSnapshot()) {
@@ -373,7 +373,7 @@ export function createDashboardSceneFromDashboardModel(
preload: dto.preload ?? false,
id: oldModel.id,
isDirty: false,
links: oldModel.links || [],
links: [...(oldModel.links ?? []), ...(options?.defaultLinks ?? [])],
meta: oldModel.meta,
tags: oldModel.tags || [],
title: oldModel.title,

View File

@@ -167,29 +167,52 @@ jest.mock('@grafana/scenes', () => ({
describe('transformSceneToSaveModel', () => {
describe('Given a simple scene with custom settings', () => {
const dashboardWithCustomSettings = {
...dashboard_to_load1,
title: 'My custom title',
description: 'My custom description',
tags: ['tag1', 'tag2'],
timezone: 'America/New_York',
weekStart: 'monday',
graphTooltip: 1,
editable: false,
refresh: '5m',
timepicker: {
...dashboard_to_load1.timepicker,
refresh_intervals: ['5m', '15m', '30m', '1h'],
hidden: true,
},
links: [{ ...NEW_LINK, title: 'Link 1' }],
};
it('Should transform back to persisted model', () => {
const dashboardWithCustomSettings = {
...dashboard_to_load1,
title: 'My custom title',
description: 'My custom description',
tags: ['tag1', 'tag2'],
timezone: 'America/New_York',
weekStart: 'monday',
graphTooltip: 1,
editable: false,
refresh: '5m',
timepicker: {
...dashboard_to_load1.timepicker,
refresh_intervals: ['5m', '15m', '30m', '1h'],
hidden: true,
},
links: [{ ...NEW_LINK, title: 'Link 1' }],
};
const scene = transformSaveModelToScene({ dashboard: dashboardWithCustomSettings as DashboardDataDTO, meta: {} });
const saveModel = transformSceneToSaveModel(scene);
expect(saveModel).toMatchSnapshot();
});
it('Should not transform back links that are registered by a datasource', () => {
const scene = transformSaveModelToScene({
dashboard: {
...dashboardWithCustomSettings,
links: [
{ ...NEW_LINK, title: 'Link 1' },
// This link should not be part of the JSON model, as it was registered by (and managed by) a datasource plugin
{
...NEW_LINK,
title: 'Link 2',
source: { uid: '123456', sourceId: 'prometheus', sourceType: 'datasource' },
},
],
} as DashboardDataDTO,
meta: {},
});
const saveModel = transformSceneToSaveModel(scene);
expect(saveModel).toMatchSnapshot();
});
});
describe('Given a simple scene with variables', () => {
@@ -197,6 +220,65 @@ describe('transformSceneToSaveModel', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as DashboardDataDTO, meta: {} });
const saveModel = transformSceneToSaveModel(scene);
expect(saveModel).toMatchSnapshot();
});
it('Should not transform back variables registered by a datasource', () => {
const scene = transformSaveModelToScene({
dashboard: {
...dashboard_to_load1,
templating: {
list: [
...dashboard_to_load1.templating.list,
{
current: {
selected: true,
text: ['a'],
value: ['a'],
},
hide: 0,
includeAll: true,
multi: true,
name: 'customVar',
options: [
{
selected: false,
text: 'All',
value: '$__all',
},
{
selected: true,
text: 'a',
value: 'a',
},
{
selected: false,
text: 'b',
value: 'b',
},
{
selected: false,
text: 'c',
value: 'c',
},
],
query: 'a, b, c',
skipUrlSync: false,
type: 'custom',
// This marks that the variable was registered by a datasource
source: {
uid: '123456',
sourceId: 'prometheus',
sourceType: 'datasource',
},
},
],
},
} as DashboardDataDTO,
meta: {},
});
const saveModel = transformSceneToSaveModel(scene);
expect(saveModel).toMatchSnapshot();
});
});

View File

@@ -204,6 +204,24 @@ describe('transformSceneToSaveModelSchemaV2', () => {
tooltip: '',
type: 'link',
},
// This link is was added by a datasource, we wouldn't like it to end up in the JSON schema
{
title: 'Default link',
url: 'http://test.com',
asDropdown: false,
icon: '',
includeVars: false,
keepTime: false,
tags: [],
targetBlank: false,
tooltip: '',
type: 'link',
source: {
uid: '123456',
sourceId: 'prometheus',
sourceType: 'datasource',
},
},
],
body: new DefaultGridLayoutManager({
grid: new SceneGridLayout({

View File

@@ -91,19 +91,23 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
liveNow: getLiveNow(sceneDash),
preload: sceneDash.preload ?? defaultDashboardV2Spec().preload,
editable: sceneDash.editable ?? defaultDashboardV2Spec().editable,
links: (sceneDash.links || []).map((link) => ({
title: link.title ?? defaultDashboardLink().title,
url: link.url ?? defaultDashboardLink().url,
type: link.type ?? defaultDashboardLinkType(),
icon: link.icon ?? defaultDashboardLink().icon,
tooltip: link.tooltip ?? defaultDashboardLink().tooltip,
tags: link.tags ?? defaultDashboardLink().tags,
asDropdown: link.asDropdown ?? defaultDashboardLink().asDropdown,
keepTime: link.keepTime ?? defaultDashboardLink().keepTime,
includeVars: link.includeVars ?? defaultDashboardLink().includeVars,
targetBlank: link.targetBlank ?? defaultDashboardLink().targetBlank,
...(link.placement !== undefined && { placement: link.placement }),
})),
links: (sceneDash.links || [])
// Links with a `source` property didn't come from the persisted JSON schema, so we also skip them
// from generating the JSON model from the scenes object.
.filter((link) => link.source === undefined)
.map((link) => ({
title: link.title ?? defaultDashboardLink().title,
url: link.url ?? defaultDashboardLink().url,
type: link.type ?? defaultDashboardLinkType(),
icon: link.icon ?? defaultDashboardLink().icon,
tooltip: link.tooltip ?? defaultDashboardLink().tooltip,
tags: link.tags ?? defaultDashboardLink().tags,
asDropdown: link.asDropdown ?? defaultDashboardLink().asDropdown,
keepTime: link.keepTime ?? defaultDashboardLink().keepTime,
includeVars: link.includeVars ?? defaultDashboardLink().includeVars,
targetBlank: link.targetBlank ?? defaultDashboardLink().targetBlank,
...(link.placement !== undefined && { placement: link.placement }),
})),
tags: sceneDash.tags ?? defaultDashboardV2Spec().tags,
// EOF dashboard settings

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,171 @@
import { TypedVariableModel } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { SceneVariable } from '@grafana/scenes';
import { DashboardLink, DataSourceRef, VariableHide } from '@grafana/schema';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { DashboardDTO } from 'app/types/dashboard';
import { getRuntimePanelDataSource } from '../serialization/layoutSerializers/utils';
export const loadDatasources = (refs: DataSourceRef[]) => {
return Promise.all(refs.map((ref) => getDataSourceSrv().get(ref)));
};
// Deduplicates datasource refs by type, keeping only one ref per datasource plugin type
export const deduplicateDatasourceRefsByType = (refs: Array<DataSourceRef | null | undefined>): DataSourceRef[] => {
const dsByType: Record<string, DataSourceRef> = {};
for (const ref of refs) {
if (ref && ref.type && !dsByType[ref.type]) {
dsByType[ref.type] = ref;
}
}
return Object.values(dsByType);
};
export const loadDefaultControlsFromDatasources = async (refs: DataSourceRef[]) => {
const datasources = await loadDatasources(refs);
const defaultVariables: TypedVariableModel[] = [];
const defaultLinks: DashboardLink[] = [];
// Default variables
for (const ds of datasources) {
if (ds.getDefaultVariables) {
const dsVariables = ds.getDefaultVariables();
if (dsVariables && dsVariables.length) {
defaultVariables.push(
...dsVariables.map((v) => ({
...v,
// Putting under the dashbaord controls menu by default
hide: VariableHide.inControlsMenu,
source: {
uid: ds.uid,
sourceId: ds.type,
sourceType: 'datasource',
},
}))
);
}
}
// Default links
if (ds.getDefaultLinks) {
const dsLinks = ds.getDefaultLinks();
if (dsLinks && dsLinks.length) {
defaultLinks.push(
...dsLinks.map((l) => ({
...l,
isDefault: true,
parentDatasourceRef: ds.getRef(),
// Putting under the dashboard-controls menu by default
placement: 'inControlsMenu' as const,
source: {
uid: ds.uid,
sourceId: ds.type,
sourceType: 'datasource',
},
}))
);
}
}
}
return { defaultVariables, defaultLinks };
};
export const getDsRefsFromV1Dashboard = (rsp: DashboardDTO) => {
const dashboardModel = new DashboardModel(rsp.dashboard, rsp.meta);
// Datasources from panels
const datasourceRefs = dashboardModel.panels
.filter((panel) => panel.type !== 'row')
.map((panel): DataSourceRef | null | undefined =>
panel.datasource
? panel.datasource
: panel.targets?.find((t) => t.datasource !== null && t.datasource !== undefined)?.datasource
)
.filter((ref) => ref !== null && ref !== undefined);
// Datasources from variables
if (dashboardModel.templating?.list) {
for (const variable of dashboardModel.templating.list) {
if (variable.type === 'query' && variable.datasource) {
datasourceRefs.push(variable.datasource);
} else if (variable.type === 'datasource' && variable.query) {
datasourceRefs.push({ type: variable.query });
}
}
}
return deduplicateDatasourceRefsByType(datasourceRefs);
};
export const getDsRefsFromV2Dashboard = (rsp: DashboardWithAccessInfo<DashboardV2Spec>) => {
const datasourceRefs: Array<DataSourceRef | null | undefined> = [];
//Datasources from panels
if (rsp.spec.elements) {
for (const element of Object.values(rsp.spec.elements)) {
if (element.kind === 'Panel') {
const panel = element;
if (panel.spec.data?.spec?.queries) {
for (const query of panel.spec.data.spec.queries) {
const queryDs = query.spec.query.datasource?.name
? { uid: query.spec.query.datasource.name, type: query.spec.query.group }
: getRuntimePanelDataSource(query.spec.query);
if (queryDs) {
datasourceRefs.push(queryDs);
}
}
}
}
}
}
// Datasources from variables
if (rsp.spec.variables) {
for (const variable of rsp.spec.variables) {
if (variable.kind === 'QueryVariable') {
const queryVar = variable;
if (queryVar.spec.query?.datasource?.name) {
datasourceRefs.push({
uid: queryVar.spec.query.datasource.name,
type: queryVar.spec.query.group,
});
} else if (queryVar.spec.query?.group) {
datasourceRefs.push({ type: queryVar.spec.query.group });
}
} else if (variable.kind === 'DatasourceVariable') {
if (variable.spec.pluginId) {
datasourceRefs.push({ type: variable.spec.pluginId });
}
}
}
}
return deduplicateDatasourceRefsByType(datasourceRefs);
};
const sortByProp = <T>(items: T[], propGetter: (item: T) => Object | undefined) => {
return items.sort((a, b) => {
const aProp = propGetter(a) ?? false;
const bProp = propGetter(b) ?? false;
if (aProp && !bProp) {
return -1;
}
if (!aProp && bProp) {
return 1;
}
return 0;
});
};
export const sortDefaultVarsFirst = (items: SceneVariable[]) => sortByProp(items, (item) => item.state.source);
export const sortDefaultLinksFirst = (items: DashboardLink[]) => sortByProp(items, (item) => item.source);

View File

@@ -22,8 +22,8 @@ import { getCurrentValueForOldIntervalModel, getIntervalsFromQueryString } from
const DEFAULT_DATASOURCE = 'default';
export function createVariablesForDashboard(oldModel: DashboardModel) {
const variableObjects = oldModel.templating.list
export function createVariablesForDashboard(oldModel: DashboardModel, defaultVariables: TypedVariableModel[] = []) {
const variableObjects = [...oldModel.templating.list, ...defaultVariables]
.map((v) => {
try {
return createSceneVariableFromVariableModel(v);
@@ -42,7 +42,7 @@ export function createVariablesForDashboard(oldModel: DashboardModel) {
}
return new SceneVariableSet({
variables: variableObjects,
variables: [...variableObjects],
});
}
@@ -143,6 +143,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
name: variable.name,
label: variable.label,
description: variable.description,
source: variable.source,
};
if (variable.type === 'adhoc') {
const originFilters: AdHocVariableFilter[] = [];

View File

@@ -21,7 +21,7 @@ afterEach(() => {
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
[P in keyof T]?: Partial<T[P]>;
}
: T;

View File

@@ -3604,11 +3604,11 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes-react@npm:6.52.0":
version: 6.52.0
resolution: "@grafana/scenes-react@npm:6.52.0"
"@grafana/scenes-react@npm:6.53.0--canary.1315.20365173522.0":
version: 6.53.0--canary.1315.20365173522.0
resolution: "@grafana/scenes-react@npm:6.53.0--canary.1315.20365173522.0"
dependencies:
"@grafana/scenes": "npm:6.52.0"
"@grafana/scenes": "npm:6.53.0--canary.1315.20365173522.0"
lru-cache: "npm:^10.2.2"
react-use: "npm:^17.4.0"
peerDependencies:
@@ -3620,7 +3620,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/7f121bcc4fd50f525c7c3457666ad3a32b04783d322d6715aedb6119538f911d0ec265c9c5b49a80478c1deb99286d36d003399a8831f76b6c483f4458b4ce8b
checksum: 10/10ef8b3d22c96bb9acd383f4f1fac7eb677ab5f1c517d5320a286013b77361acaac783c3216b0cfe73c514591a6c5a29e2b785e55aa19a02ee2259580dcec3ec
languageName: node
linkType: hard
@@ -3650,9 +3650,9 @@ __metadata:
languageName: node
linkType: hard
"@grafana/scenes@npm:6.52.0":
version: 6.52.0
resolution: "@grafana/scenes@npm:6.52.0"
"@grafana/scenes@npm:6.53.0--canary.1315.20365173522.0":
version: 6.53.0--canary.1315.20365173522.0
resolution: "@grafana/scenes@npm:6.53.0--canary.1315.20365173522.0"
dependencies:
"@floating-ui/react": "npm:^0.26.16"
"@leeoniya/ufuzzy": "npm:^1.0.16"
@@ -3672,7 +3672,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/e52e0fb83396776c6cb79f8ac6a8aad0799eb2ccce9d0139f5734a49c3add7a1e3b97f14e0142c95b2bceee3ed8fa97b675b9b94c02382ecd683f470d06ef145
checksum: 10/5f651027143a5fd1e9875ecf4b596aac4f43ea0553dd656c04a60698b70cc7c74eed43c6d405eab2e3f9f8fa2854e8eced1f2f0cf0ddf5f69d706fa4321dad72
languageName: node
linkType: hard
@@ -19508,8 +19508,8 @@ __metadata:
"@grafana/plugin-ui": "npm:^0.11.1"
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/scenes": "npm:6.52.0"
"@grafana/scenes-react": "npm:6.52.0"
"@grafana/scenes": "npm:6.53.0--canary.1315.20365173522.0"
"@grafana/scenes-react": "npm:6.53.0--canary.1315.20365173522.0"
"@grafana/schema": "workspace:*"
"@grafana/sql": "workspace:*"
"@grafana/test-utils": "workspace:*"