diff --git a/packages/grafana-data/src/types/annotations.ts b/packages/grafana-data/src/types/annotations.ts index 559504ee7b9..3d840020ee4 100644 --- a/packages/grafana-data/src/types/annotations.ts +++ b/packages/grafana-data/src/types/annotations.ts @@ -3,13 +3,13 @@ import { ComponentType } from 'react'; import { QueryEditorProps } from './datasource'; import { DataFrame } from './dataFrame'; -import { DataQuery, DatasourceRef } from './query'; +import { DataQuery, DataSourceRef } from './query'; /** * This JSON object is stored in the dashboard json model. */ export interface AnnotationQuery { - datasource?: DatasourceRef | string | null; + datasource?: DataSourceRef | string | null; enable: boolean; name: string; diff --git a/packages/grafana-data/src/types/dashboard.ts b/packages/grafana-data/src/types/dashboard.ts index fc69f8f75c9..12ce41cbd69 100644 --- a/packages/grafana-data/src/types/dashboard.ts +++ b/packages/grafana-data/src/types/dashboard.ts @@ -1,5 +1,5 @@ import { FieldConfigSource } from './fieldOverrides'; -import { DataQuery, DatasourceRef } from './query'; +import { DataQuery, DataSourceRef } from './query'; export enum DashboardCursorSync { Off, @@ -30,7 +30,7 @@ export interface PanelModel { pluginVersion?: string; /** The datasource used in all targets */ - datasource?: DatasourceRef | null; + datasource?: DataSourceRef | null; /** The queries in a panel */ targets?: DataQuery[]; diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 64c4151af46..05304075dfa 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -13,6 +13,7 @@ import { LiveChannelSupport } from './live'; import { CustomVariableSupport, DataSourceVariableSupport, StandardVariableSupport } from './variables'; import { makeClassES5Compatible } from '../utils/makeClassES5Compatible'; import { DataQuery } from './query'; +import { DataSourceRef } from '.'; export interface DataSourcePluginOptionsEditorProps { options: DataSourceSettings; @@ -315,6 +316,11 @@ abstract class DataSourceApi< */ getHighlighterExpression?(query: TQuery): string[]; + /** Get an identifier object for this datasource instance */ + getRef(): DataSourceRef { + return { type: this.type, uid: this.uid }; + } + /** * Used in explore */ diff --git a/packages/grafana-data/src/types/query.ts b/packages/grafana-data/src/types/query.ts index 84025ce1bb0..69d7d59b801 100644 --- a/packages/grafana-data/src/types/query.ts +++ b/packages/grafana-data/src/types/query.ts @@ -8,11 +8,15 @@ export enum DataTopic { } /** - * In 8.2, this will become an interface - * * @public */ -export type DatasourceRef = string; +export interface DataSourceRef { + /** The plugin type-id */ + type?: string; + + /** Specific datasource instance */ + uid?: string; +} /** * These are the common properties available to all queries in all datasources @@ -46,5 +50,5 @@ export interface DataQuery { * For mixed data sources the selected datasource is on the query level. * For non mixed scenarios this is undefined. */ - datasource?: DatasourceRef; + datasource?: DataSourceRef; } diff --git a/packages/grafana-data/src/types/queryRunner.ts b/packages/grafana-data/src/types/queryRunner.ts index ff526eb8dbd..ef5a9100d79 100644 --- a/packages/grafana-data/src/types/queryRunner.ts +++ b/packages/grafana-data/src/types/queryRunner.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs'; -import { DataQuery, DatasourceRef } from './query'; +import { DataQuery, DataSourceRef } from './query'; import { DataSourceApi } from './datasource'; import { PanelData } from './panel'; import { ScopedVars } from './ScopedVars'; @@ -11,7 +11,7 @@ import { TimeRange, TimeZone } from './time'; * @internal */ export interface QueryRunnerOptions { - datasource: DatasourceRef | DataSourceApi | null; + datasource: DataSourceRef | DataSourceApi | null; queries: DataQuery[]; panelId?: number; dashboardId?: number; diff --git a/packages/grafana-data/src/utils/datasource.ts b/packages/grafana-data/src/utils/datasource.ts index 3697a132005..f92eed2858f 100644 --- a/packages/grafana-data/src/utils/datasource.ts +++ b/packages/grafana-data/src/utils/datasource.ts @@ -1,4 +1,40 @@ -import { DataSourcePluginOptionsEditorProps, SelectableValue, KeyValue, DataSourceSettings } from '../types'; +import { isString } from 'lodash'; +import { + DataSourcePluginOptionsEditorProps, + SelectableValue, + KeyValue, + DataSourceSettings, + DataSourceInstanceSettings, + DataSourceRef, +} from '../types'; + +/** + * Convert instance settings to a reference + * + * @public + */ +export function getDataSourceRef(ds: DataSourceInstanceSettings): DataSourceRef { + return { uid: ds.uid, type: ds.type }; +} + +function isDataSourceRef(ref: DataSourceRef | string | null): ref is DataSourceRef { + return typeof ref === 'object' && (typeof ref?.uid === 'string' || typeof ref?.uid === 'undefined'); +} + +/** + * Get the UID from a string of reference + * + * @public + */ +export function getDataSourceUID(ref: DataSourceRef | string | null): string | undefined { + if (isDataSourceRef(ref)) { + return ref.uid; + } + if (isString(ref)) { + return ref; + } + return undefined; +} export const onUpdateDatasourceOption = (props: DataSourcePluginOptionsEditorProps, key: keyof DataSourceSettings) => ( event: React.SyntheticEvent diff --git a/packages/grafana-runtime/src/components/DataSourcePicker.tsx b/packages/grafana-runtime/src/components/DataSourcePicker.tsx index 334f7cd1dbe..68384b34f3d 100644 --- a/packages/grafana-runtime/src/components/DataSourcePicker.tsx +++ b/packages/grafana-runtime/src/components/DataSourcePicker.tsx @@ -3,7 +3,13 @@ import React, { PureComponent } from 'react'; // Components import { HorizontalGroup, PluginSignatureBadge, Select, stylesFactory } from '@grafana/ui'; -import { DataSourceInstanceSettings, isUnsignedPluginSignature, SelectableValue } from '@grafana/data'; +import { + DataSourceInstanceSettings, + DataSourceRef, + getDataSourceUID, + isUnsignedPluginSignature, + SelectableValue, +} from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { getDataSourceSrv } from '../services/dataSourceSrv'; import { css, cx } from '@emotion/css'; @@ -15,7 +21,7 @@ import { css, cx } from '@emotion/css'; */ export interface DataSourcePickerProps { onChange: (ds: DataSourceInstanceSettings) => void; - current: string | null; + current: DataSourceRef | string | null; // uid hideTextValue?: boolean; onBlur?: () => void; autoFocus?: boolean; @@ -85,7 +91,6 @@ export class DataSourcePicker extends PureComponent | undefined { const { current, hideTextValue, noDefault } = this.props; - if (!current && noDefault) { return; } @@ -95,16 +100,17 @@ export class DataSourcePicker extends PureComponent; + get(ref?: DataSourceRef | string | null, scopedVars?: ScopedVars): Promise; /** * Get a list of data sources @@ -24,7 +24,7 @@ export interface DataSourceSrv { /** * Get settings and plugin metadata by name or uid */ - getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined; + getInstanceSettings(ref?: DataSourceRef | string | null): DataSourceInstanceSettings | undefined; } /** @public */ diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index 55f803980ad..0046c4aaa12 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -102,7 +102,7 @@ class DataSourceWithBackend< const ds = getDataSourceSrv().getInstanceSettings(q.datasource); if (!ds) { - throw new Error('Unknown Datasource: ' + q.datasource); + throw new Error(`Unknown Datasource: ${JSON.stringify(q.datasource)}`); } datasourceId = ds.id; diff --git a/pkg/expr/graph.go b/pkg/expr/graph.go index 6f02526d104..ab107e2a29c 100644 --- a/pkg/expr/graph.go +++ b/pkg/expr/graph.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/expr/mathexp" - "gonum.org/v1/gonum/graph" "gonum.org/v1/gonum/graph/simple" "gonum.org/v1/gonum/graph/topo" ) @@ -129,9 +128,11 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) { for _, query := range req.Queries { rawQueryProp := make(map[string]interface{}) queryBytes, err := query.JSON.MarshalJSON() + if err != nil { return nil, err } + err = json.Unmarshal(queryBytes, &rawQueryProp) if err != nil { return nil, err @@ -145,23 +146,23 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) { DatasourceUID: query.DatasourceUID, } - dsName, err := rn.GetDatasourceName() + isExpr, err := rn.IsExpressionQuery() if err != nil { return nil, err } - dsUID := rn.DatasourceUID + var node Node - var node graph.Node - switch { - case dsName == DatasourceName || dsUID == DatasourceUID: + if isExpr { node, err = buildCMDNode(dp, rn) - default: // If it's not an expression query, it's a data source query. + } else { node, err = s.buildDSNode(dp, rn, req) } + if err != nil { return nil, err } + dp.AddNode(node) } return dp, nil diff --git a/pkg/expr/graph_test.go b/pkg/expr/graph_test.go index 3b7f0928578..b743419416c 100644 --- a/pkg/expr/graph_test.go +++ b/pkg/expr/graph_test.go @@ -195,6 +195,33 @@ func TestServicebuildPipeLine(t *testing.T) { }, expectErrContains: "classic conditions may not be the input for other expressions", }, + { + name: "Queries with new datasource ref object", + req: &Request{ + Queries: []Query{ + { + RefID: "A", + JSON: json.RawMessage(`{ + "datasource": { + "uid": "MyDS" + } + }`), + }, + { + RefID: "B", + JSON: json.RawMessage(`{ + "datasource": { + "uid": "MyDS" + }, + "expression": "A", + "reducer": "mean", + "type": "reduce" + }`), + }, + }, + }, + expectedOrder: []string{"B", "A"}, + }, } s := Service{} for _, tt := range tests { diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index 76be62b1def..edb7ed25a82 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -33,16 +33,59 @@ type rawNode struct { DatasourceUID string } -func (rn *rawNode) GetDatasourceName() (string, error) { +func (rn *rawNode) GetDatasourceUID() (string, error) { + if rn.DatasourceUID != "" { + return rn.DatasourceUID, nil + } + rawDs, ok := rn.Query["datasource"] if !ok { - return "", nil + return "", fmt.Errorf("no datasource property found in query model") } - dsName, ok := rawDs.(string) + + // For old queries with string datasource prop representing data source name + if dsName, ok := rawDs.(string); ok { + return dsName, nil + } + + dsRef, ok := rawDs.(map[string]interface{}) if !ok { - return "", fmt.Errorf("expted datasource identifier to be a string, got %T", rawDs) + return "", fmt.Errorf("data source property is not an object nor string, got %T", rawDs) } - return dsName, nil + + if dsUid, ok := dsRef["uid"].(string); ok { + return dsUid, nil + } + + return "", fmt.Errorf("no datasource uid found for query, got %T", rn.Query) +} + +func (rn *rawNode) IsExpressionQuery() (bool, error) { + if rn.DatasourceUID != "" { + return rn.DatasourceUID == DatasourceUID, nil + } + + rawDs, ok := rn.Query["datasource"] + if !ok { + return false, fmt.Errorf("no datasource property found in query model") + } + + // For old queries with string datasource prop representing data source name + dsName, ok := rawDs.(string) + if ok && dsName == DatasourceName { + return true, nil + } + + dsRef, ok := rawDs.(map[string]interface{}) + if !ok { + return false, nil + } + + if dsRef["uid"].(string) == DatasourceUID { + return true, nil + } + + return false, nil } func (rn *rawNode) GetCommandType() (c CommandType, err error) { @@ -171,18 +214,18 @@ func (s *Service) buildDSNode(dp *simple.DirectedGraph, rn *rawNode, req *Reques } rawDsID, ok := rn.Query["datasourceId"] - switch ok { - case true: + if ok { floatDsID, ok := rawDsID.(float64) if !ok { return nil, fmt.Errorf("expected datasourceId to be a float64, got type %T for refId %v", rawDsID, rn.RefID) } dsNode.datasourceID = int64(floatDsID) - default: - if rn.DatasourceUID == "" { + } else { + dsUid, err := rn.GetDatasourceUID() + if err != nil { return nil, fmt.Errorf("neither datasourceId or datasourceUid in expression data source request for refId %v", rn.RefID) } - dsNode.datasourceUID = rn.DatasourceUID + dsNode.datasourceUID = dsUid } var floatIntervalMS float64 diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 3373990fedb..f8cb0500236 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -30,8 +30,24 @@ func NewDashAlertExtractor(dash *models.Dashboard, orgID int64, user *models.Sig } } -func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*models.DataSource, error) { - if dsName == "" { +func (e *DashAlertExtractor) lookupQueryDataSource(panel *simplejson.Json, panelQuery *simplejson.Json) (*models.DataSource, error) { + dsName := "" + dsUid := "" + + datasource, ok := panelQuery.CheckGet("datasource") + + if !ok { + fmt.Printf("no query level data soure \n") + datasource = panel.Get("datasource") + } + + if name, err := datasource.String(); err == nil { + dsName = name + } else if uid, ok := datasource.CheckGet("uid"); ok { + dsUid = uid.MustString() + } + + if dsName == "" && dsUid == "" { query := &models.GetDefaultDataSourceQuery{OrgId: e.OrgID} if err := bus.DispatchCtx(context.TODO(), query); err != nil { return nil, err @@ -39,7 +55,7 @@ func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*models.DataSour return query.Result, nil } - query := &models.GetDataSourceQuery{Name: dsName, OrgId: e.OrgID} + query := &models.GetDataSourceQuery{Name: dsName, Uid: dsUid, OrgId: e.OrgID} if err := bus.DispatchCtx(context.TODO(), query); err != nil { return nil, err } @@ -159,17 +175,9 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, return nil, ValidationError{Reason: reason} } - dsName := "" - if panelQuery.Get("datasource").MustString() != "" { - dsName = panelQuery.Get("datasource").MustString() - } else if panel.Get("datasource").MustString() != "" { - dsName = panel.Get("datasource").MustString() - } - - datasource, err := e.lookupDatasourceID(dsName) + datasource, err := e.lookupQueryDataSource(panel, panelQuery) if err != nil { - e.log.Debug("Error looking up datasource", "error", err) - return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)} + return nil, err } dsFilterQuery := models.DatasourcesPermissionFilterQuery{ diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index bd201049ac8..fc63e30dc90 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -18,10 +18,10 @@ func TestAlertRuleExtraction(t *testing.T) { }) // mock data - defaultDs := &models.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true} - graphite2Ds := &models.DataSource{Id: 15, OrgId: 1, Name: "graphite2"} - influxDBDs := &models.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"} - prom := &models.DataSource{Id: 17, OrgId: 1, Name: "Prometheus"} + defaultDs := &models.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true, Uid: "def-uid"} + graphite2Ds := &models.DataSource{Id: 15, OrgId: 1, Name: "graphite2", Uid: "graphite2-uid"} + influxDBDs := &models.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB", Uid: "InfluxDB-uid"} + prom := &models.DataSource{Id: 17, OrgId: 1, Name: "Prometheus", Uid: "Prometheus-uid"} bus.AddHandler("test", func(query *models.GetDefaultDataSourceQuery) error { query.Result = defaultDs @@ -29,16 +29,16 @@ func TestAlertRuleExtraction(t *testing.T) { }) bus.AddHandler("test", func(query *models.GetDataSourceQuery) error { - if query.Name == defaultDs.Name { + if query.Name == defaultDs.Name || query.Uid == defaultDs.Uid { query.Result = defaultDs } - if query.Name == graphite2Ds.Name { + if query.Name == graphite2Ds.Name || query.Uid == graphite2Ds.Uid { query.Result = graphite2Ds } - if query.Name == influxDBDs.Name { + if query.Name == influxDBDs.Name || query.Uid == influxDBDs.Uid { query.Result = influxDBDs } - if query.Name == prom.Name { + if query.Name == prom.Name || query.Uid == prom.Uid { query.Result = prom } @@ -246,4 +246,25 @@ func TestAlertRuleExtraction(t *testing.T) { _, err = extractor.GetAlerts() require.Equal(t, err.Error(), "alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1") }) + + t.Run("Extract data source given new DataSourceRef object model", func(t *testing.T) { + json, err := ioutil.ReadFile("./testdata/panel-with-datasource-ref.json") + require.Nil(t, err) + + dashJSON, err := simplejson.NewJson(json) + require.Nil(t, err) + dash := models.NewDashboardFromJson(dashJSON) + extractor := NewDashAlertExtractor(dash, 1, nil) + + err = extractor.ValidateAlerts() + + require.Nil(t, err) + + alerts, err := extractor.GetAlerts() + require.Nil(t, err) + + condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0]) + query := condition.Get("query") + require.EqualValues(t, 15, query.Get("datasourceId").MustInt64()) + }) } diff --git a/pkg/services/alerting/testdata/panel-with-datasource-ref.json b/pkg/services/alerting/testdata/panel-with-datasource-ref.json new file mode 100644 index 00000000000..225f60b0fa0 --- /dev/null +++ b/pkg/services/alerting/testdata/panel-with-datasource-ref.json @@ -0,0 +1,38 @@ +{ + "id": 57, + "title": "Graphite 4", + "originalTitle": "Graphite 4", + "tags": ["graphite"], + "panels": [ + { + "title": "Active desktop users", + "id": 2, + "editable": true, + "type": "graph", + "targets": [ + { + "refId": "A", + "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)" + } + ], + "datasource": { + "uid": "graphite2-uid", + "type": "graphite" + }, + "alert": { + "name": "name1", + "message": "desc1", + "handler": 1, + "frequency": "60s", + "conditions": [ + { + "type": "query", + "query": { "params": ["A", "5m", "now"] }, + "reducer": { "type": "avg", "params": [] }, + "evaluator": { "type": ">", "params": [100] } + } + ] + } + } + ] +} diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 280ae6c0b09..c9dcadbca6e 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -198,7 +198,9 @@ describe('getExploreUrl', () => { }, datasourceSrv: { get() { - return {}; + return { + getRef: jest.fn(), + }; }, getDataSourceById: jest.fn(), }, @@ -239,7 +241,7 @@ describe('hasNonEmptyQuery', () => { }); test('should return false if query is empty', () => { - expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'panel', datasource: 'some-ds' }])).toBeFalsy(); + expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'panel', datasource: { uid: 'some-ds' } }])).toBeFalsy(); }); test('should return false if no queries exist', () => { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 11f8c522f18..2d360c8d749 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -99,7 +99,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise ({ ...t, datasource: exploreDatasource.name })), + queries: exploreTargets.map((t) => ({ ...t, datasource: exploreDatasource.getRef() })), }; } diff --git a/public/app/core/utils/query.ts b/public/app/core/utils/query.ts index f515cc3da14..e02fb0bea03 100644 --- a/public/app/core/utils/query.ts +++ b/public/app/core/utils/query.ts @@ -1,4 +1,4 @@ -import { DataQuery, DataSourceInstanceSettings } from '@grafana/data'; +import { DataQuery, DataSourceInstanceSettings, DataSourceRef, getDataSourceRef } from '@grafana/data'; export const getNextRefIdChar = (queries: DataQuery[]): string => { for (let num = 0; ; num++) { @@ -9,7 +9,7 @@ export const getNextRefIdChar = (queries: DataQuery[]): string => { } }; -export function addQuery(queries: DataQuery[], query?: Partial, datasource?: string): DataQuery[] { +export function addQuery(queries: DataQuery[], query?: Partial, datasource?: DataSourceRef): DataQuery[] { const q = query || {}; q.refId = getNextRefIdChar(queries); q.hide = false; @@ -27,16 +27,18 @@ export function updateQueries( extensionID: string, // pass this in because importing it creates a circular dependency dsSettings?: DataSourceInstanceSettings ): DataQuery[] { + const datasource = getDataSourceRef(newSettings); + if (!newSettings.meta.mixed && dsSettings?.meta.mixed) { return queries.map((q) => { if (q.datasource !== extensionID) { - q.datasource = newSettings.name; + q.datasource = datasource; } return q; }); } else if (!newSettings.meta.mixed && dsSettings?.meta.id !== newSettings.meta.id) { // we are changing data source type, clear queries - return [{ refId: 'A', datasource: newSettings.name }]; + return [{ refId: 'A', datasource }]; } return queries; diff --git a/public/app/features/alerting/getAlertingValidationMessage.test.ts b/public/app/features/alerting/getAlertingValidationMessage.test.ts index 218f364dea6..6deb2188b10 100644 --- a/public/app/features/alerting/getAlertingValidationMessage.test.ts +++ b/public/app/features/alerting/getAlertingValidationMessage.test.ts @@ -1,5 +1,11 @@ import { DataSourceSrv } from '@grafana/runtime'; -import { DataSourceApi, PluginMeta, DataTransformerConfig, DataSourceInstanceSettings } from '@grafana/data'; +import { + DataSourceApi, + PluginMeta, + DataTransformerConfig, + DataSourceInstanceSettings, + DataSourceRef, +} from '@grafana/data'; import { ElasticsearchQuery } from '../../plugins/datasource/elasticsearch/types'; import { getAlertingValidationMessage } from './getAlertingValidationMessage'; @@ -18,10 +24,13 @@ describe('getAlertingValidationMessage', () => { return false; }, name: 'some name', + uid: 'some uid', } as any) as DataSourceApi; const getMock = jest.fn().mockResolvedValue(datasource); const datasourceSrv: DataSourceSrv = { - get: getMock, + get: (ref: DataSourceRef) => { + return getMock(ref.uid); + }, getList(): DataSourceInstanceSettings[] { return []; }, @@ -33,11 +42,13 @@ describe('getAlertingValidationMessage', () => { ]; const transformations: DataTransformerConfig[] = []; - const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name); + const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, { + uid: datasource.uid, + }); expect(result).toBe(''); expect(getMock).toHaveBeenCalledTimes(2); - expect(getMock).toHaveBeenCalledWith(datasource.name); + expect(getMock).toHaveBeenCalledWith(datasource.uid); }); }); @@ -73,7 +84,9 @@ describe('getAlertingValidationMessage', () => { ]; const transformations: DataTransformerConfig[] = []; - const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name); + const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, { + uid: datasource.name, + }); expect(result).toBe(''); }); @@ -88,7 +101,9 @@ describe('getAlertingValidationMessage', () => { } as any) as DataSourceApi; const getMock = jest.fn().mockResolvedValue(datasource); const datasourceSrv: DataSourceSrv = { - get: getMock, + get: (ref: DataSourceRef) => { + return getMock(ref.uid); + }, getInstanceSettings: (() => {}) as any, getList(): DataSourceInstanceSettings[] { return []; @@ -100,7 +115,9 @@ describe('getAlertingValidationMessage', () => { ]; const transformations: DataTransformerConfig[] = []; - const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name); + const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, { + uid: datasource.name, + }); expect(result).toBe('Template variables are not supported in alert queries'); expect(getMock).toHaveBeenCalledTimes(2); @@ -114,10 +131,13 @@ describe('getAlertingValidationMessage', () => { meta: ({ alerting: false } as any) as PluginMeta, targetContainsTemplate: () => false, name: 'some name', + uid: 'theid', } as any) as DataSourceApi; const getMock = jest.fn().mockResolvedValue(datasource); const datasourceSrv: DataSourceSrv = { - get: getMock, + get: (ref: DataSourceRef) => { + return getMock(ref.uid); + }, getInstanceSettings: (() => {}) as any, getList(): DataSourceInstanceSettings[] { return []; @@ -129,11 +149,13 @@ describe('getAlertingValidationMessage', () => { ]; const transformations: DataTransformerConfig[] = []; - const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name); + const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, { + uid: datasource.uid, + }); expect(result).toBe('The datasource does not support alerting queries'); expect(getMock).toHaveBeenCalledTimes(2); - expect(getMock).toHaveBeenCalledWith(datasource.name); + expect(getMock).toHaveBeenCalledWith(datasource.uid); }); }); @@ -146,7 +168,9 @@ describe('getAlertingValidationMessage', () => { } as any) as DataSourceApi; const getMock = jest.fn().mockResolvedValue(datasource); const datasourceSrv: DataSourceSrv = { - get: getMock, + get: (ref: DataSourceRef) => { + return getMock(ref.uid); + }, getInstanceSettings: (() => {}) as any, getList(): DataSourceInstanceSettings[] { return []; @@ -158,7 +182,9 @@ describe('getAlertingValidationMessage', () => { ]; const transformations: DataTransformerConfig[] = [{ id: 'A', options: null }]; - const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name); + const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, { + uid: datasource.uid, + }); expect(result).toBe('Transformations are not supported in alert queries'); expect(getMock).toHaveBeenCalledTimes(0); diff --git a/public/app/features/alerting/getAlertingValidationMessage.ts b/public/app/features/alerting/getAlertingValidationMessage.ts index 5b782094a2b..52184d0d1df 100644 --- a/public/app/features/alerting/getAlertingValidationMessage.ts +++ b/public/app/features/alerting/getAlertingValidationMessage.ts @@ -1,4 +1,4 @@ -import { DataQuery, DataTransformerConfig } from '@grafana/data'; +import { DataQuery, DataSourceRef, DataTransformerConfig } from '@grafana/data'; import { DataSourceSrv } from '@grafana/runtime'; export const getDefaultCondition = () => ({ @@ -13,7 +13,7 @@ export const getAlertingValidationMessage = async ( transformations: DataTransformerConfig[] | undefined, targets: DataQuery[], datasourceSrv: DataSourceSrv, - datasourceName: string | null + datasource: DataSourceRef | null ): Promise => { if (targets.length === 0) { return 'Could not find any metric queries'; @@ -27,8 +27,8 @@ export const getAlertingValidationMessage = async ( let templateVariablesNotSupported = 0; for (const target of targets) { - const dsName = target.datasource || datasourceName; - const ds = await datasourceSrv.get(dsName); + const dsRef = target.datasource || datasource; + const ds = await datasourceSrv.get(dsRef); if (!ds.meta.alerting) { alertingNotSupported++; } else if (ds.targetContainsTemplate && ds.targetContainsTemplate(target)) { diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx index 7b99964a5c9..f76a4f86fe7 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx @@ -148,7 +148,10 @@ const dashboard = { } as DashboardModel; const panel = ({ - datasource: dataSources.prometheus.uid, + datasource: { + type: 'prometheus', + uid: dataSources.prometheus.uid, + }, title: 'mypanel', id: 34, targets: [ @@ -169,10 +172,10 @@ describe('PanelAlertTabContent', () => { jest.resetAllMocks(); mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); const dsService = new MockDataSourceSrv(dataSources); - dsService.datasources[dataSources.prometheus.name] = new PrometheusDatasource( + dsService.datasources[dataSources.prometheus.uid] = new PrometheusDatasource( dataSources.prometheus ) as DataSourceApi; - dsService.datasources[dataSources.default.name] = new PrometheusDatasource(dataSources.default) as DataSourceApi< + dsService.datasources[dataSources.default.uid] = new PrometheusDatasource(dataSources.default) as DataSourceApi< any, any >; @@ -185,15 +188,20 @@ describe('PanelAlertTabContent', () => { maxDataPoints: 100, interval: '10s', } as any) as PanelModel); + const button = await ui.createButton.find(); const href = button.href; const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/); expect(match).toHaveLength(2); + const defaults = JSON.parse(decodeURIComponent(match![1])); expect(defaults.queries[0].model).toEqual({ expr: 'sum(some_metric [5m])) by (app)', refId: 'A', - datasource: 'Prometheus', + datasource: { + type: 'prometheus', + uid: 'mock-ds-2', + }, interval: '', intervalMs: 300000, maxDataPoints: 100, @@ -207,15 +215,20 @@ describe('PanelAlertTabContent', () => { maxDataPoints: 100, interval: '10s', } as any) as PanelModel); + const button = await ui.createButton.find(); const href = button.href; const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/); expect(match).toHaveLength(2); + const defaults = JSON.parse(decodeURIComponent(match![1])); expect(defaults.queries[0].model).toEqual({ expr: 'sum(some_metric [5m])) by (app)', refId: 'A', - datasource: 'Default', + datasource: { + type: 'prometheus', + uid: 'mock-ds-3', + }, interval: '', intervalMs: 300000, maxDataPoints: 100, @@ -223,21 +236,26 @@ describe('PanelAlertTabContent', () => { }); it('Will take into account datasource minInterval', async () => { - ((getDatasourceSrv() as any) as MockDataSourceSrv).datasources[dataSources.prometheus.name].interval = '7m'; + ((getDatasourceSrv() as any) as MockDataSourceSrv).datasources[dataSources.prometheus.uid].interval = '7m'; await renderAlertTabContent(dashboard, ({ ...panel, maxDataPoints: 100, } as any) as PanelModel); + const button = await ui.createButton.find(); const href = button.href; const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/); expect(match).toHaveLength(2); + const defaults = JSON.parse(decodeURIComponent(match![1])); expect(defaults.queries[0].model).toEqual({ expr: 'sum(some_metric [7m])) by (app)', refId: 'A', - datasource: 'Prometheus', + datasource: { + type: 'prometheus', + uid: 'mock-ds-2', + }, interval: '', intervalMs: 420000, maxDataPoints: 100, @@ -254,10 +272,12 @@ describe('PanelAlertTabContent', () => { expect(rows).toHaveLength(1); expect(rows[0]).toHaveTextContent(/dashboardrule1/); expect(rows[0]).not.toHaveTextContent(/dashboardrule2/); + const button = await ui.createButton.find(); const href = button.href; const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/); expect(match).toHaveLength(2); + const defaults = JSON.parse(decodeURIComponent(match![1])); expect(defaults).toEqual({ type: 'grafana', @@ -271,7 +291,10 @@ describe('PanelAlertTabContent', () => { model: { expr: 'sum(some_metric [15s])) by (app)', refId: 'A', - datasource: 'Prometheus', + datasource: { + type: 'prometheus', + uid: 'mock-ds-2', + }, interval: '', intervalMs: 15000, }, @@ -284,7 +307,10 @@ describe('PanelAlertTabContent', () => { refId: 'B', hide: false, type: 'classic_conditions', - datasource: '__expr__', + datasource: { + type: 'grafana-expression', + uid: '-100', + }, conditions: [ { type: 'query', diff --git a/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx b/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx index 75cc88d0fd4..030b500ccab 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx @@ -1,6 +1,6 @@ import { SelectableValue } from '@grafana/data'; import { Field, InputControl, Select } from '@grafana/ui'; -import { ExpressionDatasourceID } from 'app/features/expressions/ExpressionDatasource'; +import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource'; import React, { FC, useEffect, useMemo } from 'react'; import { useFormContext } from 'react-hook-form'; import { RuleFormValues } from '../../types/rule-form'; @@ -28,7 +28,7 @@ export const ConditionField: FC = () => { // reset condition if option no longer exists or if it is unset, but there are options available useEffect(() => { - const expressions = queries.filter((query) => query.model.datasource === ExpressionDatasourceID); + const expressions = queries.filter((query) => query.datasourceUid === ExpressionDatasourceUID); if (condition && !options.find(({ value }) => value === condition)) { setValue('condition', expressions.length ? expressions[expressions.length - 1].refId : null); } else if (!condition && expressions.length) { diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryEditor.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryEditor.tsx index 7bbfd8c1bea..e94779bf16b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryEditor.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryEditor.tsx @@ -85,7 +85,10 @@ export class QueryEditor extends PureComponent { datasourceUid: defaultDataSource.uid, model: { refId: '', - datasource: defaultDataSource.name, + datasource: { + type: defaultDataSource.type, + uid: defaultDataSource.uid, + }, }, }) ); diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 51dac27fde1..79033432099 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -3,6 +3,7 @@ import { DataSourceInstanceSettings, DataSourceJsonData, DataSourcePluginMeta, + DataSourceRef, ScopedVars, } from '@grafana/data'; import { @@ -227,6 +228,7 @@ export const mockSilence = (partial: Partial = {}): Silence => { ...partial, }; }; + export class MockDataSourceSrv implements DataSourceSrv { datasources: Record = {}; // @ts-ignore @@ -238,6 +240,7 @@ export class MockDataSourceSrv implements DataSourceSrv { getVariables: () => [], replace: (name: any) => name, }; + defaultName = ''; constructor(datasources: Record) { @@ -249,6 +252,7 @@ export class MockDataSourceSrv implements DataSourceSrv { }, {} ); + for (const dsSettings of Object.values(this.settingsMapByName)) { this.settingsMapByUid[dsSettings.uid] = dsSettings; this.settingsMapById[dsSettings.id] = dsSettings; @@ -258,7 +262,7 @@ export class MockDataSourceSrv implements DataSourceSrv { } } - get(name?: string | null, scopedVars?: ScopedVars): Promise { + get(name?: string | null | DataSourceRef, scopedVars?: ScopedVars): Promise { return DatasourceSrv.prototype.get.call(this, name, scopedVars); //return Promise.reject(new Error('not implemented')); } diff --git a/public/app/features/alerting/unified/utils/query.test.ts b/public/app/features/alerting/unified/utils/query.test.ts index 1bd05f8c74a..185efb26870 100644 --- a/public/app/features/alerting/unified/utils/query.test.ts +++ b/public/app/features/alerting/unified/utils/query.test.ts @@ -3,6 +3,7 @@ import { alertRuleToQueries } from './query'; import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; import { CombinedRule } from 'app/types/unified-alerting'; import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto'; +import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource'; describe('alertRuleToQueries', () => { it('it should convert grafana alert', () => { @@ -110,7 +111,9 @@ const grafanaAlert = { type: 'query', }, ], - datasource: '__expr__', + datasource: { + uid: ExpressionDatasourceUID, + }, hide: false, refId: 'B', type: 'classic_conditions', diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 7bce2decc79..de408a6b372 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -6,12 +6,13 @@ import { getDefaultRelativeTimeRange, TimeRange, IntervalValues, + DataSourceRef, } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { getNextRefIdChar } from 'app/core/utils/query'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; -import { ExpressionDatasourceID, ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource'; +import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource'; import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; import { @@ -189,7 +190,10 @@ const getDefaultExpression = (refId: string): AlertQuery => { refId, hide: false, type: ExpressionQueryType.classic, - datasource: ExpressionDatasourceID, + datasource: { + uid: ExpressionDatasourceUID, + type: 'grafana-expression', + }, conditions: [ { type: 'query', @@ -223,14 +227,15 @@ const dataQueriesToGrafanaQueries = async ( queries: DataQuery[], relativeTimeRange: RelativeTimeRange, scopedVars: ScopedVars | {}, - datasourceName?: string, + panelDataSourceRef?: DataSourceRef, maxDataPoints?: number, minInterval?: string ): Promise => { const result: AlertQuery[] = []; + for (const target of queries) { - const datasource = await getDataSourceSrv().get(target.datasource || datasourceName); - const dsName = datasource.name; + const datasource = await getDataSourceSrv().get(target.datasource?.uid ? target.datasource : panelDataSourceRef); + const dsRef = { uid: datasource.uid, type: datasource.type }; const range = rangeUtil.relativeToTimeRange(relativeTimeRange); const { interval, intervalMs } = getIntervals(range, minInterval ?? datasource.interval, maxDataPoints); @@ -239,37 +244,37 @@ const dataQueriesToGrafanaQueries = async ( __interval_ms: { text: intervalMs, value: intervalMs }, ...scopedVars, }; + const interpolatedTarget = datasource.interpolateVariablesInQueries ? await datasource.interpolateVariablesInQueries([target], queryVariables)[0] : target; - if (dsName) { - // expressions - if (dsName === ExpressionDatasourceID) { + + // expressions + if (dsRef.uid === ExpressionDatasourceUID) { + const newQuery: AlertQuery = { + refId: interpolatedTarget.refId, + queryType: '', + relativeTimeRange, + datasourceUid: ExpressionDatasourceUID, + model: interpolatedTarget, + }; + result.push(newQuery); + // queries + } else { + const datasourceSettings = getDataSourceSrv().getInstanceSettings(dsRef); + if (datasourceSettings && datasourceSettings.meta.alerting) { const newQuery: AlertQuery = { refId: interpolatedTarget.refId, - queryType: '', + queryType: interpolatedTarget.queryType ?? '', relativeTimeRange, - datasourceUid: ExpressionDatasourceUID, - model: interpolatedTarget, + datasourceUid: datasourceSettings.uid, + model: { + ...interpolatedTarget, + maxDataPoints, + intervalMs, + }, }; result.push(newQuery); - // queries - } else { - const datasourceSettings = getDataSourceSrv().getInstanceSettings(dsName); - if (datasourceSettings && datasourceSettings.meta.alerting) { - const newQuery: AlertQuery = { - refId: interpolatedTarget.refId, - queryType: interpolatedTarget.queryType ?? '', - relativeTimeRange, - datasourceUid: datasourceSettings.uid, - model: { - ...interpolatedTarget, - maxDataPoints, - intervalMs, - }, - }; - result.push(newQuery); - } } } } diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts index 4fd6ff27e72..b1f5307bec8 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts @@ -2,17 +2,13 @@ import { find } from 'lodash'; import config from 'app/core/config'; import { DashboardExporter, LibraryElementExport } from './DashboardExporter'; import { DashboardModel } from '../../state/DashboardModel'; -import { PanelPluginMeta } from '@grafana/data'; +import { DataSourceInstanceSettings, DataSourceRef, PanelPluginMeta } from '@grafana/data'; import { variableAdapters } from '../../../variables/adapters'; import { createConstantVariableAdapter } from '../../../variables/constant/adapter'; import { createQueryVariableAdapter } from '../../../variables/query/adapter'; import { createDataSourceVariableAdapter } from '../../../variables/datasource/adapter'; import { LibraryElementKind } from '../../../library-panels/types'; -function getStub(arg: string) { - return Promise.resolve(stubs[arg || 'gfdb']); -} - jest.mock('app/core/store', () => { return { getBool: jest.fn(), @@ -22,9 +18,16 @@ jest.mock('app/core/store', () => { jest.mock('@grafana/runtime', () => ({ ...((jest.requireActual('@grafana/runtime') as unknown) as object), - getDataSourceSrv: () => ({ - get: jest.fn((arg) => getStub(arg)), - }), + getDataSourceSrv: () => { + return { + get: (v: any) => { + const s = getStubInstanceSettings(v); + // console.log('GET', v, s); + return Promise.resolve(s); + }, + getInstanceSettings: getStubInstanceSettings, + }; + }, config: { buildInfo: {}, panels: {}, @@ -48,7 +51,7 @@ describe('given dashboard with repeated panels', () => { { name: 'apps', type: 'query', - datasource: 'gfdb', + datasource: { uid: 'gfdb', type: 'testdb' }, current: { value: 'Asd', text: 'Asd' }, options: [{ value: 'Asd', text: 'Asd' }], }, @@ -72,22 +75,22 @@ describe('given dashboard with repeated panels', () => { list: [ { name: 'logs', - datasource: 'gfdb', + datasource: { uid: 'gfdb', type: 'testdb' }, }, ], }, panels: [ - { id: 6, datasource: 'gfdb', type: 'graph' }, + { id: 6, datasource: { uid: 'gfdb', type: 'testdb' }, type: 'graph' }, { id: 7 }, { id: 8, - datasource: '-- Mixed --', - targets: [{ datasource: 'other' }], + datasource: { uid: '-- Mixed --', type: 'mixed' }, + targets: [{ datasource: { uid: 'other', type: 'other' } }], }, - { id: 9, datasource: '$ds' }, + { id: 9, datasource: { uid: '$ds', type: 'other2' } }, { id: 17, - datasource: '$ds', + datasource: { uid: '$ds', type: 'other2' }, type: 'graph', libraryPanel: { name: 'Library Panel 2', @@ -97,7 +100,7 @@ describe('given dashboard with repeated panels', () => { { id: 2, repeat: 'apps', - datasource: 'gfdb', + datasource: { uid: 'gfdb', type: 'testdb' }, type: 'graph', }, { id: 3, repeat: null, repeatPanelId: 2 }, @@ -105,24 +108,24 @@ describe('given dashboard with repeated panels', () => { id: 4, collapsed: true, panels: [ - { id: 10, datasource: 'gfdb', type: 'table' }, + { id: 10, datasource: { uid: 'gfdb', type: 'testdb' }, type: 'table' }, { id: 11 }, { id: 12, - datasource: '-- Mixed --', - targets: [{ datasource: 'other' }], + datasource: { uid: '-- Mixed --', type: 'mixed' }, + targets: [{ datasource: { uid: 'other', type: 'other' } }], }, - { id: 13, datasource: '$ds' }, + { id: 13, datasource: { uid: '$uid', type: 'other' } }, { id: 14, repeat: 'apps', - datasource: 'gfdb', + datasource: { uid: 'gfdb', type: 'testdb' }, type: 'heatmap', }, { id: 15, repeat: null, repeatPanelId: 14 }, { id: 16, - datasource: 'gfdb', + datasource: { uid: 'gfdb', type: 'testdb' }, type: 'graph', libraryPanel: { name: 'Library Panel', @@ -264,7 +267,7 @@ describe('given dashboard with repeated panels', () => { expect(element.kind).toBe(LibraryElementKind.Panel); expect(element.model).toEqual({ id: 17, - datasource: '$ds', + datasource: '${DS_OTHER2}', type: 'graph', fieldConfig: { defaults: {}, @@ -287,6 +290,11 @@ describe('given dashboard with repeated panels', () => { }); }); +function getStubInstanceSettings(v: string | DataSourceRef): DataSourceInstanceSettings { + let key = (v as DataSourceRef)?.type ?? v; + return (stubs[(key as any) ?? 'gfdb'] ?? stubs['gfdb']) as any; +} + // Stub responses const stubs: { [key: string]: {} } = {}; stubs['gfdb'] = { diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts index 5ec69704fcf..9cae57c6d32 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts @@ -75,10 +75,13 @@ export class DashboardExporter { let datasourceVariable: any = null; // ignore data source properties that contain a variable - if (datasource && datasource.indexOf('$') === 0) { - datasourceVariable = variableLookup[datasource.substring(1)]; - if (datasourceVariable && datasourceVariable.current) { - datasource = datasourceVariable.current.value; + if (datasource && (datasource as any).uid) { + const uid = (datasource as any).uid as string; + if (uid.indexOf('$') === 0) { + datasourceVariable = variableLookup[uid.substring(1)]; + if (datasourceVariable && datasourceVariable.current) { + datasource = datasourceVariable.current.value; + } } } diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx index 8fd1724bc3f..7b7e684df57 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { QueryGroup } from 'app/features/query/components/QueryGroup'; import { PanelModel } from '../../state'; import { getLocationSrv } from '@grafana/runtime'; -import { QueryGroupOptions } from 'app/types'; +import { QueryGroupDataSource, QueryGroupOptions } from 'app/types'; import { DataQuery } from '@grafana/data'; interface Props { @@ -18,10 +18,17 @@ export class PanelEditorQueries extends PureComponent { } buildQueryOptions(panel: PanelModel): QueryGroupOptions { + const dataSource: QueryGroupDataSource = panel.datasource?.uid + ? { + default: false, + ...panel.datasource, + } + : { + default: true, + }; + return { - dataSource: { - name: panel.datasource, - }, + dataSource, queries: panel.targets, maxDataPoints: panel.maxDataPoints, minInterval: panel.interval, @@ -47,8 +54,8 @@ export class PanelEditorQueries extends PureComponent { onOptionsChange = (options: QueryGroupOptions) => { const { panel } = this.props; - const newDataSourceName = options.dataSource.default ? null : options.dataSource.name!; - const dataSourceChanged = newDataSourceName !== panel.datasource; + const newDataSourceID = options.dataSource.default ? null : options.dataSource.uid!; + const dataSourceChanged = newDataSourceID !== panel.datasource?.uid; panel.updateQueries(options); if (dataSourceChanged) { diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index 2d717941511..5846b83e847 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -162,7 +162,7 @@ describe('DashboardModel', () => { }); it('dashboard schema version should be set to latest', () => { - expect(model.schemaVersion).toBe(32); + expect(model.schemaVersion).toBe(33); }); it('graph thresholds should be migrated', () => { diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index e7eb25a474c..718124d6b8b 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -9,6 +9,7 @@ import { DashboardModel } from './DashboardModel'; import { DataLink, DataLinkBuiltInVars, + DataSourceRef, MappingType, SpecialValueMatch, PanelPlugin, @@ -39,6 +40,8 @@ import { config } from 'app/core/config'; import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module'; import { plugin as gaugePanelPlugin } from 'app/plugins/panel/gauge/module'; import { getStandardFieldConfigs, getStandardOptionEditors } from '@grafana/ui'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; +import { getDataSourceSrv } from '@grafana/runtime'; import { labelsToFieldsTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/labelsToFields'; import { mergeTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/merge'; import { @@ -62,7 +65,7 @@ export class DashboardMigrator { let i, j, k, n; const oldVersion = this.dashboard.schemaVersion; const panelUpgrades: PanelSchemeUpgradeHandler[] = []; - this.dashboard.schemaVersion = 32; + this.dashboard.schemaVersion = 33; if (oldVersion === this.dashboard.schemaVersion) { return; @@ -695,6 +698,45 @@ export class DashboardMigrator { this.migrateCloudWatchAnnotationQuery(); } + // Replace datasource name with reference, uid and type + if (oldVersion < 33) { + for (const variable of this.dashboard.templating.list) { + if (variable.type !== 'query') { + continue; + } + let name = (variable as any).datasource as string; + if (name) { + variable.datasource = migrateDatasourceNameToRef(name); + } + } + + // Mutate panel models + for (const panel of this.dashboard.panels) { + let name = (panel as any).datasource as string; + if (!name) { + panel.datasource = null; // use default + } else if (name === MIXED_DATASOURCE_NAME) { + panel.datasource = { type: MIXED_DATASOURCE_NAME }; + for (const target of panel.targets) { + name = (target as any).datasource as string; + panel.datasource = migrateDatasourceNameToRef(name); + } + continue; // do not cleanup targets + } else { + panel.datasource = migrateDatasourceNameToRef(name); + } + + // cleanup query datasource references + if (!panel.targets) { + panel.targets = []; + } else { + for (const target of panel.targets) { + delete target.datasource; + } + } + } + } + if (panelUpgrades.length === 0) { return; } @@ -1009,6 +1051,19 @@ function migrateSinglestat(panel: PanelModel) { return panel; } +export function migrateDatasourceNameToRef(name: string): DataSourceRef | null { + if (!name || name === 'default') { + return null; + } + + const ds = getDataSourceSrv().getInstanceSettings(name); + if (!ds) { + return { uid: name }; // not found + } + + return { type: ds.meta.id, uid: ds.uid }; +} + // mutates transformations appending a new transformer after the existing one function appendTransformerAfter(panel: PanelModel, id: string, cfg: DataTransformerConfig) { if (panel.transformations) { diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 91660da7b94..a23607a96a6 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -19,7 +19,7 @@ import { ScopedVars, urlUtil, PanelModel as IPanelModel, - DatasourceRef, + DataSourceRef, } from '@grafana/data'; import config from 'app/core/config'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; @@ -144,7 +144,7 @@ export class PanelModel implements DataConfigSource, IPanelModel { panels?: any; declare targets: DataQuery[]; transformations?: DataTransformerConfig[]; - datasource: DatasourceRef | null = null; + datasource: DataSourceRef | null = null; thresholds?: any; pluginVersion?: string; @@ -442,7 +442,13 @@ export class PanelModel implements DataConfigSource, IPanelModel { } updateQueries(options: QueryGroupOptions) { - this.datasource = options.dataSource.default ? null : options.dataSource.name!; + const { dataSource } = options; + this.datasource = dataSource.default + ? null + : { + uid: dataSource.uid, + type: dataSource.type, + }; this.timeFrom = options.timeRange?.from; this.timeShift = options.timeRange?.shift; this.hideTimeOverride = options.timeRange?.hide; diff --git a/public/app/features/explore/QueryRows.test.tsx b/public/app/features/explore/QueryRows.test.tsx index 2512da30882..e9e16546c8c 100644 --- a/public/app/features/explore/QueryRows.test.tsx +++ b/public/app/features/explore/QueryRows.test.tsx @@ -12,13 +12,15 @@ import { DataQuery } from '../../../../packages/grafana-data/src'; function setup(queries: DataQuery[]) { const defaultDs = { name: 'newDs', + uid: 'newDs-uid', meta: { id: 'newDs' }, }; const datasources: Record = { - newDs: defaultDs, - someDs: { + 'newDs-uid': defaultDs, + 'someDs-uid': { name: 'someDs', + uid: 'someDs-uid', meta: { id: 'someDs' }, components: { QueryEditor: () => 'someDs query editor', @@ -30,11 +32,11 @@ function setup(queries: DataQuery[]) { getList() { return Object.values(datasources).map((d) => ({ name: d.name })); }, - getInstanceSettings(name: string) { - return datasources[name] || defaultDs; + getInstanceSettings(uid: string) { + return datasources[uid] || defaultDs; }, - get(name?: string) { - return Promise.resolve(name ? datasources[name] || defaultDs : defaultDs); + get(uid?: string) { + return Promise.resolve(uid ? datasources[uid] || defaultDs : defaultDs); }, } as any); @@ -42,7 +44,7 @@ function setup(queries: DataQuery[]) { const initialState: ExploreState = { left: { ...leftState, - datasourceInstance: datasources.someDs, + datasourceInstance: datasources['someDs-uid'], queries, }, syncedTimes: false, diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 86bb0c4029b..82cec7ff84e 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -22,7 +22,7 @@ const makeSelectors = (exploreId: ExploreId) => { getEventBridge: createSelector(exploreItemSelector, (s) => s!.eventBridge), getDatasourceInstanceSettings: createSelector( exploreItemSelector, - (s) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.name)! + (s) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.uid)! ), }; }; diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/features/explore/Wrapper.test.tsx index 0ffbda113ee..171ee041ea0 100644 --- a/public/app/features/explore/Wrapper.test.tsx +++ b/public/app/features/explore/Wrapper.test.tsx @@ -317,10 +317,12 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou return dsSettings.map((d) => d.settings); }, getInstanceSettings(name: string) { - return dsSettings.map((d) => d.settings).find((x) => x.name === name); + return dsSettings.map((d) => d.settings).find((x) => x.name === name || x.uid === name); }, get(name?: string | null, scopedVars?: ScopedVars): Promise { - return Promise.resolve((name ? dsSettings.find((d) => d.api.name === name) : dsSettings[0])!.api); + return Promise.resolve( + (name ? dsSettings.find((d) => d.api.name === name || d.api.uid === name) : dsSettings[0])!.api + ); }, } as any); @@ -392,7 +394,9 @@ function makeDatasourceSetup({ name = 'loki', id = 1 }: { name?: string; id?: nu }, }, name: name, + uid: name, query: jest.fn(), + getRef: jest.fn(), meta, } as any, }; diff --git a/public/app/features/explore/state/explorePane.test.ts b/public/app/features/explore/state/explorePane.test.ts index b65d2bc8020..8d997095489 100644 --- a/public/app/features/explore/state/explorePane.test.ts +++ b/public/app/features/explore/state/explorePane.test.ts @@ -62,6 +62,7 @@ function setup(state?: any) { query: jest.fn(), name: 'newDs', meta: { id: 'newDs' }, + getRef: () => ({ uid: 'newDs' }), }, someDs: { testDatasource: jest.fn(), @@ -69,6 +70,7 @@ function setup(state?: any) { query: jest.fn(), name: 'someDs', meta: { id: 'someDs' }, + getRef: () => ({ uid: 'someDs' }), }, }; @@ -77,7 +79,7 @@ function setup(state?: any) { return Object.values(datasources).map((d) => ({ name: d.name })); }, getInstanceSettings(name: string) { - return { name: 'hello' }; + return { name, getRef: () => ({ uid: name }) }; }, get(name?: string) { return Promise.resolve( diff --git a/public/app/features/explore/state/main.test.ts b/public/app/features/explore/state/main.test.ts index 8d621bc17dc..5f88d4f3df8 100644 --- a/public/app/features/explore/state/main.test.ts +++ b/public/app/features/explore/state/main.test.ts @@ -11,10 +11,10 @@ import { locationService } from '@grafana/runtime'; const getNavigateToExploreContext = async (openInNewWindow?: (url: string) => void) => { const url = '/explore'; const panel: Partial = { - datasource: 'mocked datasource', + datasource: { uid: 'mocked datasource' }, targets: [{ refId: 'A' }], }; - const datasource = new MockDataSourceApi(panel.datasource!); + const datasource = new MockDataSourceApi(panel.datasource!.uid!); const get = jest.fn().mockResolvedValue(datasource); const getDataSourceSrv = jest.fn().mockReturnValue({ get }); const getTimeSrv = jest.fn(); diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index c578e599dcb..aedd74d7759 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -64,6 +64,7 @@ const defaultInitialState = { [ExploreId.left]: { datasourceInstance: { query: jest.fn(), + getRef: jest.fn(), meta: { id: 'something', }, @@ -160,8 +161,8 @@ describe('importing queries', () => { importQueries( ExploreId.left, [ - { datasource: 'postgres1', refId: 'refId_A' }, - { datasource: 'postgres1', refId: 'refId_B' }, + { datasource: { type: 'postgresql' }, refId: 'refId_A' }, + { datasource: { type: 'postgresql' }, refId: 'refId_B' }, ], { name: 'Postgres1', type: 'postgres' } as DataSourceApi, { name: 'Postgres2', type: 'postgres' } as DataSourceApi @@ -342,6 +343,7 @@ describe('reducer', () => { ...defaultInitialState.explore[ExploreId.left], datasourceInstance: { query: jest.fn(), + getRef: jest.fn(), meta: { id: 'something', }, diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index 33ec8339099..55766745a36 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -342,7 +342,7 @@ export const runQueries = ( const queries = exploreItemState.queries.map((query) => ({ ...query, - datasource: query.datasource || datasourceInstance?.name, + datasource: query.datasource || datasourceInstance?.getRef(), })); const cachedValue = getResultsFromCache(cache, absoluteRange); diff --git a/public/app/features/expressions/ExpressionDatasource.ts b/public/app/features/expressions/ExpressionDatasource.ts index 82a988b2ce7..9d435b90ec6 100644 --- a/public/app/features/expressions/ExpressionDatasource.ts +++ b/public/app/features/expressions/ExpressionDatasource.ts @@ -7,7 +7,7 @@ import { DataSourceWithBackend } from '@grafana/runtime'; * This is a singleton instance that just pretends to be a DataSource */ export class ExpressionDatasourceApi extends DataSourceWithBackend { - constructor(instanceSettings: DataSourceInstanceSettings) { + constructor(public instanceSettings: DataSourceInstanceSettings) { super(instanceSettings); } @@ -19,7 +19,7 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend(); const [pattern, setPattern] = useState(); const [patternPrefix, setPatternPrefix] = useState(''); - const [datasource, setDatasource] = useState(); + const [datasource, setDatasource] = useState(); const onSubmit = () => { if (!pattern) { @@ -85,7 +85,7 @@ export function AddNewRule({ onRuleAdded }: Props) { { - setDatasource(ds.name); + setDatasource(ds); setPatternPrefix(`${LiveChannelScope.DataSource}/${ds.uid}/`); }} /> diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 560dbee7928..63626277670 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -9,7 +9,14 @@ import { TemplateSrv, } from '@grafana/runtime'; // Types -import { AppEvents, DataSourceApi, DataSourceInstanceSettings, DataSourceSelectItem, ScopedVars } from '@grafana/data'; +import { + AppEvents, + DataSourceApi, + DataSourceInstanceSettings, + DataSourceRef, + DataSourceSelectItem, + ScopedVars, +} from '@grafana/data'; import { auto } from 'angular'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; // Pretend Datasource @@ -23,11 +30,11 @@ import { DataSourceVariableModel } from '../variables/types'; import { cloneDeep } from 'lodash'; export class DatasourceSrv implements DataSourceService { - private datasources: Record = {}; + private datasources: Record = {}; // UID private settingsMapByName: Record = {}; private settingsMapByUid: Record = {}; private settingsMapById: Record = {}; - private defaultName = ''; + private defaultName = ''; // actually UID /** @ngInject */ constructor( @@ -43,22 +50,39 @@ export class DatasourceSrv implements DataSourceService { this.defaultName = defaultName; for (const dsSettings of Object.values(settingsMapByName)) { + if (!dsSettings.uid) { + dsSettings.uid = dsSettings.name; // -- Grafana --, -- Mixed etc + } + this.settingsMapByUid[dsSettings.uid] = dsSettings; this.settingsMapById[dsSettings.id] = dsSettings; } + + // Preload expressions + this.datasources[ExpressionDatasourceID] = expressionDatasource as any; + this.datasources[ExpressionDatasourceUID] = expressionDatasource as any; + this.settingsMapByUid[ExpressionDatasourceID] = expressionInstanceSettings; + this.settingsMapByUid[ExpressionDatasourceUID] = expressionInstanceSettings; } getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined { return this.settingsMapByUid[uid]; } - getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined { - if (nameOrUid === 'default' || nameOrUid === null || nameOrUid === undefined) { - return this.settingsMapByName[this.defaultName]; - } + getInstanceSettings(ref: string | null | undefined | DataSourceRef): DataSourceInstanceSettings | undefined { + const isstring = typeof ref === 'string'; + let nameOrUid = isstring ? (ref as string) : ((ref as any)?.uid as string | undefined); - if (nameOrUid === ExpressionDatasourceID || nameOrUid === ExpressionDatasourceUID) { - return expressionInstanceSettings; + if (nameOrUid === 'default' || nameOrUid === null || nameOrUid === undefined) { + if (!isstring && ref) { + const type = (ref as any)?.type as string; + if (type === ExpressionDatasourceID) { + return expressionDatasource.instanceSettings; + } else if (type) { + console.log('FIND Default instance for datasource type?', ref); + } + } + return this.settingsMapByUid[this.defaultName] ?? this.settingsMapByName[this.defaultName]; } // Complex logic to support template variable data source names @@ -89,15 +113,16 @@ export class DatasourceSrv implements DataSourceService { return this.settingsMapByUid[nameOrUid] ?? this.settingsMapByName[nameOrUid]; } - get(nameOrUid?: string | null, scopedVars?: ScopedVars): Promise { + get(ref?: string | DataSourceRef | null, scopedVars?: ScopedVars): Promise { + let nameOrUid = typeof ref === 'string' ? (ref as string) : ((ref as any)?.uid as string | undefined); if (!nameOrUid) { return this.get(this.defaultName); } // Check if nameOrUid matches a uid and then get the name - const byUid = this.settingsMapByUid[nameOrUid]; - if (byUid) { - nameOrUid = byUid.name; + const byName = this.settingsMapByName[nameOrUid]; + if (byName) { + nameOrUid = byName.uid; } // This check is duplicated below, this is here mainly as performance optimization to skip interpolation @@ -119,26 +144,22 @@ export class DatasourceSrv implements DataSourceService { return this.loadDatasource(nameOrUid); } - async loadDatasource(name: string): Promise> { - // Expression Datasource (not a real datasource) - if (name === ExpressionDatasourceID || name === ExpressionDatasourceUID) { - this.datasources[name] = expressionDatasource as any; - return Promise.resolve(expressionDatasource); + async loadDatasource(key: string): Promise> { + if (this.datasources[key]) { + return Promise.resolve(this.datasources[key]); } - let dsConfig = this.settingsMapByName[name]; + // find the metadata + const dsConfig = this.settingsMapByUid[key] ?? this.settingsMapByName[key] ?? this.settingsMapById[key]; if (!dsConfig) { - dsConfig = this.settingsMapById[name]; - if (!dsConfig) { - return Promise.reject({ message: `Datasource named ${name} was not found` }); - } + return Promise.reject({ message: `Datasource ${key} was not found` }); } try { const dsPlugin = await importDataSourcePlugin(dsConfig.meta); // check if its in cache now - if (this.datasources[name]) { - return this.datasources[name]; + if (this.datasources[key]) { + return this.datasources[key]; } // If there is only one constructor argument it is instanceSettings @@ -153,11 +174,14 @@ export class DatasourceSrv implements DataSourceService { instance.meta = dsConfig.meta; // store in instance cache - this.datasources[name] = instance; + this.datasources[key] = instance; + this.datasources[instance.uid] = instance; return instance; } catch (err) { - this.$rootScope.appEvent(AppEvents.alertError, [dsConfig.name + ' plugin failed', err.toString()]); - return Promise.reject({ message: `Datasource named ${name} was not found` }); + if (this.$rootScope) { + this.$rootScope.appEvent(AppEvents.alertError, [dsConfig.name + ' plugin failed', err.toString()]); + } + return Promise.reject({ message: `Datasource: ${key} was not found` }); } } diff --git a/public/app/features/plugins/specs/datasource_srv.test.ts b/public/app/features/plugins/specs/datasource_srv.test.ts index 63e19d13a43..53b8516b9bb 100644 --- a/public/app/features/plugins/specs/datasource_srv.test.ts +++ b/public/app/features/plugins/specs/datasource_srv.test.ts @@ -221,6 +221,7 @@ describe('datasource_srv', () => { }, "name": "-- Mixed --", "type": "test-db", + "uid": "-- Mixed --", }, Object { "meta": Object { @@ -230,6 +231,7 @@ describe('datasource_srv', () => { }, "name": "-- Dashboard --", "type": "dashboard", + "uid": "-- Dashboard --", }, Object { "meta": Object { @@ -239,6 +241,7 @@ describe('datasource_srv', () => { }, "name": "-- Grafana --", "type": "grafana", + "uid": "-- Grafana --", }, ] `); diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 768db3cea2e..739ac4596d2 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -121,7 +121,7 @@ export class QueryEditorRow extends PureComponent { return { getDataSourceSrv: () => ({ - getInstanceSettings: jest.fn(), - getList: jest.fn().mockReturnValue([]), + get: () => Promise.resolve(mockDS), + getList: () => [mockDS], + getInstanceSettings: () => mockDS, }), }; }); diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index 96c343f15f8..5786b0149e5 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -67,7 +67,7 @@ export class QueryEditorRows extends PureComponent { if (previous?.type === dataSource.type) { return { ...item, - datasource: dataSource.name, + datasource: { uid: dataSource.uid }, }; } } @@ -75,7 +75,7 @@ export class QueryEditorRows extends PureComponent { return { refId: item.refId, hide: item.hide, - datasource: dataSource.name, + datasource: { uid: dataSource.uid }, }; }) ); diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index 714c544a0bb..fc5d9f02997 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -22,6 +22,7 @@ import { DataQuery, DataSourceApi, DataSourceInstanceSettings, + DataSourceRef, getDefaultTimeRange, LoadingState, PanelData, @@ -91,7 +92,8 @@ export class QueryGroup extends PureComponent { const ds = await this.dataSourceSrv.get(options.dataSource.name); const dsSettings = this.dataSourceSrv.getInstanceSettings(options.dataSource.name); const defaultDataSource = await this.dataSourceSrv.get(); - const queries = options.queries.map((q) => (q.datasource ? q : { ...q, datasource: dsSettings?.name })); + const datasource: DataSourceRef = { type: ds.type, uid: ds.uid }; + const queries = options.queries.map((q) => (q.datasource ? q : { ...q, datasource })); this.setState({ queries, dataSource: ds, dsSettings, defaultDataSource }); } catch (error) { console.log('failed to load data source', error); @@ -119,6 +121,7 @@ export class QueryGroup extends PureComponent { dataSource: { name: newSettings.name, uid: newSettings.uid, + type: newSettings.meta.id, default: newSettings.isDefault, }, }); @@ -139,12 +142,10 @@ export class QueryGroup extends PureComponent { newQuery(): Partial { const { dsSettings, defaultDataSource } = this.state; - if (!dsSettings?.meta.mixed) { - return { datasource: dsSettings?.name }; - } + const ds = !dsSettings?.meta.mixed ? dsSettings : defaultDataSource; return { - datasource: defaultDataSource?.name, + datasource: { uid: ds?.uid, type: ds?.type }, }; } @@ -182,7 +183,7 @@ export class QueryGroup extends PureComponent {
{ onAddQuery = (query: Partial) => { const { dsSettings, queries } = this.state; - this.onQueriesChange(addQuery(queries, query, dsSettings?.name)); + this.onQueriesChange(addQuery(queries, query, { type: dsSettings?.type, uid: dsSettings?.uid })); this.onScrollBottom(); }; diff --git a/public/app/features/query/state/PanelQueryRunner.test.ts b/public/app/features/query/state/PanelQueryRunner.test.ts index 03b196fc517..255b7bd60cd 100644 --- a/public/app/features/query/state/PanelQueryRunner.test.ts +++ b/public/app/features/query/state/PanelQueryRunner.test.ts @@ -108,6 +108,7 @@ function describeQueryRunnerScenario( const datasource: any = { name: 'TestDB', + uid: 'TestDB-uid', interval: ctx.dsInterval, query: (options: grafanaData.DataQueryRequest) => { ctx.queryCalledWith = options; @@ -156,8 +157,8 @@ describe('PanelQueryRunner', () => { expect(ctx.queryCalledWith?.requestId).toBe('Q100'); }); - it('should set datasource name on request', async () => { - expect(ctx.queryCalledWith?.targets[0].datasource).toBe('TestDB'); + it('should set datasource uid on request', async () => { + expect(ctx.queryCalledWith?.targets[0].datasource?.uid).toBe('TestDB-uid'); }); it('should pass scopedVars to datasource with interval props', async () => { diff --git a/public/app/features/query/state/PanelQueryRunner.ts b/public/app/features/query/state/PanelQueryRunner.ts index bd78bc1abb0..a0e5eb11579 100644 --- a/public/app/features/query/state/PanelQueryRunner.ts +++ b/public/app/features/query/state/PanelQueryRunner.ts @@ -21,6 +21,7 @@ import { DataQueryRequest, DataSourceApi, DataSourceJsonData, + DataSourceRef, DataTransformerConfig, LoadingState, PanelData, @@ -38,7 +39,7 @@ export interface QueryRunnerOptions< TQuery extends DataQuery = DataQuery, TOptions extends DataSourceJsonData = DataSourceJsonData > { - datasource: string | DataSourceApi | null; + datasource: DataSourceRef | DataSourceApi | null; queries: TQuery[]; panelId?: number; dashboardId?: number; @@ -223,7 +224,7 @@ export class PanelQueryRunner { // Attach the data source name to each query request.targets = request.targets.map((query) => { if (!query.datasource) { - query.datasource = ds.name; + query.datasource = { uid: ds.uid }; } return query; }); @@ -326,7 +327,7 @@ export class PanelQueryRunner { } async function getDataSource( - datasource: string | DataSourceApi | null, + datasource: DataSourceRef | string | DataSourceApi | null, scopedVars: ScopedVars ): Promise { if (datasource && (datasource as any).query) { diff --git a/public/app/features/query/state/QueryRunner.ts b/public/app/features/query/state/QueryRunner.ts index 115592a6436..8e8ec394a29 100644 --- a/public/app/features/query/state/QueryRunner.ts +++ b/public/app/features/query/state/QueryRunner.ts @@ -8,6 +8,7 @@ import { QueryRunnerOptions, QueryRunner as QueryRunnerSrv, LoadingState, + DataSourceRef, } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -78,7 +79,7 @@ export class QueryRunner implements QueryRunnerSrv { // Attach the datasource name to each query request.targets = request.targets.map((query) => { if (!query.datasource) { - query.datasource = ds.name; + query.datasource = ds.getRef(); } return query; }); @@ -140,11 +141,11 @@ export class QueryRunner implements QueryRunnerSrv { } async function getDataSource( - datasource: string | DataSourceApi | null, + datasource: DataSourceRef | DataSourceApi | null, scopedVars: ScopedVars ): Promise { if (datasource && (datasource as any).query) { return datasource as DataSourceApi; } - return await getDatasourceSrv().get(datasource as string, scopedVars); + return await getDatasourceSrv().get(datasource, scopedVars); } diff --git a/public/app/features/sandbox/TestStuffPage.tsx b/public/app/features/sandbox/TestStuffPage.tsx index 5430cc72b75..360402b78dd 100644 --- a/public/app/features/sandbox/TestStuffPage.tsx +++ b/public/app/features/sandbox/TestStuffPage.tsx @@ -32,7 +32,7 @@ export const TestStuffPage: FC = () => { queryRunner.run({ queries: queryOptions.queries, - datasource: queryOptions.dataSource.name!, + datasource: queryOptions.dataSource, timezone: 'browser', timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange }, maxDataPoints: queryOptions.maxDataPoints ?? 100, diff --git a/public/app/features/templating/template_srv.test.ts b/public/app/features/templating/template_srv.test.ts index 26724604cab..a63572175a1 100644 --- a/public/app/features/templating/template_srv.test.ts +++ b/public/app/features/templating/template_srv.test.ts @@ -6,6 +6,8 @@ import { createQueryVariableAdapter } from '../variables/query/adapter'; import { createAdHocVariableAdapter } from '../variables/adhoc/adapter'; import { VariableModel } from '../variables/types'; import { FormatRegistryID } from './formatRegistry'; +import { setDataSourceSrv } from '@grafana/runtime'; +import { mockDataSource, MockDataSourceSrv } from '../alerting/unified/mocks'; variableAdapters.setInit(() => [ (createQueryVariableAdapter() as unknown) as VariableAdapter, @@ -119,9 +121,17 @@ describe('templateSrv', () => { name: 'ds', current: { value: 'logstash', text: 'logstash' }, }, - { type: 'adhoc', name: 'test', datasource: 'oogle', filters: [1] }, - { type: 'adhoc', name: 'test2', datasource: '$ds', filters: [2] }, + { type: 'adhoc', name: 'test', datasource: { uid: 'oogle' }, filters: [1] }, + { type: 'adhoc', name: 'test2', datasource: { uid: '$ds' }, filters: [2] }, ]); + setDataSourceSrv( + new MockDataSourceSrv({ + oogle: mockDataSource({ + name: 'oogle', + uid: 'oogle', + }), + }) + ); }); it('should return filters if datasourceName match', () => { diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index a642954d7eb..72ece2d0f76 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -3,8 +3,8 @@ import { deprecationWarning, ScopedVars, TimeRange } from '@grafana/data'; import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors'; import { variableRegex } from '../variables/utils'; import { isAdHoc } from '../variables/guard'; -import { VariableModel } from '../variables/types'; -import { setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime'; +import { AdHocVariableFilter, AdHocVariableModel, VariableModel } from '../variables/types'; +import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime'; import { FormatOptions, formatRegistry, FormatRegistryID } from './formatRegistry'; import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/state/types'; import { safeStringifyValue } from '../../core/utils/explore'; @@ -92,14 +92,21 @@ export class TemplateSrv implements BaseTemplateSrv { this.index[variable.name] = variable; } - getAdhocFilters(datasourceName: string) { + getAdhocFilters(datasourceName: string): AdHocVariableFilter[] { let filters: any = []; + let ds = getDataSourceSrv().getInstanceSettings(datasourceName); + + if (!ds) { + return []; + } for (const variable of this.getAdHocVariables()) { - if (variable.datasource === null || variable.datasource === datasourceName) { + const variableUid = variable.datasource?.uid; + + if (variableUid === ds.uid || (variable.datasource == null && ds?.isDefault)) { filters = filters.concat(variable.filters); - } else if (variable.datasource.indexOf('$') === 0) { - if (this.replace(variable.datasource) === datasourceName) { + } else if (variableUid?.indexOf('$') === 0) { + if (this.replace(variableUid) === datasourceName) { filters = filters.concat(variable.filters); } } @@ -334,8 +341,8 @@ export class TemplateSrv implements BaseTemplateSrv { return this.index[name]; } - private getAdHocVariables(): any[] { - return this.dependencies.getFilteredVariables(isAdHoc); + private getAdHocVariables(): AdHocVariableModel[] { + return this.dependencies.getFilteredVariables(isAdHoc) as AdHocVariableModel[]; } } diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx index 6c60ae79ae1..c3e46a37f72 100644 --- a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx +++ b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Alert, InlineFieldRow, VerticalGroup } from '@grafana/ui'; -import { SelectableValue } from '@grafana/data'; +import { DataSourceRef, SelectableValue } from '@grafana/data'; import { AdHocVariableModel } from '../types'; import { VariableEditorProps } from '../editor/types'; @@ -32,7 +32,7 @@ export class AdHocVariableEditorUnConnected extends PureComponent { this.props.initAdHocVariableEditor(); } - onDatasourceChanged = (option: SelectableValue) => { + onDatasourceChanged = (option: SelectableValue) => { this.props.changeVariableDatasource(option.value); }; @@ -40,7 +40,7 @@ export class AdHocVariableEditorUnConnected extends PureComponent { const { variable, editor } = this.props; const dataSources = editor.extended?.dataSources ?? []; const infoText = editor.extended?.infoText ?? null; - const options = dataSources.map((ds) => ({ label: ds.text, value: ds.value })); + const options = dataSources.map((ds) => ({ label: ds.text, value: { uid: ds.value } })); const value = options.find((o) => o.value === variable.datasource) ?? options[0]; return ( diff --git a/public/app/features/variables/adhoc/actions.test.ts b/public/app/features/variables/adhoc/actions.test.ts index 82e09609227..1ce5180a386 100644 --- a/public/app/features/variables/adhoc/actions.test.ts +++ b/public/app/features/variables/adhoc/actions.test.ts @@ -39,7 +39,7 @@ describe('adhoc actions', () => { describe('when applyFilterFromTable is dispatched and filter already exist', () => { it('then correct actions are dispatched', async () => { const options: AdHocTableOptions = { - datasource: 'influxdb', + datasource: { uid: 'influxdb' }, key: 'filter-key', value: 'filter-value', operator: '=', @@ -76,7 +76,7 @@ describe('adhoc actions', () => { describe('when applyFilterFromTable is dispatched and previously no variable or filter exists', () => { it('then correct actions are dispatched', async () => { const options: AdHocTableOptions = { - datasource: 'influxdb', + datasource: { uid: 'influxdb' }, key: 'filter-key', value: 'filter-value', operator: '=', @@ -103,7 +103,7 @@ describe('adhoc actions', () => { describe('when applyFilterFromTable is dispatched and previously no filter exists', () => { it('then correct actions are dispatched', async () => { const options: AdHocTableOptions = { - datasource: 'influxdb', + datasource: { uid: 'influxdb' }, key: 'filter-key', value: 'filter-value', operator: '=', @@ -132,7 +132,7 @@ describe('adhoc actions', () => { describe('when applyFilterFromTable is dispatched and adhoc variable with other datasource exists', () => { it('then correct actions are dispatched', async () => { const options: AdHocTableOptions = { - datasource: 'influxdb', + datasource: { uid: 'influxdb' }, key: 'filter-key', value: 'filter-value', operator: '=', @@ -141,7 +141,7 @@ describe('adhoc actions', () => { const existing = adHocBuilder() .withId('elastic-filter') .withName('elastic-filter') - .withDatasource('elasticsearch') + .withDatasource({ uid: 'elasticsearch' }) .build(); const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource(options.datasource).build(); @@ -181,7 +181,7 @@ describe('adhoc actions', () => { .withId('elastic-filter') .withName('elastic-filter') .withFilters([existing]) - .withDatasource('elasticsearch') + .withDatasource({ uid: 'elasticsearch' }) .build(); const update = { index: 0, filter: updated }; @@ -218,7 +218,7 @@ describe('adhoc actions', () => { .withId('elastic-filter') .withName('elastic-filter') .withFilters([existing]) - .withDatasource('elasticsearch') + .withDatasource({ uid: 'elasticsearch' }) .build(); const tester = await reduxTester() @@ -247,7 +247,7 @@ describe('adhoc actions', () => { .withId('elastic-filter') .withName('elastic-filter') .withFilters([]) - .withDatasource('elasticsearch') + .withDatasource({ uid: 'elasticsearch' }) .build(); const tester = await reduxTester() @@ -268,7 +268,7 @@ describe('adhoc actions', () => { .withId('elastic-filter') .withName('elastic-filter') .withFilters([]) - .withDatasource('elasticsearch') + .withDatasource({ uid: 'elasticsearch' }) .build(); const tester = await reduxTester() @@ -296,7 +296,7 @@ describe('adhoc actions', () => { .withId('elastic-filter') .withName('elastic-filter') .withFilters([filter]) - .withDatasource('elasticsearch') + .withDatasource({ uid: 'elasticsearch' }) .build(); const tester = await reduxTester() @@ -324,7 +324,7 @@ describe('adhoc actions', () => { .withId('elastic-filter') .withName('elastic-filter') .withFilters([existing]) - .withDatasource('elasticsearch') + .withDatasource({ uid: 'elasticsearch' }) .build(); const fromUrl = [ @@ -382,9 +382,9 @@ describe('adhoc actions', () => { describe('when changeVariableDatasource is dispatched with unsupported datasource', () => { it('then correct actions are dispatched', async () => { - const datasource = 'mysql'; + const datasource = { uid: 'mysql' }; const loadingText = 'Ad hoc filters are applied automatically to all queries that target this data source'; - const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource('influxdb').build(); + const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource({ uid: 'influxdb' }).build(); getDatasource.mockRestore(); getDatasource.mockResolvedValue(null); @@ -408,9 +408,9 @@ describe('adhoc actions', () => { describe('when changeVariableDatasource is dispatched with datasource', () => { it('then correct actions are dispatched', async () => { - const datasource = 'elasticsearch'; + const datasource = { uid: 'elasticsearch' }; const loadingText = 'Ad hoc filters are applied automatically to all queries that target this data source'; - const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource('influxdb').build(); + const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource({ uid: 'influxdb' }).build(); getDatasource.mockRestore(); getDatasource.mockResolvedValue({ diff --git a/public/app/features/variables/adhoc/actions.ts b/public/app/features/variables/adhoc/actions.ts index f6d8219f18c..9c90cadf5f1 100644 --- a/public/app/features/variables/adhoc/actions.ts +++ b/public/app/features/variables/adhoc/actions.ts @@ -16,9 +16,10 @@ import { import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types'; import { variableUpdated } from '../state/actions'; import { isAdHoc } from '../guard'; +import { DataSourceRef } from '@grafana/data'; export interface AdHocTableOptions { - datasource: string; + datasource: DataSourceRef; key: string; value: string; operator: string; @@ -29,6 +30,7 @@ const filterTableName = 'Filters'; export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult => { return async (dispatch, getState) => { let variable = getVariableByOptions(options, getState()); + console.log('getVariableByOptions', options, getState().templating.variables); if (!variable) { dispatch(createAdHocVariable(options)); @@ -80,7 +82,7 @@ export const setFiltersFromUrl = (id: string, filters: AdHocVariableFilter[]): T }; }; -export const changeVariableDatasource = (datasource?: string): ThunkResult => { +export const changeVariableDatasource = (datasource?: DataSourceRef): ThunkResult => { return async (dispatch, getState) => { const { editor } = getState().templating; const variable = getVariable(editor.id, getState()); @@ -155,6 +157,6 @@ const createAdHocVariable = (options: AdHocTableOptions): ThunkResult => { const getVariableByOptions = (options: AdHocTableOptions, state: StoreState): AdHocVariableModel => { return Object.values(state.templating.variables).find( - (v) => isAdHoc(v) && v.datasource === options.datasource + (v) => isAdHoc(v) && v.datasource?.uid === options.datasource.uid ) as AdHocVariableModel; }; diff --git a/public/app/features/variables/adhoc/picker/AdHocFilterBuilder.tsx b/public/app/features/variables/adhoc/picker/AdHocFilterBuilder.tsx index a3a311e2d3e..907897b6cc6 100644 --- a/public/app/features/variables/adhoc/picker/AdHocFilterBuilder.tsx +++ b/public/app/features/variables/adhoc/picker/AdHocFilterBuilder.tsx @@ -1,11 +1,11 @@ import React, { FC, useCallback, useState } from 'react'; import { AdHocVariableFilter } from 'app/features/variables/types'; -import { SelectableValue } from '@grafana/data'; +import { DataSourceRef, SelectableValue } from '@grafana/data'; import { AdHocFilterKey, REMOVE_FILTER_KEY } from './AdHocFilterKey'; import { AdHocFilterRenderer } from './AdHocFilterRenderer'; interface Props { - datasource: string; + datasource: DataSourceRef; onCompleted: (filter: AdHocVariableFilter) => void; appendBefore?: React.ReactNode; } diff --git a/public/app/features/variables/adhoc/picker/AdHocFilterKey.tsx b/public/app/features/variables/adhoc/picker/AdHocFilterKey.tsx index 6259dc42473..83d560a6e56 100644 --- a/public/app/features/variables/adhoc/picker/AdHocFilterKey.tsx +++ b/public/app/features/variables/adhoc/picker/AdHocFilterKey.tsx @@ -1,10 +1,10 @@ import React, { FC, ReactElement } from 'react'; import { Icon, SegmentAsync } from '@grafana/ui'; import { getDatasourceSrv } from '../../../plugins/datasource_srv'; -import { SelectableValue } from '@grafana/data'; +import { DataSourceRef, SelectableValue } from '@grafana/data'; interface Props { - datasource: string; + datasource: DataSourceRef; filterKey: string | null; onChange: (item: SelectableValue) => void; } @@ -51,7 +51,7 @@ const plusSegment: ReactElement = ( ); -const fetchFilterKeys = async (datasource: string): Promise>> => { +const fetchFilterKeys = async (datasource: DataSourceRef): Promise>> => { const ds = await getDatasourceSrv().get(datasource); if (!ds || !ds.getTagKeys) { @@ -62,7 +62,7 @@ const fetchFilterKeys = async (datasource: string): Promise ({ label: m.text, value: m.text })); }; -const fetchFilterKeysWithRemove = async (datasource: string): Promise>> => { +const fetchFilterKeysWithRemove = async (datasource: DataSourceRef): Promise>> => { const keys = await fetchFilterKeys(datasource); return [REMOVE_VALUE, ...keys]; }; diff --git a/public/app/features/variables/adhoc/picker/AdHocFilterRenderer.tsx b/public/app/features/variables/adhoc/picker/AdHocFilterRenderer.tsx index 126c15b4219..63ace211bf6 100644 --- a/public/app/features/variables/adhoc/picker/AdHocFilterRenderer.tsx +++ b/public/app/features/variables/adhoc/picker/AdHocFilterRenderer.tsx @@ -1,12 +1,12 @@ import React, { FC } from 'react'; import { OperatorSegment } from './OperatorSegment'; import { AdHocVariableFilter } from 'app/features/variables/types'; -import { SelectableValue } from '@grafana/data'; +import { DataSourceRef, SelectableValue } from '@grafana/data'; import { AdHocFilterKey } from './AdHocFilterKey'; import { AdHocFilterValue } from './AdHocFilterValue'; interface Props { - datasource: string; + datasource: DataSourceRef; filter: AdHocVariableFilter; onKeyChange: (item: SelectableValue) => void; onOperatorChange: (item: SelectableValue) => void; diff --git a/public/app/features/variables/adhoc/picker/AdHocFilterValue.tsx b/public/app/features/variables/adhoc/picker/AdHocFilterValue.tsx index fd8168b448b..89a04eb9c81 100644 --- a/public/app/features/variables/adhoc/picker/AdHocFilterValue.tsx +++ b/public/app/features/variables/adhoc/picker/AdHocFilterValue.tsx @@ -1,10 +1,10 @@ import React, { FC } from 'react'; import { SegmentAsync } from '@grafana/ui'; import { getDatasourceSrv } from '../../../plugins/datasource_srv'; -import { MetricFindValue, SelectableValue } from '@grafana/data'; +import { DataSourceRef, MetricFindValue, SelectableValue } from '@grafana/data'; interface Props { - datasource: string; + datasource: DataSourceRef; filterKey: string; filterValue?: string; onChange: (item: SelectableValue) => void; @@ -27,7 +27,7 @@ export const AdHocFilterValue: FC = ({ datasource, onChange, filterKey, f ); }; -const fetchFilterValues = async (datasource: string, key: string): Promise>> => { +const fetchFilterValues = async (datasource: DataSourceRef, key: string): Promise>> => { const ds = await getDatasourceSrv().get(datasource); if (!ds || !ds.getTagValues) { diff --git a/public/app/features/variables/query/QueryVariableEditor.test.tsx b/public/app/features/variables/query/QueryVariableEditor.test.tsx index c88b589e678..66e9bcfaf48 100644 --- a/public/app/features/variables/query/QueryVariableEditor.test.tsx +++ b/public/app/features/variables/query/QueryVariableEditor.test.tsx @@ -9,7 +9,8 @@ import { initialVariableEditorState } from '../editor/reducer'; import { describe, expect } from '../../../../test/lib/common'; import { NEW_VARIABLE_ID } from '../state/types'; import { LegacyVariableQueryEditor } from '../editor/LegacyVariableQueryEditor'; -import { setDataSourceSrv } from '@grafana/runtime'; +import { mockDataSource } from 'app/features/alerting/unified/mocks'; +import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; const setupTestContext = (options: Partial) => { const defaults: Props = { @@ -34,10 +35,20 @@ const setupTestContext = (options: Partial) => { return { rerender, props }; }; -setDataSourceSrv({ - getInstanceSettings: () => null, - getList: () => [], -} as any); +const mockDS = mockDataSource({ + name: 'CloudManager', + type: DataSourceType.Alertmanager, +}); + +jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { + return { + getDataSourceSrv: () => ({ + get: () => Promise.resolve(mockDS), + getList: () => [mockDS], + getInstanceSettings: () => mockDS, + }), + }; +}); describe('QueryVariableEditor', () => { describe('when the component is mounted', () => { diff --git a/public/app/features/variables/shared/testing/adHocVariableBuilder.ts b/public/app/features/variables/shared/testing/adHocVariableBuilder.ts index 99085c7abd9..973e0fd62f8 100644 --- a/public/app/features/variables/shared/testing/adHocVariableBuilder.ts +++ b/public/app/features/variables/shared/testing/adHocVariableBuilder.ts @@ -1,8 +1,9 @@ +import { DataSourceRef } from '@grafana/data'; import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types'; import { VariableBuilder } from './variableBuilder'; export class AdHocVariableBuilder extends VariableBuilder { - withDatasource(datasource: string) { + withDatasource(datasource: DataSourceRef) { this.variable.datasource = datasource; return this; } diff --git a/public/app/features/variables/types.ts b/public/app/features/variables/types.ts index 6a1f9c7273c..f012eefeab0 100644 --- a/public/app/features/variables/types.ts +++ b/public/app/features/variables/types.ts @@ -2,6 +2,7 @@ import { ComponentType } from 'react'; import { DataQuery, DataSourceJsonData, + DataSourceRef, LoadingState, QueryEditorProps, VariableModel as BaseVariableModel, @@ -48,7 +49,7 @@ export interface AdHocVariableFilter { } export interface AdHocVariableModel extends VariableModel { - datasource: string | null; + datasource: DataSourceRef | null; filters: AdHocVariableFilter[]; } diff --git a/public/app/plugins/datasource/dashboard/runSharedRequest.ts b/public/app/plugins/datasource/dashboard/runSharedRequest.ts index 00897caa8e8..13bddc95eb2 100644 --- a/public/app/plugins/datasource/dashboard/runSharedRequest.ts +++ b/public/app/plugins/datasource/dashboard/runSharedRequest.ts @@ -6,17 +6,18 @@ import { DataQuery, DataQueryRequest, DataSourceApi, + DataSourceRef, getDefaultTimeRange, LoadingState, PanelData, } from '@grafana/data'; -export function isSharedDashboardQuery(datasource: string | DataSourceApi | null) { +export function isSharedDashboardQuery(datasource: string | DataSourceRef | DataSourceApi | null) { if (!datasource) { // default datasource return false; } - if (datasource === SHARED_DASHBODARD_QUERY) { + if (datasource === SHARED_DASHBODARD_QUERY || (datasource as any)?.uid === SHARED_DASHBODARD_QUERY) { return true; } const ds = datasource as DataSourceApi; diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index fd1cccb6360..d660831600d 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -385,7 +385,7 @@ export class ElasticDatasource const expandedQueries = queries.map( (query): ElasticsearchQuery => ({ ...query, - datasource: this.name, + datasource: this.getRef(), query: this.interpolateLuceneQuery(query.query || '', scopedVars), bucketAggs: query.bucketAggs?.map(interpolateBucketAgg), }) diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 4df139e5455..feff2953581 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -7,7 +7,7 @@ import { DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings, - DatasourceRef, + DataSourceRef, isValidLiveChannelAddress, parseLiveChannelAddress, StreamingFrameOptions, @@ -18,6 +18,7 @@ import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery, GrafanaQue import AnnotationQueryEditor from './components/AnnotationQueryEditor'; import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv'; import { isString } from 'lodash'; +import { migrateDatasourceNameToRef } from 'app/features/dashboard/state/DashboardMigrator'; import { map } from 'rxjs/operators'; let counter = 100; @@ -39,10 +40,16 @@ export class GrafanaDatasource extends DataSourceWithBackend { return json; }, prepareQuery(anno: AnnotationQuery): GrafanaQuery { - let datasource: DatasourceRef | undefined | null = undefined; + let datasource: DataSourceRef | undefined | null = undefined; if (isString(anno.datasource)) { - datasource = anno.datasource as DatasourceRef; + const ref = migrateDatasourceNameToRef(anno.datasource); + if (ref) { + datasource = ref; + } + } else { + datasource = anno.datasource as DataSourceRef; } + return { ...anno, refId: anno.name, queryType: GrafanaQueryType.Annotations, datasource }; }, }; diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 0aacf3b031d..fdf8fc0e7be 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -231,7 +231,7 @@ export class GraphiteDatasource extends DataSourceApi< expandedQueries = queries.map((query) => { const expandedQuery = { ...query, - datasource: this.name, + datasource: this.getRef(), target: this.templateSrv.replace(query.target ?? '', scopedVars), }; return expandedQuery; diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 5bd878ac911..38d0e6a74bb 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -342,7 +342,7 @@ export default class InfluxDatasource extends DataSourceWithBackend { const expandedQuery = { ...query, - datasource: this.name, + datasource: this.getRef(), measurement: this.templateSrv.replace(query.measurement ?? '', scopedVars, 'regex'), policy: this.templateSrv.replace(query.policy ?? '', scopedVars, 'regex'), }; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 9dec61e94ad..9d28a4e4df8 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -311,7 +311,7 @@ export class LokiDatasource if (queries && queries.length) { expandedQueries = queries.map((query) => ({ ...query, - datasource: this.name, + datasource: this.getRef(), expr: this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr), })); } diff --git a/public/app/plugins/datasource/mixed/MixedDataSource.test.ts b/public/app/plugins/datasource/mixed/MixedDataSource.test.ts index 28cd6b58557..df6f9842067 100644 --- a/public/app/plugins/datasource/mixed/MixedDataSource.test.ts +++ b/public/app/plugins/datasource/mixed/MixedDataSource.test.ts @@ -30,9 +30,9 @@ describe('MixedDatasource', () => { const ds = new MixedDatasource({} as any); const requestMixed = getQueryOptions({ targets: [ - { refId: 'QA', datasource: 'A' }, // 1 - { refId: 'QB', datasource: 'B' }, // 2 - { refId: 'QC', datasource: 'C' }, // 3 + { refId: 'QA', datasource: { uid: 'A' } }, // 1 + { refId: 'QB', datasource: { uid: 'B' } }, // 2 + { refId: 'QC', datasource: { uid: 'C' } }, // 3 ], }); @@ -52,11 +52,11 @@ describe('MixedDatasource', () => { const ds = new MixedDatasource({} as any); const requestMixed = getQueryOptions({ targets: [ - { refId: 'QA', datasource: 'A' }, // 1 - { refId: 'QD', datasource: 'D' }, // 2 - { refId: 'QB', datasource: 'B' }, // 3 - { refId: 'QE', datasource: 'E' }, // 4 - { refId: 'QC', datasource: 'C' }, // 5 + { refId: 'QA', datasource: { uid: 'A' } }, // 1 + { refId: 'QD', datasource: { uid: 'D' } }, // 2 + { refId: 'QB', datasource: { uid: 'B' } }, // 3 + { refId: 'QE', datasource: { uid: 'E' } }, // 4 + { refId: 'QC', datasource: { uid: 'C' } }, // 5 ], }); @@ -84,9 +84,9 @@ describe('MixedDatasource', () => { const ds = new MixedDatasource({} as any); const request: any = { targets: [ - { refId: 'A', datasource: 'Loki' }, - { refId: 'B', datasource: 'Loki' }, - { refId: 'C', datasource: 'A' }, + { refId: 'A', datasource: { uid: 'Loki' } }, + { refId: 'B', datasource: { uid: 'Loki' } }, + { refId: 'C', datasource: { uid: 'A' } }, ], }; @@ -115,8 +115,8 @@ describe('MixedDatasource', () => { await expect( ds.query({ targets: [ - { refId: 'QA', datasource: 'A' }, - { refId: 'QB', datasource: 'B' }, + { refId: 'QA', datasource: { uid: 'A' } }, + { refId: 'QB', datasource: { uid: 'B' } }, ], } as any) ).toEmitValuesWith((results) => { diff --git a/public/app/plugins/datasource/mixed/MixedDataSource.ts b/public/app/plugins/datasource/mixed/MixedDataSource.ts index e06ccf68be8..63bdfa4ed50 100644 --- a/public/app/plugins/datasource/mixed/MixedDataSource.ts +++ b/public/app/plugins/datasource/mixed/MixedDataSource.ts @@ -26,7 +26,7 @@ export class MixedDatasource extends DataSourceApi { query(request: DataQueryRequest): Observable { // Remove any invalid queries const queries = request.targets.filter((t) => { - return t.datasource !== MIXED_DATASOURCE_NAME; + return t.datasource?.type !== MIXED_DATASOURCE_NAME; }); if (!queries.length) { @@ -34,19 +34,23 @@ export class MixedDatasource extends DataSourceApi { } // Build groups of queries to run in parallel - const sets: { [key: string]: DataQuery[] } = groupBy(queries, 'datasource'); + const sets: { [key: string]: DataQuery[] } = groupBy(queries, 'datasource.uid'); const mixed: BatchedQueries[] = []; for (const key in sets) { const targets = sets[key]; - const dsName = targets[0].datasource; mixed.push({ - datasource: getDataSourceSrv().get(dsName, request.scopedVars), + datasource: getDataSourceSrv().get(targets[0].datasource, request.scopedVars), targets, }); } + // Missing UIDs? + if (!mixed.length) { + return of({ data: [] } as DataQueryResponse); // nothing + } + return this.batchQueries(mixed, request); } diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index 7dad40d0cd9..f9f248c82de 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -61,7 +61,7 @@ export class MssqlDatasource extends DataSourceWithBackend { const expandedQuery = { ...query, - datasource: this.name, + datasource: this.getRef(), rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable), rawQuery: true, }; diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index 8a176e2ebbe..149c8091740 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -62,7 +62,7 @@ export class MysqlDatasource extends DataSourceWithBackend { const expandedQuery = { ...query, - datasource: this.name, + datasource: this.getRef(), rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable), rawQuery: true, }; diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 7b3339aae32..b2d131daf4d 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -64,7 +64,7 @@ export class PostgresDatasource extends DataSourceWithBackend { const expandedQuery = { ...query, - datasource: this.name, + datasource: this.getRef(), rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable), rawQuery: true, }; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 138c272e0f0..53adb179248 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -780,7 +780,7 @@ export class PrometheusDatasource extends DataSourceWithBackend { const expandedQuery = { ...query, - datasource: this.name, + datasource: this.getRef(), expr: this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr), interval: this.templateSrv.replace(query.interval, scopedVars), }; diff --git a/public/app/types/query.ts b/public/app/types/query.ts index e7f97e92523..bfa859ae135 100644 --- a/public/app/types/query.ts +++ b/public/app/types/query.ts @@ -1,4 +1,4 @@ -import { DataQuery } from '@grafana/data'; +import { DataQuery, DataSourceRef } from '@grafana/data'; import { ExpressionQuery } from '../features/expressions/types'; export interface QueryGroupOptions { @@ -14,8 +14,7 @@ export interface QueryGroupOptions { }; } -export interface QueryGroupDataSource { +export interface QueryGroupDataSource extends DataSourceRef { name?: string | null; - uid?: string; default?: boolean; } diff --git a/public/test/mocks/datasource_srv.ts b/public/test/mocks/datasource_srv.ts index 33cd24e2186..3b6eeb45b30 100644 --- a/public/test/mocks/datasource_srv.ts +++ b/public/test/mocks/datasource_srv.ts @@ -4,6 +4,8 @@ import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta, + DataSourceRef, + getDataSourceUID, } from '@grafana/data'; import { Observable } from 'rxjs'; @@ -12,15 +14,16 @@ export class DatasourceSrvMock { // } - get(name?: string): Promise { - if (!name) { + get(ref?: DataSourceRef | string): Promise { + if (!ref) { return Promise.resolve(this.defaultDS); } - const ds = this.datasources[name]; + const uid = getDataSourceUID(ref) ?? ''; + const ds = this.datasources[uid]; if (ds) { return Promise.resolve(ds); } - return Promise.reject('Unknown Datasource: ' + name); + return Promise.reject(`Unknown Datasource: ${JSON.stringify(ref)}`); } }