mirror of https://github.com/grafana/grafana.git
				
				
				
			Dashboard: replace datasource name with a reference object (#33817)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: Elfo404 <me@giordanoricci.com>
This commit is contained in:
		
							parent
							
								
									61fbdb60ff
								
							
						
					
					
						commit
						7319efe077
					
				|  | @ -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<TQuery extends DataQuery = DataQuery> { | ||||
|   datasource?: DatasourceRef | string | null; | ||||
|   datasource?: DataSourceRef | string | null; | ||||
| 
 | ||||
|   enable: boolean; | ||||
|   name: string; | ||||
|  |  | |||
|  | @ -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<TOptions = any, TCustomFieldConfig = any> { | |||
|   pluginVersion?: string; | ||||
| 
 | ||||
|   /** The datasource used in all targets */ | ||||
|   datasource?: DatasourceRef | null; | ||||
|   datasource?: DataSourceRef | null; | ||||
| 
 | ||||
|   /** The queries in a panel */ | ||||
|   targets?: DataQuery[]; | ||||
|  |  | |||
|  | @ -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<JSONData = DataSourceJsonData, SecureJSONData = {}> { | ||||
|   options: DataSourceSettings<JSONData, SecureJSONData>; | ||||
|  | @ -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 | ||||
|    */ | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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<HTMLInputElement | HTMLSelectElement> | ||||
|  |  | |||
|  | @ -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<DataSourcePickerProps, DataS | |||
| 
 | ||||
|   private getCurrentValue(): SelectableValue<string> | undefined { | ||||
|     const { current, hideTextValue, noDefault } = this.props; | ||||
| 
 | ||||
|     if (!current && noDefault) { | ||||
|       return; | ||||
|     } | ||||
|  | @ -95,16 +100,17 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS | |||
|     if (ds) { | ||||
|       return { | ||||
|         label: ds.name.substr(0, 37), | ||||
|         value: ds.name, | ||||
|         value: ds.uid, | ||||
|         imgUrl: ds.meta.info.logos.small, | ||||
|         hideText: hideTextValue, | ||||
|         meta: ds.meta, | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     const uid = getDataSourceUID(current); | ||||
|     return { | ||||
|       label: (current ?? 'no name') + ' - not found', | ||||
|       value: current === null ? undefined : current, | ||||
|       label: (uid ?? 'no name') + ' - not found', | ||||
|       value: uid ?? undefined, | ||||
|       imgUrl: '', | ||||
|       hideText: hideTextValue, | ||||
|     }; | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ export class GrafanaBootConfig implements GrafanaConfig { | |||
|   externalUserMngInfo = ''; | ||||
|   allowOrgCreate = false; | ||||
|   disableLoginForm = false; | ||||
|   defaultDatasource = ''; | ||||
|   defaultDatasource = ''; // UID
 | ||||
|   alertingEnabled = false; | ||||
|   alertingErrorOrTimeout = ''; | ||||
|   alertingNoDataOrNullValues = ''; | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { ScopedVars, DataSourceApi, DataSourceInstanceSettings } from '@grafana/data'; | ||||
| import { ScopedVars, DataSourceApi, DataSourceInstanceSettings, DataSourceRef } from '@grafana/data'; | ||||
| 
 | ||||
| /** | ||||
|  * This is the entry point for communicating with a datasource that is added as | ||||
|  | @ -11,10 +11,10 @@ import { ScopedVars, DataSourceApi, DataSourceInstanceSettings } from '@grafana/ | |||
| export interface DataSourceSrv { | ||||
|   /** | ||||
|    * Returns the requested dataSource. If it cannot be found it rejects the promise. | ||||
|    * @param nameOrUid - name or Uid of the datasource plugin you want to use. | ||||
|    * @param ref - The datasource identifier, typically an object with UID and type, | ||||
|    * @param scopedVars - variables used to interpolate a templated passed as name. | ||||
|    */ | ||||
|   get(nameOrUid?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi>; | ||||
|   get(ref?: DataSourceRef | string | null, scopedVars?: ScopedVars): Promise<DataSourceApi>; | ||||
| 
 | ||||
|   /** | ||||
|    * 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 */ | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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 | ||||
| 	} | ||||
| 	dsName, ok := rawDs.(string) | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("expted datasource identifier to be a string, got %T", rawDs) | ||||
| 		return "", fmt.Errorf("no datasource property found in query model") | ||||
| 	} | ||||
| 
 | ||||
| 	// 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("data source property is not an object nor string, got %T", rawDs) | ||||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
|  |  | |||
|  | @ -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{ | ||||
|  |  | |||
|  | @ -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()) | ||||
| 	}) | ||||
| } | ||||
|  |  | |||
|  | @ -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] } | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -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', () => { | ||||
|  |  | |||
|  | @ -99,7 +99,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise<strin | |||
|         ...state, | ||||
|         datasource: exploreDatasource.name, | ||||
|         context: 'explore', | ||||
|         queries: exploreTargets.map((t) => ({ ...t, datasource: exploreDatasource.name })), | ||||
|         queries: exploreTargets.map((t) => ({ ...t, datasource: exploreDatasource.getRef() })), | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<DataQuery>, datasource?: string): DataQuery[] { | ||||
| export function addQuery(queries: DataQuery[], query?: Partial<DataQuery>, 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; | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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<string> => { | ||||
|   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)) { | ||||
|  |  | |||
|  | @ -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<any, any>; | ||||
|     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', | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -85,7 +85,10 @@ export class QueryEditor extends PureComponent<Props, State> { | |||
|         datasourceUid: defaultDataSource.uid, | ||||
|         model: { | ||||
|           refId: '', | ||||
|           datasource: defaultDataSource.name, | ||||
|           datasource: { | ||||
|             type: defaultDataSource.type, | ||||
|             uid: defaultDataSource.uid, | ||||
|           }, | ||||
|         }, | ||||
|       }) | ||||
|     ); | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import { | |||
|   DataSourceInstanceSettings, | ||||
|   DataSourceJsonData, | ||||
|   DataSourcePluginMeta, | ||||
|   DataSourceRef, | ||||
|   ScopedVars, | ||||
| } from '@grafana/data'; | ||||
| import { | ||||
|  | @ -227,6 +228,7 @@ export const mockSilence = (partial: Partial<Silence> = {}): Silence => { | |||
|     ...partial, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export class MockDataSourceSrv implements DataSourceSrv { | ||||
|   datasources: Record<string, DataSourceApi> = {}; | ||||
|   // @ts-ignore
 | ||||
|  | @ -238,6 +240,7 @@ export class MockDataSourceSrv implements DataSourceSrv { | |||
|     getVariables: () => [], | ||||
|     replace: (name: any) => name, | ||||
|   }; | ||||
| 
 | ||||
|   defaultName = ''; | ||||
| 
 | ||||
|   constructor(datasources: Record<string, DataSourceInstanceSettings>) { | ||||
|  | @ -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<DataSourceApi> { | ||||
|   get(name?: string | null | DataSourceRef, scopedVars?: ScopedVars): Promise<DataSourceApi> { | ||||
|     return DatasourceSrv.prototype.get.call(this, name, scopedVars); | ||||
|     //return Promise.reject(new Error('not implemented'));
 | ||||
|   } | ||||
|  |  | |||
|  | @ -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', | ||||
|  |  | |||
|  | @ -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<AlertQuery[]> => { | ||||
|   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,12 +244,13 @@ 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) { | ||||
|     if (dsRef.uid === ExpressionDatasourceUID) { | ||||
|       const newQuery: AlertQuery = { | ||||
|         refId: interpolatedTarget.refId, | ||||
|         queryType: '', | ||||
|  | @ -255,7 +261,7 @@ const dataQueriesToGrafanaQueries = async ( | |||
|       result.push(newQuery); | ||||
|       // queries
 | ||||
|     } else { | ||||
|         const datasourceSettings = getDataSourceSrv().getInstanceSettings(dsName); | ||||
|       const datasourceSettings = getDataSourceSrv().getInstanceSettings(dsRef); | ||||
|       if (datasourceSettings && datasourceSettings.meta.alerting) { | ||||
|         const newQuery: AlertQuery = { | ||||
|           refId: interpolatedTarget.refId, | ||||
|  | @ -272,7 +278,6 @@ const dataQueriesToGrafanaQueries = async ( | |||
|       } | ||||
|     } | ||||
|   } | ||||
|   } | ||||
|   return result; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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'] = { | ||||
|  |  | |||
|  | @ -75,12 +75,15 @@ 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 (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; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       promises.push( | ||||
|         getDataSourceSrv() | ||||
|  |  | |||
|  | @ -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<Props> { | |||
|   } | ||||
| 
 | ||||
|   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<Props> { | |||
|   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) { | ||||
|  |  | |||
|  | @ -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', () => { | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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<string, any> = { | ||||
|     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, | ||||
|  |  | |||
|  | @ -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)! | ||||
|     ), | ||||
|   }; | ||||
| }; | ||||
|  |  | |||
|  | @ -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<DataSourceApi> { | ||||
|       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, | ||||
|   }; | ||||
|  |  | |||
|  | @ -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( | ||||
|  |  | |||
|  | @ -11,10 +11,10 @@ import { locationService } from '@grafana/runtime'; | |||
| const getNavigateToExploreContext = async (openInNewWindow?: (url: string) => void) => { | ||||
|   const url = '/explore'; | ||||
|   const panel: Partial<PanelModel> = { | ||||
|     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(); | ||||
|  |  | |||
|  | @ -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<DataQuery, DataSourceJsonData, {}>, | ||||
|           { name: 'Postgres2', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}> | ||||
|  | @ -342,6 +343,7 @@ describe('reducer', () => { | |||
|             ...defaultInitialState.explore[ExploreId.left], | ||||
|             datasourceInstance: { | ||||
|               query: jest.fn(), | ||||
|               getRef: jest.fn(), | ||||
|               meta: { | ||||
|                 id: 'something', | ||||
|               }, | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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<ExpressionQuery> { | ||||
|   constructor(instanceSettings: DataSourceInstanceSettings) { | ||||
|   constructor(public instanceSettings: DataSourceInstanceSettings) { | ||||
|     super(instanceSettings); | ||||
|   } | ||||
| 
 | ||||
|  | @ -19,7 +19,7 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQue | |||
|     return { | ||||
|       refId: '--', // Replaced with query
 | ||||
|       type: query?.type ?? ExpressionQueryType.math, | ||||
|       datasource: ExpressionDatasourceID, | ||||
|       datasource: ExpressionDatasourceRef, | ||||
|       conditions: query?.conditions ?? undefined, | ||||
|     }; | ||||
|   } | ||||
|  | @ -28,6 +28,10 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQue | |||
| // MATCHES the constant in DataSourceWithBackend
 | ||||
| export const ExpressionDatasourceID = '__expr__'; | ||||
| export const ExpressionDatasourceUID = '-100'; | ||||
| export const ExpressionDatasourceRef = Object.freeze({ | ||||
|   type: ExpressionDatasourceID, | ||||
|   uid: ExpressionDatasourceID, | ||||
| }); | ||||
| 
 | ||||
| export const instanceSettings: DataSourceInstanceSettings = { | ||||
|   id: -100, | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import React, { useState } from 'react'; | ||||
| import { Input, Field, Button, ValuePicker, HorizontalGroup } from '@grafana/ui'; | ||||
| import { DataSourcePicker, getBackendSrv } from '@grafana/runtime'; | ||||
| import { AppEvents, DatasourceRef, LiveChannelScope, SelectableValue } from '@grafana/data'; | ||||
| import { AppEvents, DataSourceRef, LiveChannelScope, SelectableValue } from '@grafana/data'; | ||||
| import appEvents from 'app/core/app_events'; | ||||
| import { Rule } from './types'; | ||||
| 
 | ||||
|  | @ -28,7 +28,7 @@ export function AddNewRule({ onRuleAdded }: Props) { | |||
|   const [patternType, setPatternType] = useState<PatternType>(); | ||||
|   const [pattern, setPattern] = useState<string>(); | ||||
|   const [patternPrefix, setPatternPrefix] = useState<string>(''); | ||||
|   const [datasource, setDatasource] = useState<DatasourceRef>(); | ||||
|   const [datasource, setDatasource] = useState<DataSourceRef>(); | ||||
| 
 | ||||
|   const onSubmit = () => { | ||||
|     if (!pattern) { | ||||
|  | @ -85,7 +85,7 @@ export function AddNewRule({ onRuleAdded }: Props) { | |||
|                 <DataSourcePicker | ||||
|                   current={datasource} | ||||
|                   onChange={(ds) => { | ||||
|                     setDatasource(ds.name); | ||||
|                     setDatasource(ds); | ||||
|                     setPatternPrefix(`${LiveChannelScope.DataSource}/${ds.uid}/`); | ||||
|                   }} | ||||
|                 /> | ||||
|  |  | |||
|  | @ -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<string, DataSourceApi> = {}; | ||||
|   private datasources: Record<string, DataSourceApi> = {}; // UID
 | ||||
|   private settingsMapByName: Record<string, DataSourceInstanceSettings> = {}; | ||||
|   private settingsMapByUid: Record<string, DataSourceInstanceSettings> = {}; | ||||
|   private settingsMapById: Record<string, DataSourceInstanceSettings> = {}; | ||||
|   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<DataSourceApi> { | ||||
|   get(ref?: string | DataSourceRef | null, scopedVars?: ScopedVars): Promise<DataSourceApi> { | ||||
|     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<DataSourceApi<any, any>> { | ||||
|     // 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<DataSourceApi<any, any>> { | ||||
|     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) { | ||||
|       if (this.$rootScope) { | ||||
|         this.$rootScope.appEvent(AppEvents.alertError, [dsConfig.name + ' plugin failed', err.toString()]); | ||||
|       return Promise.reject({ message: `Datasource named ${name} was not found` }); | ||||
|       } | ||||
|       return Promise.reject({ message: `Datasource: ${key} was not found` }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 --", | ||||
|           }, | ||||
|         ] | ||||
|       `);
 | ||||
|  |  | |||
|  | @ -121,7 +121,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop | |||
| 
 | ||||
|   getQueryDataSourceIdentifier(): string | null | undefined { | ||||
|     const { query, dataSource: dsSettings } = this.props; | ||||
|     return query.datasource ?? dsSettings.name; | ||||
|     return query.datasource?.uid ?? dsSettings.uid; | ||||
|   } | ||||
| 
 | ||||
|   async loadDatasource() { | ||||
|  |  | |||
|  | @ -3,12 +3,20 @@ import { fireEvent, render, screen } from '@testing-library/react'; | |||
| import { Props, QueryEditorRowHeader } from './QueryEditorRowHeader'; | ||||
| import { DataSourceInstanceSettings } from '@grafana/data'; | ||||
| import { selectors } from '@grafana/e2e-selectors'; | ||||
| import { mockDataSource } from 'app/features/alerting/unified/mocks'; | ||||
| import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; | ||||
| 
 | ||||
| const mockDS = mockDataSource({ | ||||
|   name: 'CloudManager', | ||||
|   type: DataSourceType.Alertmanager, | ||||
| }); | ||||
| 
 | ||||
| jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { | ||||
|   return { | ||||
|     getDataSourceSrv: () => ({ | ||||
|       getInstanceSettings: jest.fn(), | ||||
|       getList: jest.fn().mockReturnValue([]), | ||||
|       get: () => Promise.resolve(mockDS), | ||||
|       getList: () => [mockDS], | ||||
|       getInstanceSettings: () => mockDS, | ||||
|     }), | ||||
|   }; | ||||
| }); | ||||
|  |  | |||
|  | @ -67,7 +67,7 @@ export class QueryEditorRows extends PureComponent<Props> { | |||
|           if (previous?.type === dataSource.type) { | ||||
|             return { | ||||
|               ...item, | ||||
|               datasource: dataSource.name, | ||||
|               datasource: { uid: dataSource.uid }, | ||||
|             }; | ||||
|           } | ||||
|         } | ||||
|  | @ -75,7 +75,7 @@ export class QueryEditorRows extends PureComponent<Props> { | |||
|         return { | ||||
|           refId: item.refId, | ||||
|           hide: item.hide, | ||||
|           datasource: dataSource.name, | ||||
|           datasource: { uid: dataSource.uid }, | ||||
|         }; | ||||
|       }) | ||||
|     ); | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ import { | |||
|   DataQuery, | ||||
|   DataSourceApi, | ||||
|   DataSourceInstanceSettings, | ||||
|   DataSourceRef, | ||||
|   getDefaultTimeRange, | ||||
|   LoadingState, | ||||
|   PanelData, | ||||
|  | @ -91,7 +92,8 @@ export class QueryGroup extends PureComponent<Props, State> { | |||
|       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<Props, State> { | |||
|       dataSource: { | ||||
|         name: newSettings.name, | ||||
|         uid: newSettings.uid, | ||||
|         type: newSettings.meta.id, | ||||
|         default: newSettings.isDefault, | ||||
|       }, | ||||
|     }); | ||||
|  | @ -139,12 +142,10 @@ export class QueryGroup extends PureComponent<Props, State> { | |||
|   newQuery(): Partial<DataQuery> { | ||||
|     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<Props, State> { | |||
|           <div className={styles.dataSourceRowItem}> | ||||
|             <DataSourcePicker | ||||
|               onChange={this.onChangeDataSource} | ||||
|               current={options.dataSource.name} | ||||
|               current={options.dataSource} | ||||
|               metrics={true} | ||||
|               mixed={true} | ||||
|               dashboard={true} | ||||
|  | @ -258,7 +259,7 @@ export class QueryGroup extends PureComponent<Props, State> { | |||
| 
 | ||||
|   onAddQuery = (query: Partial<DataQuery>) => { | ||||
|     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(); | ||||
|   }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 () => { | ||||
|  |  | |||
|  | @ -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<TQuery, TOptions> | null; | ||||
|   datasource: DataSourceRef | DataSourceApi<TQuery, TOptions> | 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<DataSourceApi> { | ||||
|   if (datasource && (datasource as any).query) { | ||||
|  |  | |||
|  | @ -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<DataSourceApi> { | ||||
|   if (datasource && (datasource as any).query) { | ||||
|     return datasource as DataSourceApi; | ||||
|   } | ||||
|   return await getDatasourceSrv().get(datasource as string, scopedVars); | ||||
|   return await getDatasourceSrv().get(datasource, scopedVars); | ||||
| } | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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<VariableModel>, | ||||
|  | @ -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', () => { | ||||
|  |  | |||
|  | @ -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[]; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<Props> { | |||
|     this.props.initAdHocVariableEditor(); | ||||
|   } | ||||
| 
 | ||||
|   onDatasourceChanged = (option: SelectableValue<string>) => { | ||||
|   onDatasourceChanged = (option: SelectableValue<DataSourceRef>) => { | ||||
|     this.props.changeVariableDatasource(option.value); | ||||
|   }; | ||||
| 
 | ||||
|  | @ -40,7 +40,7 @@ export class AdHocVariableEditorUnConnected extends PureComponent<Props> { | |||
|     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 ( | ||||
|  |  | |||
|  | @ -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<RootReducerType>() | ||||
|  | @ -247,7 +247,7 @@ describe('adhoc actions', () => { | |||
|         .withId('elastic-filter') | ||||
|         .withName('elastic-filter') | ||||
|         .withFilters([]) | ||||
|         .withDatasource('elasticsearch') | ||||
|         .withDatasource({ uid: 'elasticsearch' }) | ||||
|         .build(); | ||||
| 
 | ||||
|       const tester = await reduxTester<RootReducerType>() | ||||
|  | @ -268,7 +268,7 @@ describe('adhoc actions', () => { | |||
|         .withId('elastic-filter') | ||||
|         .withName('elastic-filter') | ||||
|         .withFilters([]) | ||||
|         .withDatasource('elasticsearch') | ||||
|         .withDatasource({ uid: 'elasticsearch' }) | ||||
|         .build(); | ||||
| 
 | ||||
|       const tester = await reduxTester<RootReducerType>() | ||||
|  | @ -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<RootReducerType>() | ||||
|  | @ -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({ | ||||
|  |  | |||
|  | @ -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<void> => { | ||||
|   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<void> => { | ||||
| export const changeVariableDatasource = (datasource?: DataSourceRef): ThunkResult<void> => { | ||||
|   return async (dispatch, getState) => { | ||||
|     const { editor } = getState().templating; | ||||
|     const variable = getVariable(editor.id, getState()); | ||||
|  | @ -155,6 +157,6 @@ const createAdHocVariable = (options: AdHocTableOptions): ThunkResult<void> => { | |||
| 
 | ||||
| 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; | ||||
| }; | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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<string | null>) => void; | ||||
| } | ||||
|  | @ -51,7 +51,7 @@ const plusSegment: ReactElement = ( | |||
|   </a> | ||||
| ); | ||||
| 
 | ||||
| const fetchFilterKeys = async (datasource: string): Promise<Array<SelectableValue<string>>> => { | ||||
| const fetchFilterKeys = async (datasource: DataSourceRef): Promise<Array<SelectableValue<string>>> => { | ||||
|   const ds = await getDatasourceSrv().get(datasource); | ||||
| 
 | ||||
|   if (!ds || !ds.getTagKeys) { | ||||
|  | @ -62,7 +62,7 @@ const fetchFilterKeys = async (datasource: string): Promise<Array<SelectableValu | |||
|   return metrics.map((m) => ({ label: m.text, value: m.text })); | ||||
| }; | ||||
| 
 | ||||
| const fetchFilterKeysWithRemove = async (datasource: string): Promise<Array<SelectableValue<string>>> => { | ||||
| const fetchFilterKeysWithRemove = async (datasource: DataSourceRef): Promise<Array<SelectableValue<string>>> => { | ||||
|   const keys = await fetchFilterKeys(datasource); | ||||
|   return [REMOVE_VALUE, ...keys]; | ||||
| }; | ||||
|  |  | |||
|  | @ -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<string | null>) => void; | ||||
|   onOperatorChange: (item: SelectableValue<string>) => void; | ||||
|  |  | |||
|  | @ -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<string>) => void; | ||||
|  | @ -27,7 +27,7 @@ export const AdHocFilterValue: FC<Props> = ({ datasource, onChange, filterKey, f | |||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const fetchFilterValues = async (datasource: string, key: string): Promise<Array<SelectableValue<string>>> => { | ||||
| const fetchFilterValues = async (datasource: DataSourceRef, key: string): Promise<Array<SelectableValue<string>>> => { | ||||
|   const ds = await getDatasourceSrv().get(datasource); | ||||
| 
 | ||||
|   if (!ds || !ds.getTagValues) { | ||||
|  |  | |||
|  | @ -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<Props>) => { | ||||
|   const defaults: Props = { | ||||
|  | @ -34,10 +35,20 @@ const setupTestContext = (options: Partial<Props>) => { | |||
|   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', () => { | ||||
|  |  | |||
|  | @ -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<AdHocVariableModel> { | ||||
|   withDatasource(datasource: string) { | ||||
|   withDatasource(datasource: DataSourceRef) { | ||||
|     this.variable.datasource = datasource; | ||||
|     return this; | ||||
|   } | ||||
|  |  | |||
|  | @ -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[]; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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), | ||||
|       }) | ||||
|  |  | |||
|  | @ -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<GrafanaQuery> { | |||
|         return json; | ||||
|       }, | ||||
|       prepareQuery(anno: AnnotationQuery<GrafanaAnnotationQuery>): 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 }; | ||||
|       }, | ||||
|     }; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -342,7 +342,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery, | |||
|       expandedQueries = queries.map((query) => { | ||||
|         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'), | ||||
|         }; | ||||
|  |  | |||
|  | @ -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), | ||||
|       })); | ||||
|     } | ||||
|  |  | |||
|  | @ -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) => { | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ export class MixedDatasource extends DataSourceApi<DataQuery> { | |||
|   query(request: DataQueryRequest<DataQuery>): Observable<DataQueryResponse> { | ||||
|     // 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<DataQuery> { | |||
|     } | ||||
| 
 | ||||
|     // 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); | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -61,7 +61,7 @@ export class MssqlDatasource extends DataSourceWithBackend<MssqlQuery, MssqlOpti | |||
|       expandedQueries = queries.map((query) => { | ||||
|         const expandedQuery = { | ||||
|           ...query, | ||||
|           datasource: this.name, | ||||
|           datasource: this.getRef(), | ||||
|           rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable), | ||||
|           rawQuery: true, | ||||
|         }; | ||||
|  |  | |||
|  | @ -62,7 +62,7 @@ export class MysqlDatasource extends DataSourceWithBackend<MySQLQuery, MySQLOpti | |||
|       expandedQueries = queries.map((query) => { | ||||
|         const expandedQuery = { | ||||
|           ...query, | ||||
|           datasource: this.name, | ||||
|           datasource: this.getRef(), | ||||
|           rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable), | ||||
|           rawQuery: true, | ||||
|         }; | ||||
|  |  | |||
|  | @ -64,7 +64,7 @@ export class PostgresDatasource extends DataSourceWithBackend<PostgresQuery, Pos | |||
|       expandedQueries = queries.map((query) => { | ||||
|         const expandedQuery = { | ||||
|           ...query, | ||||
|           datasource: this.name, | ||||
|           datasource: this.getRef(), | ||||
|           rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable), | ||||
|           rawQuery: true, | ||||
|         }; | ||||
|  |  | |||
|  | @ -780,7 +780,7 @@ export class PrometheusDatasource extends DataSourceWithBackend<PromQuery, PromO | |||
|       expandedQueries = queries.map((query) => { | ||||
|         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), | ||||
|         }; | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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<DataSourceApi> { | ||||
|     if (!name) { | ||||
|   get(ref?: DataSourceRef | string): Promise<DataSourceApi> { | ||||
|     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)}`); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue