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:
Ryan McKinley 2021-10-29 10:57:24 -07:00 committed by GitHub
parent 61fbdb60ff
commit 7319efe077
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 759 additions and 320 deletions

View File

@ -3,13 +3,13 @@ import { ComponentType } from 'react';
import { QueryEditorProps } from './datasource'; import { QueryEditorProps } from './datasource';
import { DataFrame } from './dataFrame'; import { DataFrame } from './dataFrame';
import { DataQuery, DatasourceRef } from './query'; import { DataQuery, DataSourceRef } from './query';
/** /**
* This JSON object is stored in the dashboard json model. * This JSON object is stored in the dashboard json model.
*/ */
export interface AnnotationQuery<TQuery extends DataQuery = DataQuery> { export interface AnnotationQuery<TQuery extends DataQuery = DataQuery> {
datasource?: DatasourceRef | string | null; datasource?: DataSourceRef | string | null;
enable: boolean; enable: boolean;
name: string; name: string;

View File

@ -1,5 +1,5 @@
import { FieldConfigSource } from './fieldOverrides'; import { FieldConfigSource } from './fieldOverrides';
import { DataQuery, DatasourceRef } from './query'; import { DataQuery, DataSourceRef } from './query';
export enum DashboardCursorSync { export enum DashboardCursorSync {
Off, Off,
@ -30,7 +30,7 @@ export interface PanelModel<TOptions = any, TCustomFieldConfig = any> {
pluginVersion?: string; pluginVersion?: string;
/** The datasource used in all targets */ /** The datasource used in all targets */
datasource?: DatasourceRef | null; datasource?: DataSourceRef | null;
/** The queries in a panel */ /** The queries in a panel */
targets?: DataQuery[]; targets?: DataQuery[];

View File

@ -13,6 +13,7 @@ import { LiveChannelSupport } from './live';
import { CustomVariableSupport, DataSourceVariableSupport, StandardVariableSupport } from './variables'; import { CustomVariableSupport, DataSourceVariableSupport, StandardVariableSupport } from './variables';
import { makeClassES5Compatible } from '../utils/makeClassES5Compatible'; import { makeClassES5Compatible } from '../utils/makeClassES5Compatible';
import { DataQuery } from './query'; import { DataQuery } from './query';
import { DataSourceRef } from '.';
export interface DataSourcePluginOptionsEditorProps<JSONData = DataSourceJsonData, SecureJSONData = {}> { export interface DataSourcePluginOptionsEditorProps<JSONData = DataSourceJsonData, SecureJSONData = {}> {
options: DataSourceSettings<JSONData, SecureJSONData>; options: DataSourceSettings<JSONData, SecureJSONData>;
@ -315,6 +316,11 @@ abstract class DataSourceApi<
*/ */
getHighlighterExpression?(query: TQuery): string[]; getHighlighterExpression?(query: TQuery): string[];
/** Get an identifier object for this datasource instance */
getRef(): DataSourceRef {
return { type: this.type, uid: this.uid };
}
/** /**
* Used in explore * Used in explore
*/ */

View File

@ -8,11 +8,15 @@ export enum DataTopic {
} }
/** /**
* In 8.2, this will become an interface
*
* @public * @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 * 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 mixed data sources the selected datasource is on the query level.
* For non mixed scenarios this is undefined. * For non mixed scenarios this is undefined.
*/ */
datasource?: DatasourceRef; datasource?: DataSourceRef;
} }

View File

@ -1,5 +1,5 @@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DataQuery, DatasourceRef } from './query'; import { DataQuery, DataSourceRef } from './query';
import { DataSourceApi } from './datasource'; import { DataSourceApi } from './datasource';
import { PanelData } from './panel'; import { PanelData } from './panel';
import { ScopedVars } from './ScopedVars'; import { ScopedVars } from './ScopedVars';
@ -11,7 +11,7 @@ import { TimeRange, TimeZone } from './time';
* @internal * @internal
*/ */
export interface QueryRunnerOptions { export interface QueryRunnerOptions {
datasource: DatasourceRef | DataSourceApi | null; datasource: DataSourceRef | DataSourceApi | null;
queries: DataQuery[]; queries: DataQuery[];
panelId?: number; panelId?: number;
dashboardId?: number; dashboardId?: number;

View File

@ -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) => ( export const onUpdateDatasourceOption = (props: DataSourcePluginOptionsEditorProps, key: keyof DataSourceSettings) => (
event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement> event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>

View File

@ -3,7 +3,13 @@ import React, { PureComponent } from 'react';
// Components // Components
import { HorizontalGroup, PluginSignatureBadge, Select, stylesFactory } from '@grafana/ui'; 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 { selectors } from '@grafana/e2e-selectors';
import { getDataSourceSrv } from '../services/dataSourceSrv'; import { getDataSourceSrv } from '../services/dataSourceSrv';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
@ -15,7 +21,7 @@ import { css, cx } from '@emotion/css';
*/ */
export interface DataSourcePickerProps { export interface DataSourcePickerProps {
onChange: (ds: DataSourceInstanceSettings) => void; onChange: (ds: DataSourceInstanceSettings) => void;
current: string | null; current: DataSourceRef | string | null; // uid
hideTextValue?: boolean; hideTextValue?: boolean;
onBlur?: () => void; onBlur?: () => void;
autoFocus?: boolean; autoFocus?: boolean;
@ -85,7 +91,6 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
private getCurrentValue(): SelectableValue<string> | undefined { private getCurrentValue(): SelectableValue<string> | undefined {
const { current, hideTextValue, noDefault } = this.props; const { current, hideTextValue, noDefault } = this.props;
if (!current && noDefault) { if (!current && noDefault) {
return; return;
} }
@ -95,16 +100,17 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
if (ds) { if (ds) {
return { return {
label: ds.name.substr(0, 37), label: ds.name.substr(0, 37),
value: ds.name, value: ds.uid,
imgUrl: ds.meta.info.logos.small, imgUrl: ds.meta.info.logos.small,
hideText: hideTextValue, hideText: hideTextValue,
meta: ds.meta, meta: ds.meta,
}; };
} }
const uid = getDataSourceUID(current);
return { return {
label: (current ?? 'no name') + ' - not found', label: (uid ?? 'no name') + ' - not found',
value: current === null ? undefined : current, value: uid ?? undefined,
imgUrl: '', imgUrl: '',
hideText: hideTextValue, hideText: hideTextValue,
}; };

View File

@ -34,7 +34,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
externalUserMngInfo = ''; externalUserMngInfo = '';
allowOrgCreate = false; allowOrgCreate = false;
disableLoginForm = false; disableLoginForm = false;
defaultDatasource = ''; defaultDatasource = ''; // UID
alertingEnabled = false; alertingEnabled = false;
alertingErrorOrTimeout = ''; alertingErrorOrTimeout = '';
alertingNoDataOrNullValues = ''; alertingNoDataOrNullValues = '';

View File

@ -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 * 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 { export interface DataSourceSrv {
/** /**
* Returns the requested dataSource. If it cannot be found it rejects the promise. * 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. * @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 * Get a list of data sources
@ -24,7 +24,7 @@ export interface DataSourceSrv {
/** /**
* Get settings and plugin metadata by name or uid * Get settings and plugin metadata by name or uid
*/ */
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined; getInstanceSettings(ref?: DataSourceRef | string | null): DataSourceInstanceSettings | undefined;
} }
/** @public */ /** @public */

View File

@ -102,7 +102,7 @@ class DataSourceWithBackend<
const ds = getDataSourceSrv().getInstanceSettings(q.datasource); const ds = getDataSourceSrv().getInstanceSettings(q.datasource);
if (!ds) { if (!ds) {
throw new Error('Unknown Datasource: ' + q.datasource); throw new Error(`Unknown Datasource: ${JSON.stringify(q.datasource)}`);
} }
datasourceId = ds.id; datasourceId = ds.id;

View File

@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/expr/mathexp" "github.com/grafana/grafana/pkg/expr/mathexp"
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/simple" "gonum.org/v1/gonum/graph/simple"
"gonum.org/v1/gonum/graph/topo" "gonum.org/v1/gonum/graph/topo"
) )
@ -129,9 +128,11 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) {
for _, query := range req.Queries { for _, query := range req.Queries {
rawQueryProp := make(map[string]interface{}) rawQueryProp := make(map[string]interface{})
queryBytes, err := query.JSON.MarshalJSON() queryBytes, err := query.JSON.MarshalJSON()
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = json.Unmarshal(queryBytes, &rawQueryProp) err = json.Unmarshal(queryBytes, &rawQueryProp)
if err != nil { if err != nil {
return nil, err return nil, err
@ -145,23 +146,23 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) {
DatasourceUID: query.DatasourceUID, DatasourceUID: query.DatasourceUID,
} }
dsName, err := rn.GetDatasourceName() isExpr, err := rn.IsExpressionQuery()
if err != nil { if err != nil {
return nil, err return nil, err
} }
dsUID := rn.DatasourceUID var node Node
var node graph.Node if isExpr {
switch {
case dsName == DatasourceName || dsUID == DatasourceUID:
node, err = buildCMDNode(dp, rn) 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) node, err = s.buildDSNode(dp, rn, req)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
dp.AddNode(node) dp.AddNode(node)
} }
return dp, nil return dp, nil

View File

@ -195,6 +195,33 @@ func TestServicebuildPipeLine(t *testing.T) {
}, },
expectErrContains: "classic conditions may not be the input for other expressions", 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{} s := Service{}
for _, tt := range tests { for _, tt := range tests {

View File

@ -33,16 +33,59 @@ type rawNode struct {
DatasourceUID string 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"] rawDs, ok := rn.Query["datasource"]
if !ok { if !ok {
return "", nil return "", fmt.Errorf("no datasource property found in query model")
} }
dsName, ok := rawDs.(string)
// For old queries with string datasource prop representing data source name
if dsName, ok := rawDs.(string); ok {
return dsName, nil
}
dsRef, ok := rawDs.(map[string]interface{})
if !ok { if !ok {
return "", fmt.Errorf("expted datasource identifier to be a string, got %T", rawDs) return "", fmt.Errorf("data source property is not an object nor string, got %T", rawDs)
} }
return dsName, nil
if dsUid, ok := dsRef["uid"].(string); ok {
return dsUid, nil
}
return "", fmt.Errorf("no datasource uid found for query, got %T", rn.Query)
}
func (rn *rawNode) IsExpressionQuery() (bool, error) {
if rn.DatasourceUID != "" {
return rn.DatasourceUID == DatasourceUID, nil
}
rawDs, ok := rn.Query["datasource"]
if !ok {
return false, fmt.Errorf("no datasource property found in query model")
}
// For old queries with string datasource prop representing data source name
dsName, ok := rawDs.(string)
if ok && dsName == DatasourceName {
return true, nil
}
dsRef, ok := rawDs.(map[string]interface{})
if !ok {
return false, nil
}
if dsRef["uid"].(string) == DatasourceUID {
return true, nil
}
return false, nil
} }
func (rn *rawNode) GetCommandType() (c CommandType, err error) { 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"] rawDsID, ok := rn.Query["datasourceId"]
switch ok { if ok {
case true:
floatDsID, ok := rawDsID.(float64) floatDsID, ok := rawDsID.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("expected datasourceId to be a float64, got type %T for refId %v", rawDsID, rn.RefID) return nil, fmt.Errorf("expected datasourceId to be a float64, got type %T for refId %v", rawDsID, rn.RefID)
} }
dsNode.datasourceID = int64(floatDsID) dsNode.datasourceID = int64(floatDsID)
default: } else {
if rn.DatasourceUID == "" { 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) 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 var floatIntervalMS float64

View File

@ -30,8 +30,24 @@ func NewDashAlertExtractor(dash *models.Dashboard, orgID int64, user *models.Sig
} }
} }
func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*models.DataSource, error) { func (e *DashAlertExtractor) lookupQueryDataSource(panel *simplejson.Json, panelQuery *simplejson.Json) (*models.DataSource, error) {
if dsName == "" { 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} query := &models.GetDefaultDataSourceQuery{OrgId: e.OrgID}
if err := bus.DispatchCtx(context.TODO(), query); err != nil { if err := bus.DispatchCtx(context.TODO(), query); err != nil {
return nil, err return nil, err
@ -39,7 +55,7 @@ func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*models.DataSour
return query.Result, nil 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 { if err := bus.DispatchCtx(context.TODO(), query); err != nil {
return nil, err return nil, err
} }
@ -159,17 +175,9 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
return nil, ValidationError{Reason: reason} return nil, ValidationError{Reason: reason}
} }
dsName := "" datasource, err := e.lookupQueryDataSource(panel, panelQuery)
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)
if err != nil { if err != nil {
e.log.Debug("Error looking up datasource", "error", err) return nil, err
return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)}
} }
dsFilterQuery := models.DatasourcesPermissionFilterQuery{ dsFilterQuery := models.DatasourcesPermissionFilterQuery{

View File

@ -18,10 +18,10 @@ func TestAlertRuleExtraction(t *testing.T) {
}) })
// mock data // mock data
defaultDs := &models.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true} 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"} graphite2Ds := &models.DataSource{Id: 15, OrgId: 1, Name: "graphite2", Uid: "graphite2-uid"}
influxDBDs := &models.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"} influxDBDs := &models.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB", Uid: "InfluxDB-uid"}
prom := &models.DataSource{Id: 17, OrgId: 1, Name: "Prometheus"} prom := &models.DataSource{Id: 17, OrgId: 1, Name: "Prometheus", Uid: "Prometheus-uid"}
bus.AddHandler("test", func(query *models.GetDefaultDataSourceQuery) error { bus.AddHandler("test", func(query *models.GetDefaultDataSourceQuery) error {
query.Result = defaultDs query.Result = defaultDs
@ -29,16 +29,16 @@ func TestAlertRuleExtraction(t *testing.T) {
}) })
bus.AddHandler("test", func(query *models.GetDataSourceQuery) error { 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 query.Result = defaultDs
} }
if query.Name == graphite2Ds.Name { if query.Name == graphite2Ds.Name || query.Uid == graphite2Ds.Uid {
query.Result = graphite2Ds query.Result = graphite2Ds
} }
if query.Name == influxDBDs.Name { if query.Name == influxDBDs.Name || query.Uid == influxDBDs.Uid {
query.Result = influxDBDs query.Result = influxDBDs
} }
if query.Name == prom.Name { if query.Name == prom.Name || query.Uid == prom.Uid {
query.Result = prom query.Result = prom
} }
@ -246,4 +246,25 @@ func TestAlertRuleExtraction(t *testing.T) {
_, err = extractor.GetAlerts() _, err = extractor.GetAlerts()
require.Equal(t, err.Error(), "alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1") 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())
})
} }

View File

@ -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] }
}
]
}
}
]
}

View File

@ -198,7 +198,9 @@ describe('getExploreUrl', () => {
}, },
datasourceSrv: { datasourceSrv: {
get() { get() {
return {}; return {
getRef: jest.fn(),
};
}, },
getDataSourceById: jest.fn(), getDataSourceById: jest.fn(),
}, },
@ -239,7 +241,7 @@ describe('hasNonEmptyQuery', () => {
}); });
test('should return false if query is empty', () => { 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', () => { test('should return false if no queries exist', () => {

View File

@ -99,7 +99,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise<strin
...state, ...state,
datasource: exploreDatasource.name, datasource: exploreDatasource.name,
context: 'explore', context: 'explore',
queries: exploreTargets.map((t) => ({ ...t, datasource: exploreDatasource.name })), queries: exploreTargets.map((t) => ({ ...t, datasource: exploreDatasource.getRef() })),
}; };
} }

View File

@ -1,4 +1,4 @@
import { DataQuery, DataSourceInstanceSettings } from '@grafana/data'; import { DataQuery, DataSourceInstanceSettings, DataSourceRef, getDataSourceRef } from '@grafana/data';
export const getNextRefIdChar = (queries: DataQuery[]): string => { export const getNextRefIdChar = (queries: DataQuery[]): string => {
for (let num = 0; ; num++) { 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 || {}; const q = query || {};
q.refId = getNextRefIdChar(queries); q.refId = getNextRefIdChar(queries);
q.hide = false; q.hide = false;
@ -27,16 +27,18 @@ export function updateQueries(
extensionID: string, // pass this in because importing it creates a circular dependency extensionID: string, // pass this in because importing it creates a circular dependency
dsSettings?: DataSourceInstanceSettings dsSettings?: DataSourceInstanceSettings
): DataQuery[] { ): DataQuery[] {
const datasource = getDataSourceRef(newSettings);
if (!newSettings.meta.mixed && dsSettings?.meta.mixed) { if (!newSettings.meta.mixed && dsSettings?.meta.mixed) {
return queries.map((q) => { return queries.map((q) => {
if (q.datasource !== extensionID) { if (q.datasource !== extensionID) {
q.datasource = newSettings.name; q.datasource = datasource;
} }
return q; return q;
}); });
} else if (!newSettings.meta.mixed && dsSettings?.meta.id !== newSettings.meta.id) { } else if (!newSettings.meta.mixed && dsSettings?.meta.id !== newSettings.meta.id) {
// we are changing data source type, clear queries // we are changing data source type, clear queries
return [{ refId: 'A', datasource: newSettings.name }]; return [{ refId: 'A', datasource }];
} }
return queries; return queries;

View File

@ -1,5 +1,11 @@
import { DataSourceSrv } from '@grafana/runtime'; 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 { ElasticsearchQuery } from '../../plugins/datasource/elasticsearch/types';
import { getAlertingValidationMessage } from './getAlertingValidationMessage'; import { getAlertingValidationMessage } from './getAlertingValidationMessage';
@ -18,10 +24,13 @@ describe('getAlertingValidationMessage', () => {
return false; return false;
}, },
name: 'some name', name: 'some name',
uid: 'some uid',
} as any) as DataSourceApi; } as any) as DataSourceApi;
const getMock = jest.fn().mockResolvedValue(datasource); const getMock = jest.fn().mockResolvedValue(datasource);
const datasourceSrv: DataSourceSrv = { const datasourceSrv: DataSourceSrv = {
get: getMock, get: (ref: DataSourceRef) => {
return getMock(ref.uid);
},
getList(): DataSourceInstanceSettings[] { getList(): DataSourceInstanceSettings[] {
return []; return [];
}, },
@ -33,11 +42,13 @@ describe('getAlertingValidationMessage', () => {
]; ];
const transformations: DataTransformerConfig[] = []; 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(result).toBe('');
expect(getMock).toHaveBeenCalledTimes(2); expect(getMock).toHaveBeenCalledTimes(2);
expect(getMock).toHaveBeenCalledWith(datasource.name); expect(getMock).toHaveBeenCalledWith(datasource.uid);
}); });
}); });
@ -73,7 +84,9 @@ describe('getAlertingValidationMessage', () => {
]; ];
const transformations: DataTransformerConfig[] = []; 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(''); expect(result).toBe('');
}); });
@ -88,7 +101,9 @@ describe('getAlertingValidationMessage', () => {
} as any) as DataSourceApi; } as any) as DataSourceApi;
const getMock = jest.fn().mockResolvedValue(datasource); const getMock = jest.fn().mockResolvedValue(datasource);
const datasourceSrv: DataSourceSrv = { const datasourceSrv: DataSourceSrv = {
get: getMock, get: (ref: DataSourceRef) => {
return getMock(ref.uid);
},
getInstanceSettings: (() => {}) as any, getInstanceSettings: (() => {}) as any,
getList(): DataSourceInstanceSettings[] { getList(): DataSourceInstanceSettings[] {
return []; return [];
@ -100,7 +115,9 @@ describe('getAlertingValidationMessage', () => {
]; ];
const transformations: DataTransformerConfig[] = []; 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(result).toBe('Template variables are not supported in alert queries');
expect(getMock).toHaveBeenCalledTimes(2); expect(getMock).toHaveBeenCalledTimes(2);
@ -114,10 +131,13 @@ describe('getAlertingValidationMessage', () => {
meta: ({ alerting: false } as any) as PluginMeta, meta: ({ alerting: false } as any) as PluginMeta,
targetContainsTemplate: () => false, targetContainsTemplate: () => false,
name: 'some name', name: 'some name',
uid: 'theid',
} as any) as DataSourceApi; } as any) as DataSourceApi;
const getMock = jest.fn().mockResolvedValue(datasource); const getMock = jest.fn().mockResolvedValue(datasource);
const datasourceSrv: DataSourceSrv = { const datasourceSrv: DataSourceSrv = {
get: getMock, get: (ref: DataSourceRef) => {
return getMock(ref.uid);
},
getInstanceSettings: (() => {}) as any, getInstanceSettings: (() => {}) as any,
getList(): DataSourceInstanceSettings[] { getList(): DataSourceInstanceSettings[] {
return []; return [];
@ -129,11 +149,13 @@ describe('getAlertingValidationMessage', () => {
]; ];
const transformations: DataTransformerConfig[] = []; 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(result).toBe('The datasource does not support alerting queries');
expect(getMock).toHaveBeenCalledTimes(2); expect(getMock).toHaveBeenCalledTimes(2);
expect(getMock).toHaveBeenCalledWith(datasource.name); expect(getMock).toHaveBeenCalledWith(datasource.uid);
}); });
}); });
@ -146,7 +168,9 @@ describe('getAlertingValidationMessage', () => {
} as any) as DataSourceApi; } as any) as DataSourceApi;
const getMock = jest.fn().mockResolvedValue(datasource); const getMock = jest.fn().mockResolvedValue(datasource);
const datasourceSrv: DataSourceSrv = { const datasourceSrv: DataSourceSrv = {
get: getMock, get: (ref: DataSourceRef) => {
return getMock(ref.uid);
},
getInstanceSettings: (() => {}) as any, getInstanceSettings: (() => {}) as any,
getList(): DataSourceInstanceSettings[] { getList(): DataSourceInstanceSettings[] {
return []; return [];
@ -158,7 +182,9 @@ describe('getAlertingValidationMessage', () => {
]; ];
const transformations: DataTransformerConfig[] = [{ id: 'A', options: null }]; 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(result).toBe('Transformations are not supported in alert queries');
expect(getMock).toHaveBeenCalledTimes(0); expect(getMock).toHaveBeenCalledTimes(0);

View File

@ -1,4 +1,4 @@
import { DataQuery, DataTransformerConfig } from '@grafana/data'; import { DataQuery, DataSourceRef, DataTransformerConfig } from '@grafana/data';
import { DataSourceSrv } from '@grafana/runtime'; import { DataSourceSrv } from '@grafana/runtime';
export const getDefaultCondition = () => ({ export const getDefaultCondition = () => ({
@ -13,7 +13,7 @@ export const getAlertingValidationMessage = async (
transformations: DataTransformerConfig[] | undefined, transformations: DataTransformerConfig[] | undefined,
targets: DataQuery[], targets: DataQuery[],
datasourceSrv: DataSourceSrv, datasourceSrv: DataSourceSrv,
datasourceName: string | null datasource: DataSourceRef | null
): Promise<string> => { ): Promise<string> => {
if (targets.length === 0) { if (targets.length === 0) {
return 'Could not find any metric queries'; return 'Could not find any metric queries';
@ -27,8 +27,8 @@ export const getAlertingValidationMessage = async (
let templateVariablesNotSupported = 0; let templateVariablesNotSupported = 0;
for (const target of targets) { for (const target of targets) {
const dsName = target.datasource || datasourceName; const dsRef = target.datasource || datasource;
const ds = await datasourceSrv.get(dsName); const ds = await datasourceSrv.get(dsRef);
if (!ds.meta.alerting) { if (!ds.meta.alerting) {
alertingNotSupported++; alertingNotSupported++;
} else if (ds.targetContainsTemplate && ds.targetContainsTemplate(target)) { } else if (ds.targetContainsTemplate && ds.targetContainsTemplate(target)) {

View File

@ -148,7 +148,10 @@ const dashboard = {
} as DashboardModel; } as DashboardModel;
const panel = ({ const panel = ({
datasource: dataSources.prometheus.uid, datasource: {
type: 'prometheus',
uid: dataSources.prometheus.uid,
},
title: 'mypanel', title: 'mypanel',
id: 34, id: 34,
targets: [ targets: [
@ -169,10 +172,10 @@ describe('PanelAlertTabContent', () => {
jest.resetAllMocks(); jest.resetAllMocks();
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
const dsService = new MockDataSourceSrv(dataSources); const dsService = new MockDataSourceSrv(dataSources);
dsService.datasources[dataSources.prometheus.name] = new PrometheusDatasource( dsService.datasources[dataSources.prometheus.uid] = new PrometheusDatasource(
dataSources.prometheus dataSources.prometheus
) as DataSourceApi<any, any>; ) 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,
any any
>; >;
@ -185,15 +188,20 @@ describe('PanelAlertTabContent', () => {
maxDataPoints: 100, maxDataPoints: 100,
interval: '10s', interval: '10s',
} as any) as PanelModel); } as any) as PanelModel);
const button = await ui.createButton.find(); const button = await ui.createButton.find();
const href = button.href; const href = button.href;
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/); const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
expect(match).toHaveLength(2); expect(match).toHaveLength(2);
const defaults = JSON.parse(decodeURIComponent(match![1])); const defaults = JSON.parse(decodeURIComponent(match![1]));
expect(defaults.queries[0].model).toEqual({ expect(defaults.queries[0].model).toEqual({
expr: 'sum(some_metric [5m])) by (app)', expr: 'sum(some_metric [5m])) by (app)',
refId: 'A', refId: 'A',
datasource: 'Prometheus', datasource: {
type: 'prometheus',
uid: 'mock-ds-2',
},
interval: '', interval: '',
intervalMs: 300000, intervalMs: 300000,
maxDataPoints: 100, maxDataPoints: 100,
@ -207,15 +215,20 @@ describe('PanelAlertTabContent', () => {
maxDataPoints: 100, maxDataPoints: 100,
interval: '10s', interval: '10s',
} as any) as PanelModel); } as any) as PanelModel);
const button = await ui.createButton.find(); const button = await ui.createButton.find();
const href = button.href; const href = button.href;
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/); const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
expect(match).toHaveLength(2); expect(match).toHaveLength(2);
const defaults = JSON.parse(decodeURIComponent(match![1])); const defaults = JSON.parse(decodeURIComponent(match![1]));
expect(defaults.queries[0].model).toEqual({ expect(defaults.queries[0].model).toEqual({
expr: 'sum(some_metric [5m])) by (app)', expr: 'sum(some_metric [5m])) by (app)',
refId: 'A', refId: 'A',
datasource: 'Default', datasource: {
type: 'prometheus',
uid: 'mock-ds-3',
},
interval: '', interval: '',
intervalMs: 300000, intervalMs: 300000,
maxDataPoints: 100, maxDataPoints: 100,
@ -223,21 +236,26 @@ describe('PanelAlertTabContent', () => {
}); });
it('Will take into account datasource minInterval', async () => { 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, ({ await renderAlertTabContent(dashboard, ({
...panel, ...panel,
maxDataPoints: 100, maxDataPoints: 100,
} as any) as PanelModel); } as any) as PanelModel);
const button = await ui.createButton.find(); const button = await ui.createButton.find();
const href = button.href; const href = button.href;
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/); const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
expect(match).toHaveLength(2); expect(match).toHaveLength(2);
const defaults = JSON.parse(decodeURIComponent(match![1])); const defaults = JSON.parse(decodeURIComponent(match![1]));
expect(defaults.queries[0].model).toEqual({ expect(defaults.queries[0].model).toEqual({
expr: 'sum(some_metric [7m])) by (app)', expr: 'sum(some_metric [7m])) by (app)',
refId: 'A', refId: 'A',
datasource: 'Prometheus', datasource: {
type: 'prometheus',
uid: 'mock-ds-2',
},
interval: '', interval: '',
intervalMs: 420000, intervalMs: 420000,
maxDataPoints: 100, maxDataPoints: 100,
@ -254,10 +272,12 @@ describe('PanelAlertTabContent', () => {
expect(rows).toHaveLength(1); expect(rows).toHaveLength(1);
expect(rows[0]).toHaveTextContent(/dashboardrule1/); expect(rows[0]).toHaveTextContent(/dashboardrule1/);
expect(rows[0]).not.toHaveTextContent(/dashboardrule2/); expect(rows[0]).not.toHaveTextContent(/dashboardrule2/);
const button = await ui.createButton.find(); const button = await ui.createButton.find();
const href = button.href; const href = button.href;
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/); const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
expect(match).toHaveLength(2); expect(match).toHaveLength(2);
const defaults = JSON.parse(decodeURIComponent(match![1])); const defaults = JSON.parse(decodeURIComponent(match![1]));
expect(defaults).toEqual({ expect(defaults).toEqual({
type: 'grafana', type: 'grafana',
@ -271,7 +291,10 @@ describe('PanelAlertTabContent', () => {
model: { model: {
expr: 'sum(some_metric [15s])) by (app)', expr: 'sum(some_metric [15s])) by (app)',
refId: 'A', refId: 'A',
datasource: 'Prometheus', datasource: {
type: 'prometheus',
uid: 'mock-ds-2',
},
interval: '', interval: '',
intervalMs: 15000, intervalMs: 15000,
}, },
@ -284,7 +307,10 @@ describe('PanelAlertTabContent', () => {
refId: 'B', refId: 'B',
hide: false, hide: false,
type: 'classic_conditions', type: 'classic_conditions',
datasource: '__expr__', datasource: {
type: 'grafana-expression',
uid: '-100',
},
conditions: [ conditions: [
{ {
type: 'query', type: 'query',

View File

@ -1,6 +1,6 @@
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { Field, InputControl, Select } from '@grafana/ui'; 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 React, { FC, useEffect, useMemo } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { RuleFormValues } from '../../types/rule-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 // reset condition if option no longer exists or if it is unset, but there are options available
useEffect(() => { 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)) { if (condition && !options.find(({ value }) => value === condition)) {
setValue('condition', expressions.length ? expressions[expressions.length - 1].refId : null); setValue('condition', expressions.length ? expressions[expressions.length - 1].refId : null);
} else if (!condition && expressions.length) { } else if (!condition && expressions.length) {

View File

@ -85,7 +85,10 @@ export class QueryEditor extends PureComponent<Props, State> {
datasourceUid: defaultDataSource.uid, datasourceUid: defaultDataSource.uid,
model: { model: {
refId: '', refId: '',
datasource: defaultDataSource.name, datasource: {
type: defaultDataSource.type,
uid: defaultDataSource.uid,
},
}, },
}) })
); );

View File

@ -3,6 +3,7 @@ import {
DataSourceInstanceSettings, DataSourceInstanceSettings,
DataSourceJsonData, DataSourceJsonData,
DataSourcePluginMeta, DataSourcePluginMeta,
DataSourceRef,
ScopedVars, ScopedVars,
} from '@grafana/data'; } from '@grafana/data';
import { import {
@ -227,6 +228,7 @@ export const mockSilence = (partial: Partial<Silence> = {}): Silence => {
...partial, ...partial,
}; };
}; };
export class MockDataSourceSrv implements DataSourceSrv { export class MockDataSourceSrv implements DataSourceSrv {
datasources: Record<string, DataSourceApi> = {}; datasources: Record<string, DataSourceApi> = {};
// @ts-ignore // @ts-ignore
@ -238,6 +240,7 @@ export class MockDataSourceSrv implements DataSourceSrv {
getVariables: () => [], getVariables: () => [],
replace: (name: any) => name, replace: (name: any) => name,
}; };
defaultName = ''; defaultName = '';
constructor(datasources: Record<string, DataSourceInstanceSettings>) { constructor(datasources: Record<string, DataSourceInstanceSettings>) {
@ -249,6 +252,7 @@ export class MockDataSourceSrv implements DataSourceSrv {
}, },
{} {}
); );
for (const dsSettings of Object.values(this.settingsMapByName)) { for (const dsSettings of Object.values(this.settingsMapByName)) {
this.settingsMapByUid[dsSettings.uid] = dsSettings; this.settingsMapByUid[dsSettings.uid] = dsSettings;
this.settingsMapById[dsSettings.id] = 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 DatasourceSrv.prototype.get.call(this, name, scopedVars);
//return Promise.reject(new Error('not implemented')); //return Promise.reject(new Error('not implemented'));
} }

View File

@ -3,6 +3,7 @@ import { alertRuleToQueries } from './query';
import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
import { CombinedRule } from 'app/types/unified-alerting'; import { CombinedRule } from 'app/types/unified-alerting';
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto'; import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
describe('alertRuleToQueries', () => { describe('alertRuleToQueries', () => {
it('it should convert grafana alert', () => { it('it should convert grafana alert', () => {
@ -110,7 +111,9 @@ const grafanaAlert = {
type: 'query', type: 'query',
}, },
], ],
datasource: '__expr__', datasource: {
uid: ExpressionDatasourceUID,
},
hide: false, hide: false,
refId: 'B', refId: 'B',
type: 'classic_conditions', type: 'classic_conditions',

View File

@ -6,12 +6,13 @@ import {
getDefaultRelativeTimeRange, getDefaultRelativeTimeRange,
TimeRange, TimeRange,
IntervalValues, IntervalValues,
DataSourceRef,
} from '@grafana/data'; } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { getNextRefIdChar } from 'app/core/utils/query'; import { getNextRefIdChar } from 'app/core/utils/query';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; 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 { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types';
import { RuleWithLocation } from 'app/types/unified-alerting'; import { RuleWithLocation } from 'app/types/unified-alerting';
import { import {
@ -189,7 +190,10 @@ const getDefaultExpression = (refId: string): AlertQuery => {
refId, refId,
hide: false, hide: false,
type: ExpressionQueryType.classic, type: ExpressionQueryType.classic,
datasource: ExpressionDatasourceID, datasource: {
uid: ExpressionDatasourceUID,
type: 'grafana-expression',
},
conditions: [ conditions: [
{ {
type: 'query', type: 'query',
@ -223,14 +227,15 @@ const dataQueriesToGrafanaQueries = async (
queries: DataQuery[], queries: DataQuery[],
relativeTimeRange: RelativeTimeRange, relativeTimeRange: RelativeTimeRange,
scopedVars: ScopedVars | {}, scopedVars: ScopedVars | {},
datasourceName?: string, panelDataSourceRef?: DataSourceRef,
maxDataPoints?: number, maxDataPoints?: number,
minInterval?: string minInterval?: string
): Promise<AlertQuery[]> => { ): Promise<AlertQuery[]> => {
const result: AlertQuery[] = []; const result: AlertQuery[] = [];
for (const target of queries) { for (const target of queries) {
const datasource = await getDataSourceSrv().get(target.datasource || datasourceName); const datasource = await getDataSourceSrv().get(target.datasource?.uid ? target.datasource : panelDataSourceRef);
const dsName = datasource.name; const dsRef = { uid: datasource.uid, type: datasource.type };
const range = rangeUtil.relativeToTimeRange(relativeTimeRange); const range = rangeUtil.relativeToTimeRange(relativeTimeRange);
const { interval, intervalMs } = getIntervals(range, minInterval ?? datasource.interval, maxDataPoints); const { interval, intervalMs } = getIntervals(range, minInterval ?? datasource.interval, maxDataPoints);
@ -239,37 +244,37 @@ const dataQueriesToGrafanaQueries = async (
__interval_ms: { text: intervalMs, value: intervalMs }, __interval_ms: { text: intervalMs, value: intervalMs },
...scopedVars, ...scopedVars,
}; };
const interpolatedTarget = datasource.interpolateVariablesInQueries const interpolatedTarget = datasource.interpolateVariablesInQueries
? await datasource.interpolateVariablesInQueries([target], queryVariables)[0] ? await datasource.interpolateVariablesInQueries([target], queryVariables)[0]
: target; : target;
if (dsName) {
// expressions // expressions
if (dsName === ExpressionDatasourceID) { if (dsRef.uid === ExpressionDatasourceUID) {
const newQuery: AlertQuery = {
refId: interpolatedTarget.refId,
queryType: '',
relativeTimeRange,
datasourceUid: ExpressionDatasourceUID,
model: interpolatedTarget,
};
result.push(newQuery);
// queries
} else {
const datasourceSettings = getDataSourceSrv().getInstanceSettings(dsRef);
if (datasourceSettings && datasourceSettings.meta.alerting) {
const newQuery: AlertQuery = { const newQuery: AlertQuery = {
refId: interpolatedTarget.refId, refId: interpolatedTarget.refId,
queryType: '', queryType: interpolatedTarget.queryType ?? '',
relativeTimeRange, relativeTimeRange,
datasourceUid: ExpressionDatasourceUID, datasourceUid: datasourceSettings.uid,
model: interpolatedTarget, model: {
...interpolatedTarget,
maxDataPoints,
intervalMs,
},
}; };
result.push(newQuery); result.push(newQuery);
// queries
} else {
const datasourceSettings = getDataSourceSrv().getInstanceSettings(dsName);
if (datasourceSettings && datasourceSettings.meta.alerting) {
const newQuery: AlertQuery = {
refId: interpolatedTarget.refId,
queryType: interpolatedTarget.queryType ?? '',
relativeTimeRange,
datasourceUid: datasourceSettings.uid,
model: {
...interpolatedTarget,
maxDataPoints,
intervalMs,
},
};
result.push(newQuery);
}
} }
} }
} }

View File

@ -2,17 +2,13 @@ import { find } from 'lodash';
import config from 'app/core/config'; import config from 'app/core/config';
import { DashboardExporter, LibraryElementExport } from './DashboardExporter'; import { DashboardExporter, LibraryElementExport } from './DashboardExporter';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
import { PanelPluginMeta } from '@grafana/data'; import { DataSourceInstanceSettings, DataSourceRef, PanelPluginMeta } from '@grafana/data';
import { variableAdapters } from '../../../variables/adapters'; import { variableAdapters } from '../../../variables/adapters';
import { createConstantVariableAdapter } from '../../../variables/constant/adapter'; import { createConstantVariableAdapter } from '../../../variables/constant/adapter';
import { createQueryVariableAdapter } from '../../../variables/query/adapter'; import { createQueryVariableAdapter } from '../../../variables/query/adapter';
import { createDataSourceVariableAdapter } from '../../../variables/datasource/adapter'; import { createDataSourceVariableAdapter } from '../../../variables/datasource/adapter';
import { LibraryElementKind } from '../../../library-panels/types'; import { LibraryElementKind } from '../../../library-panels/types';
function getStub(arg: string) {
return Promise.resolve(stubs[arg || 'gfdb']);
}
jest.mock('app/core/store', () => { jest.mock('app/core/store', () => {
return { return {
getBool: jest.fn(), getBool: jest.fn(),
@ -22,9 +18,16 @@ jest.mock('app/core/store', () => {
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...((jest.requireActual('@grafana/runtime') as unknown) as object), ...((jest.requireActual('@grafana/runtime') as unknown) as object),
getDataSourceSrv: () => ({ getDataSourceSrv: () => {
get: jest.fn((arg) => getStub(arg)), return {
}), get: (v: any) => {
const s = getStubInstanceSettings(v);
// console.log('GET', v, s);
return Promise.resolve(s);
},
getInstanceSettings: getStubInstanceSettings,
};
},
config: { config: {
buildInfo: {}, buildInfo: {},
panels: {}, panels: {},
@ -48,7 +51,7 @@ describe('given dashboard with repeated panels', () => {
{ {
name: 'apps', name: 'apps',
type: 'query', type: 'query',
datasource: 'gfdb', datasource: { uid: 'gfdb', type: 'testdb' },
current: { value: 'Asd', text: 'Asd' }, current: { value: 'Asd', text: 'Asd' },
options: [{ value: 'Asd', text: 'Asd' }], options: [{ value: 'Asd', text: 'Asd' }],
}, },
@ -72,22 +75,22 @@ describe('given dashboard with repeated panels', () => {
list: [ list: [
{ {
name: 'logs', name: 'logs',
datasource: 'gfdb', datasource: { uid: 'gfdb', type: 'testdb' },
}, },
], ],
}, },
panels: [ panels: [
{ id: 6, datasource: 'gfdb', type: 'graph' }, { id: 6, datasource: { uid: 'gfdb', type: 'testdb' }, type: 'graph' },
{ id: 7 }, { id: 7 },
{ {
id: 8, id: 8,
datasource: '-- Mixed --', datasource: { uid: '-- Mixed --', type: 'mixed' },
targets: [{ datasource: 'other' }], targets: [{ datasource: { uid: 'other', type: 'other' } }],
}, },
{ id: 9, datasource: '$ds' }, { id: 9, datasource: { uid: '$ds', type: 'other2' } },
{ {
id: 17, id: 17,
datasource: '$ds', datasource: { uid: '$ds', type: 'other2' },
type: 'graph', type: 'graph',
libraryPanel: { libraryPanel: {
name: 'Library Panel 2', name: 'Library Panel 2',
@ -97,7 +100,7 @@ describe('given dashboard with repeated panels', () => {
{ {
id: 2, id: 2,
repeat: 'apps', repeat: 'apps',
datasource: 'gfdb', datasource: { uid: 'gfdb', type: 'testdb' },
type: 'graph', type: 'graph',
}, },
{ id: 3, repeat: null, repeatPanelId: 2 }, { id: 3, repeat: null, repeatPanelId: 2 },
@ -105,24 +108,24 @@ describe('given dashboard with repeated panels', () => {
id: 4, id: 4,
collapsed: true, collapsed: true,
panels: [ panels: [
{ id: 10, datasource: 'gfdb', type: 'table' }, { id: 10, datasource: { uid: 'gfdb', type: 'testdb' }, type: 'table' },
{ id: 11 }, { id: 11 },
{ {
id: 12, id: 12,
datasource: '-- Mixed --', datasource: { uid: '-- Mixed --', type: 'mixed' },
targets: [{ datasource: 'other' }], targets: [{ datasource: { uid: 'other', type: 'other' } }],
}, },
{ id: 13, datasource: '$ds' }, { id: 13, datasource: { uid: '$uid', type: 'other' } },
{ {
id: 14, id: 14,
repeat: 'apps', repeat: 'apps',
datasource: 'gfdb', datasource: { uid: 'gfdb', type: 'testdb' },
type: 'heatmap', type: 'heatmap',
}, },
{ id: 15, repeat: null, repeatPanelId: 14 }, { id: 15, repeat: null, repeatPanelId: 14 },
{ {
id: 16, id: 16,
datasource: 'gfdb', datasource: { uid: 'gfdb', type: 'testdb' },
type: 'graph', type: 'graph',
libraryPanel: { libraryPanel: {
name: 'Library Panel', name: 'Library Panel',
@ -264,7 +267,7 @@ describe('given dashboard with repeated panels', () => {
expect(element.kind).toBe(LibraryElementKind.Panel); expect(element.kind).toBe(LibraryElementKind.Panel);
expect(element.model).toEqual({ expect(element.model).toEqual({
id: 17, id: 17,
datasource: '$ds', datasource: '${DS_OTHER2}',
type: 'graph', type: 'graph',
fieldConfig: { fieldConfig: {
defaults: {}, 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 // Stub responses
const stubs: { [key: string]: {} } = {}; const stubs: { [key: string]: {} } = {};
stubs['gfdb'] = { stubs['gfdb'] = {

View File

@ -75,10 +75,13 @@ export class DashboardExporter {
let datasourceVariable: any = null; let datasourceVariable: any = null;
// ignore data source properties that contain a variable // ignore data source properties that contain a variable
if (datasource && datasource.indexOf('$') === 0) { if (datasource && (datasource as any).uid) {
datasourceVariable = variableLookup[datasource.substring(1)]; const uid = (datasource as any).uid as string;
if (datasourceVariable && datasourceVariable.current) { if (uid.indexOf('$') === 0) {
datasource = datasourceVariable.current.value; datasourceVariable = variableLookup[uid.substring(1)];
if (datasourceVariable && datasourceVariable.current) {
datasource = datasourceVariable.current.value;
}
} }
} }

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { QueryGroup } from 'app/features/query/components/QueryGroup'; import { QueryGroup } from 'app/features/query/components/QueryGroup';
import { PanelModel } from '../../state'; import { PanelModel } from '../../state';
import { getLocationSrv } from '@grafana/runtime'; import { getLocationSrv } from '@grafana/runtime';
import { QueryGroupOptions } from 'app/types'; import { QueryGroupDataSource, QueryGroupOptions } from 'app/types';
import { DataQuery } from '@grafana/data'; import { DataQuery } from '@grafana/data';
interface Props { interface Props {
@ -18,10 +18,17 @@ export class PanelEditorQueries extends PureComponent<Props> {
} }
buildQueryOptions(panel: PanelModel): QueryGroupOptions { buildQueryOptions(panel: PanelModel): QueryGroupOptions {
const dataSource: QueryGroupDataSource = panel.datasource?.uid
? {
default: false,
...panel.datasource,
}
: {
default: true,
};
return { return {
dataSource: { dataSource,
name: panel.datasource,
},
queries: panel.targets, queries: panel.targets,
maxDataPoints: panel.maxDataPoints, maxDataPoints: panel.maxDataPoints,
minInterval: panel.interval, minInterval: panel.interval,
@ -47,8 +54,8 @@ export class PanelEditorQueries extends PureComponent<Props> {
onOptionsChange = (options: QueryGroupOptions) => { onOptionsChange = (options: QueryGroupOptions) => {
const { panel } = this.props; const { panel } = this.props;
const newDataSourceName = options.dataSource.default ? null : options.dataSource.name!; const newDataSourceID = options.dataSource.default ? null : options.dataSource.uid!;
const dataSourceChanged = newDataSourceName !== panel.datasource; const dataSourceChanged = newDataSourceID !== panel.datasource?.uid;
panel.updateQueries(options); panel.updateQueries(options);
if (dataSourceChanged) { if (dataSourceChanged) {

View File

@ -162,7 +162,7 @@ describe('DashboardModel', () => {
}); });
it('dashboard schema version should be set to latest', () => { 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', () => { it('graph thresholds should be migrated', () => {

View File

@ -9,6 +9,7 @@ import { DashboardModel } from './DashboardModel';
import { import {
DataLink, DataLink,
DataLinkBuiltInVars, DataLinkBuiltInVars,
DataSourceRef,
MappingType, MappingType,
SpecialValueMatch, SpecialValueMatch,
PanelPlugin, PanelPlugin,
@ -39,6 +40,8 @@ import { config } from 'app/core/config';
import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module'; import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module';
import { plugin as gaugePanelPlugin } from 'app/plugins/panel/gauge/module'; import { plugin as gaugePanelPlugin } from 'app/plugins/panel/gauge/module';
import { getStandardFieldConfigs, getStandardOptionEditors } from '@grafana/ui'; 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 { labelsToFieldsTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/labelsToFields';
import { mergeTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/merge'; import { mergeTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/merge';
import { import {
@ -62,7 +65,7 @@ export class DashboardMigrator {
let i, j, k, n; let i, j, k, n;
const oldVersion = this.dashboard.schemaVersion; const oldVersion = this.dashboard.schemaVersion;
const panelUpgrades: PanelSchemeUpgradeHandler[] = []; const panelUpgrades: PanelSchemeUpgradeHandler[] = [];
this.dashboard.schemaVersion = 32; this.dashboard.schemaVersion = 33;
if (oldVersion === this.dashboard.schemaVersion) { if (oldVersion === this.dashboard.schemaVersion) {
return; return;
@ -695,6 +698,45 @@ export class DashboardMigrator {
this.migrateCloudWatchAnnotationQuery(); 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) { if (panelUpgrades.length === 0) {
return; return;
} }
@ -1009,6 +1051,19 @@ function migrateSinglestat(panel: PanelModel) {
return panel; 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 // mutates transformations appending a new transformer after the existing one
function appendTransformerAfter(panel: PanelModel, id: string, cfg: DataTransformerConfig) { function appendTransformerAfter(panel: PanelModel, id: string, cfg: DataTransformerConfig) {
if (panel.transformations) { if (panel.transformations) {

View File

@ -19,7 +19,7 @@ import {
ScopedVars, ScopedVars,
urlUtil, urlUtil,
PanelModel as IPanelModel, PanelModel as IPanelModel,
DatasourceRef, DataSourceRef,
} from '@grafana/data'; } from '@grafana/data';
import config from 'app/core/config'; import config from 'app/core/config';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
@ -144,7 +144,7 @@ export class PanelModel implements DataConfigSource, IPanelModel {
panels?: any; panels?: any;
declare targets: DataQuery[]; declare targets: DataQuery[];
transformations?: DataTransformerConfig[]; transformations?: DataTransformerConfig[];
datasource: DatasourceRef | null = null; datasource: DataSourceRef | null = null;
thresholds?: any; thresholds?: any;
pluginVersion?: string; pluginVersion?: string;
@ -442,7 +442,13 @@ export class PanelModel implements DataConfigSource, IPanelModel {
} }
updateQueries(options: QueryGroupOptions) { 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.timeFrom = options.timeRange?.from;
this.timeShift = options.timeRange?.shift; this.timeShift = options.timeRange?.shift;
this.hideTimeOverride = options.timeRange?.hide; this.hideTimeOverride = options.timeRange?.hide;

View File

@ -12,13 +12,15 @@ import { DataQuery } from '../../../../packages/grafana-data/src';
function setup(queries: DataQuery[]) { function setup(queries: DataQuery[]) {
const defaultDs = { const defaultDs = {
name: 'newDs', name: 'newDs',
uid: 'newDs-uid',
meta: { id: 'newDs' }, meta: { id: 'newDs' },
}; };
const datasources: Record<string, any> = { const datasources: Record<string, any> = {
newDs: defaultDs, 'newDs-uid': defaultDs,
someDs: { 'someDs-uid': {
name: 'someDs', name: 'someDs',
uid: 'someDs-uid',
meta: { id: 'someDs' }, meta: { id: 'someDs' },
components: { components: {
QueryEditor: () => 'someDs query editor', QueryEditor: () => 'someDs query editor',
@ -30,11 +32,11 @@ function setup(queries: DataQuery[]) {
getList() { getList() {
return Object.values(datasources).map((d) => ({ name: d.name })); return Object.values(datasources).map((d) => ({ name: d.name }));
}, },
getInstanceSettings(name: string) { getInstanceSettings(uid: string) {
return datasources[name] || defaultDs; return datasources[uid] || defaultDs;
}, },
get(name?: string) { get(uid?: string) {
return Promise.resolve(name ? datasources[name] || defaultDs : defaultDs); return Promise.resolve(uid ? datasources[uid] || defaultDs : defaultDs);
}, },
} as any); } as any);
@ -42,7 +44,7 @@ function setup(queries: DataQuery[]) {
const initialState: ExploreState = { const initialState: ExploreState = {
left: { left: {
...leftState, ...leftState,
datasourceInstance: datasources.someDs, datasourceInstance: datasources['someDs-uid'],
queries, queries,
}, },
syncedTimes: false, syncedTimes: false,

View File

@ -22,7 +22,7 @@ const makeSelectors = (exploreId: ExploreId) => {
getEventBridge: createSelector(exploreItemSelector, (s) => s!.eventBridge), getEventBridge: createSelector(exploreItemSelector, (s) => s!.eventBridge),
getDatasourceInstanceSettings: createSelector( getDatasourceInstanceSettings: createSelector(
exploreItemSelector, exploreItemSelector,
(s) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.name)! (s) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.uid)!
), ),
}; };
}; };

View File

@ -317,10 +317,12 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
return dsSettings.map((d) => d.settings); return dsSettings.map((d) => d.settings);
}, },
getInstanceSettings(name: string) { 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> { 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); } as any);
@ -392,7 +394,9 @@ function makeDatasourceSetup({ name = 'loki', id = 1 }: { name?: string; id?: nu
}, },
}, },
name: name, name: name,
uid: name,
query: jest.fn(), query: jest.fn(),
getRef: jest.fn(),
meta, meta,
} as any, } as any,
}; };

View File

@ -62,6 +62,7 @@ function setup(state?: any) {
query: jest.fn(), query: jest.fn(),
name: 'newDs', name: 'newDs',
meta: { id: 'newDs' }, meta: { id: 'newDs' },
getRef: () => ({ uid: 'newDs' }),
}, },
someDs: { someDs: {
testDatasource: jest.fn(), testDatasource: jest.fn(),
@ -69,6 +70,7 @@ function setup(state?: any) {
query: jest.fn(), query: jest.fn(),
name: 'someDs', name: 'someDs',
meta: { id: 'someDs' }, meta: { id: 'someDs' },
getRef: () => ({ uid: 'someDs' }),
}, },
}; };
@ -77,7 +79,7 @@ function setup(state?: any) {
return Object.values(datasources).map((d) => ({ name: d.name })); return Object.values(datasources).map((d) => ({ name: d.name }));
}, },
getInstanceSettings(name: string) { getInstanceSettings(name: string) {
return { name: 'hello' }; return { name, getRef: () => ({ uid: name }) };
}, },
get(name?: string) { get(name?: string) {
return Promise.resolve( return Promise.resolve(

View File

@ -11,10 +11,10 @@ import { locationService } from '@grafana/runtime';
const getNavigateToExploreContext = async (openInNewWindow?: (url: string) => void) => { const getNavigateToExploreContext = async (openInNewWindow?: (url: string) => void) => {
const url = '/explore'; const url = '/explore';
const panel: Partial<PanelModel> = { const panel: Partial<PanelModel> = {
datasource: 'mocked datasource', datasource: { uid: 'mocked datasource' },
targets: [{ refId: 'A' }], targets: [{ refId: 'A' }],
}; };
const datasource = new MockDataSourceApi(panel.datasource!); const datasource = new MockDataSourceApi(panel.datasource!.uid!);
const get = jest.fn().mockResolvedValue(datasource); const get = jest.fn().mockResolvedValue(datasource);
const getDataSourceSrv = jest.fn().mockReturnValue({ get }); const getDataSourceSrv = jest.fn().mockReturnValue({ get });
const getTimeSrv = jest.fn(); const getTimeSrv = jest.fn();

View File

@ -64,6 +64,7 @@ const defaultInitialState = {
[ExploreId.left]: { [ExploreId.left]: {
datasourceInstance: { datasourceInstance: {
query: jest.fn(), query: jest.fn(),
getRef: jest.fn(),
meta: { meta: {
id: 'something', id: 'something',
}, },
@ -160,8 +161,8 @@ describe('importing queries', () => {
importQueries( importQueries(
ExploreId.left, ExploreId.left,
[ [
{ datasource: 'postgres1', refId: 'refId_A' }, { datasource: { type: 'postgresql' }, refId: 'refId_A' },
{ datasource: 'postgres1', refId: 'refId_B' }, { datasource: { type: 'postgresql' }, refId: 'refId_B' },
], ],
{ name: 'Postgres1', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}>, { name: 'Postgres1', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
{ name: 'Postgres2', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}> { name: 'Postgres2', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}>
@ -342,6 +343,7 @@ describe('reducer', () => {
...defaultInitialState.explore[ExploreId.left], ...defaultInitialState.explore[ExploreId.left],
datasourceInstance: { datasourceInstance: {
query: jest.fn(), query: jest.fn(),
getRef: jest.fn(),
meta: { meta: {
id: 'something', id: 'something',
}, },

View File

@ -342,7 +342,7 @@ export const runQueries = (
const queries = exploreItemState.queries.map((query) => ({ const queries = exploreItemState.queries.map((query) => ({
...query, ...query,
datasource: query.datasource || datasourceInstance?.name, datasource: query.datasource || datasourceInstance?.getRef(),
})); }));
const cachedValue = getResultsFromCache(cache, absoluteRange); const cachedValue = getResultsFromCache(cache, absoluteRange);

View File

@ -7,7 +7,7 @@ import { DataSourceWithBackend } from '@grafana/runtime';
* This is a singleton instance that just pretends to be a DataSource * This is a singleton instance that just pretends to be a DataSource
*/ */
export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQuery> { export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQuery> {
constructor(instanceSettings: DataSourceInstanceSettings) { constructor(public instanceSettings: DataSourceInstanceSettings) {
super(instanceSettings); super(instanceSettings);
} }
@ -19,7 +19,7 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQue
return { return {
refId: '--', // Replaced with query refId: '--', // Replaced with query
type: query?.type ?? ExpressionQueryType.math, type: query?.type ?? ExpressionQueryType.math,
datasource: ExpressionDatasourceID, datasource: ExpressionDatasourceRef,
conditions: query?.conditions ?? undefined, conditions: query?.conditions ?? undefined,
}; };
} }
@ -28,6 +28,10 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQue
// MATCHES the constant in DataSourceWithBackend // MATCHES the constant in DataSourceWithBackend
export const ExpressionDatasourceID = '__expr__'; export const ExpressionDatasourceID = '__expr__';
export const ExpressionDatasourceUID = '-100'; export const ExpressionDatasourceUID = '-100';
export const ExpressionDatasourceRef = Object.freeze({
type: ExpressionDatasourceID,
uid: ExpressionDatasourceID,
});
export const instanceSettings: DataSourceInstanceSettings = { export const instanceSettings: DataSourceInstanceSettings = {
id: -100, id: -100,

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Input, Field, Button, ValuePicker, HorizontalGroup } from '@grafana/ui'; import { Input, Field, Button, ValuePicker, HorizontalGroup } from '@grafana/ui';
import { DataSourcePicker, getBackendSrv } from '@grafana/runtime'; 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 appEvents from 'app/core/app_events';
import { Rule } from './types'; import { Rule } from './types';
@ -28,7 +28,7 @@ export function AddNewRule({ onRuleAdded }: Props) {
const [patternType, setPatternType] = useState<PatternType>(); const [patternType, setPatternType] = useState<PatternType>();
const [pattern, setPattern] = useState<string>(); const [pattern, setPattern] = useState<string>();
const [patternPrefix, setPatternPrefix] = useState<string>(''); const [patternPrefix, setPatternPrefix] = useState<string>('');
const [datasource, setDatasource] = useState<DatasourceRef>(); const [datasource, setDatasource] = useState<DataSourceRef>();
const onSubmit = () => { const onSubmit = () => {
if (!pattern) { if (!pattern) {
@ -85,7 +85,7 @@ export function AddNewRule({ onRuleAdded }: Props) {
<DataSourcePicker <DataSourcePicker
current={datasource} current={datasource}
onChange={(ds) => { onChange={(ds) => {
setDatasource(ds.name); setDatasource(ds);
setPatternPrefix(`${LiveChannelScope.DataSource}/${ds.uid}/`); setPatternPrefix(`${LiveChannelScope.DataSource}/${ds.uid}/`);
}} }}
/> />

View File

@ -9,7 +9,14 @@ import {
TemplateSrv, TemplateSrv,
} from '@grafana/runtime'; } from '@grafana/runtime';
// Types // 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 { auto } from 'angular';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
// Pretend Datasource // Pretend Datasource
@ -23,11 +30,11 @@ import { DataSourceVariableModel } from '../variables/types';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
export class DatasourceSrv implements DataSourceService { export class DatasourceSrv implements DataSourceService {
private datasources: Record<string, DataSourceApi> = {}; private datasources: Record<string, DataSourceApi> = {}; // UID
private settingsMapByName: Record<string, DataSourceInstanceSettings> = {}; private settingsMapByName: Record<string, DataSourceInstanceSettings> = {};
private settingsMapByUid: Record<string, DataSourceInstanceSettings> = {}; private settingsMapByUid: Record<string, DataSourceInstanceSettings> = {};
private settingsMapById: Record<string, DataSourceInstanceSettings> = {}; private settingsMapById: Record<string, DataSourceInstanceSettings> = {};
private defaultName = ''; private defaultName = ''; // actually UID
/** @ngInject */ /** @ngInject */
constructor( constructor(
@ -43,22 +50,39 @@ export class DatasourceSrv implements DataSourceService {
this.defaultName = defaultName; this.defaultName = defaultName;
for (const dsSettings of Object.values(settingsMapByName)) { for (const dsSettings of Object.values(settingsMapByName)) {
if (!dsSettings.uid) {
dsSettings.uid = dsSettings.name; // -- Grafana --, -- Mixed etc
}
this.settingsMapByUid[dsSettings.uid] = dsSettings; this.settingsMapByUid[dsSettings.uid] = dsSettings;
this.settingsMapById[dsSettings.id] = 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 { getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined {
return this.settingsMapByUid[uid]; return this.settingsMapByUid[uid];
} }
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined { getInstanceSettings(ref: string | null | undefined | DataSourceRef): DataSourceInstanceSettings | undefined {
if (nameOrUid === 'default' || nameOrUid === null || nameOrUid === undefined) { const isstring = typeof ref === 'string';
return this.settingsMapByName[this.defaultName]; let nameOrUid = isstring ? (ref as string) : ((ref as any)?.uid as string | undefined);
}
if (nameOrUid === ExpressionDatasourceID || nameOrUid === ExpressionDatasourceUID) { if (nameOrUid === 'default' || nameOrUid === null || nameOrUid === undefined) {
return expressionInstanceSettings; 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 // 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]; 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) { if (!nameOrUid) {
return this.get(this.defaultName); return this.get(this.defaultName);
} }
// Check if nameOrUid matches a uid and then get the name // Check if nameOrUid matches a uid and then get the name
const byUid = this.settingsMapByUid[nameOrUid]; const byName = this.settingsMapByName[nameOrUid];
if (byUid) { if (byName) {
nameOrUid = byUid.name; nameOrUid = byName.uid;
} }
// This check is duplicated below, this is here mainly as performance optimization to skip interpolation // 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); return this.loadDatasource(nameOrUid);
} }
async loadDatasource(name: string): Promise<DataSourceApi<any, any>> { async loadDatasource(key: string): Promise<DataSourceApi<any, any>> {
// Expression Datasource (not a real datasource) if (this.datasources[key]) {
if (name === ExpressionDatasourceID || name === ExpressionDatasourceUID) { return Promise.resolve(this.datasources[key]);
this.datasources[name] = expressionDatasource as any;
return Promise.resolve(expressionDatasource);
} }
let dsConfig = this.settingsMapByName[name]; // find the metadata
const dsConfig = this.settingsMapByUid[key] ?? this.settingsMapByName[key] ?? this.settingsMapById[key];
if (!dsConfig) { if (!dsConfig) {
dsConfig = this.settingsMapById[name]; return Promise.reject({ message: `Datasource ${key} was not found` });
if (!dsConfig) {
return Promise.reject({ message: `Datasource named ${name} was not found` });
}
} }
try { try {
const dsPlugin = await importDataSourcePlugin(dsConfig.meta); const dsPlugin = await importDataSourcePlugin(dsConfig.meta);
// check if its in cache now // check if its in cache now
if (this.datasources[name]) { if (this.datasources[key]) {
return this.datasources[name]; return this.datasources[key];
} }
// If there is only one constructor argument it is instanceSettings // If there is only one constructor argument it is instanceSettings
@ -153,11 +174,14 @@ export class DatasourceSrv implements DataSourceService {
instance.meta = dsConfig.meta; instance.meta = dsConfig.meta;
// store in instance cache // store in instance cache
this.datasources[name] = instance; this.datasources[key] = instance;
this.datasources[instance.uid] = instance;
return instance; return instance;
} catch (err) { } catch (err) {
this.$rootScope.appEvent(AppEvents.alertError, [dsConfig.name + ' plugin failed', err.toString()]); if (this.$rootScope) {
return Promise.reject({ message: `Datasource named ${name} was not found` }); this.$rootScope.appEvent(AppEvents.alertError, [dsConfig.name + ' plugin failed', err.toString()]);
}
return Promise.reject({ message: `Datasource: ${key} was not found` });
} }
} }

View File

@ -221,6 +221,7 @@ describe('datasource_srv', () => {
}, },
"name": "-- Mixed --", "name": "-- Mixed --",
"type": "test-db", "type": "test-db",
"uid": "-- Mixed --",
}, },
Object { Object {
"meta": Object { "meta": Object {
@ -230,6 +231,7 @@ describe('datasource_srv', () => {
}, },
"name": "-- Dashboard --", "name": "-- Dashboard --",
"type": "dashboard", "type": "dashboard",
"uid": "-- Dashboard --",
}, },
Object { Object {
"meta": Object { "meta": Object {
@ -239,6 +241,7 @@ describe('datasource_srv', () => {
}, },
"name": "-- Grafana --", "name": "-- Grafana --",
"type": "grafana", "type": "grafana",
"uid": "-- Grafana --",
}, },
] ]
`); `);

View File

@ -121,7 +121,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
getQueryDataSourceIdentifier(): string | null | undefined { getQueryDataSourceIdentifier(): string | null | undefined {
const { query, dataSource: dsSettings } = this.props; const { query, dataSource: dsSettings } = this.props;
return query.datasource ?? dsSettings.name; return query.datasource?.uid ?? dsSettings.uid;
} }
async loadDatasource() { async loadDatasource() {

View File

@ -3,12 +3,20 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { Props, QueryEditorRowHeader } from './QueryEditorRowHeader'; import { Props, QueryEditorRowHeader } from './QueryEditorRowHeader';
import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceInstanceSettings } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; 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', () => { jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
return { return {
getDataSourceSrv: () => ({ getDataSourceSrv: () => ({
getInstanceSettings: jest.fn(), get: () => Promise.resolve(mockDS),
getList: jest.fn().mockReturnValue([]), getList: () => [mockDS],
getInstanceSettings: () => mockDS,
}), }),
}; };
}); });

View File

@ -67,7 +67,7 @@ export class QueryEditorRows extends PureComponent<Props> {
if (previous?.type === dataSource.type) { if (previous?.type === dataSource.type) {
return { return {
...item, ...item,
datasource: dataSource.name, datasource: { uid: dataSource.uid },
}; };
} }
} }
@ -75,7 +75,7 @@ export class QueryEditorRows extends PureComponent<Props> {
return { return {
refId: item.refId, refId: item.refId,
hide: item.hide, hide: item.hide,
datasource: dataSource.name, datasource: { uid: dataSource.uid },
}; };
}) })
); );

View File

@ -22,6 +22,7 @@ import {
DataQuery, DataQuery,
DataSourceApi, DataSourceApi,
DataSourceInstanceSettings, DataSourceInstanceSettings,
DataSourceRef,
getDefaultTimeRange, getDefaultTimeRange,
LoadingState, LoadingState,
PanelData, PanelData,
@ -91,7 +92,8 @@ export class QueryGroup extends PureComponent<Props, State> {
const ds = await this.dataSourceSrv.get(options.dataSource.name); const ds = await this.dataSourceSrv.get(options.dataSource.name);
const dsSettings = this.dataSourceSrv.getInstanceSettings(options.dataSource.name); const dsSettings = this.dataSourceSrv.getInstanceSettings(options.dataSource.name);
const defaultDataSource = await this.dataSourceSrv.get(); 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 }); this.setState({ queries, dataSource: ds, dsSettings, defaultDataSource });
} catch (error) { } catch (error) {
console.log('failed to load data source', error); console.log('failed to load data source', error);
@ -119,6 +121,7 @@ export class QueryGroup extends PureComponent<Props, State> {
dataSource: { dataSource: {
name: newSettings.name, name: newSettings.name,
uid: newSettings.uid, uid: newSettings.uid,
type: newSettings.meta.id,
default: newSettings.isDefault, default: newSettings.isDefault,
}, },
}); });
@ -139,12 +142,10 @@ export class QueryGroup extends PureComponent<Props, State> {
newQuery(): Partial<DataQuery> { newQuery(): Partial<DataQuery> {
const { dsSettings, defaultDataSource } = this.state; const { dsSettings, defaultDataSource } = this.state;
if (!dsSettings?.meta.mixed) { const ds = !dsSettings?.meta.mixed ? dsSettings : defaultDataSource;
return { datasource: dsSettings?.name };
}
return { 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}> <div className={styles.dataSourceRowItem}>
<DataSourcePicker <DataSourcePicker
onChange={this.onChangeDataSource} onChange={this.onChangeDataSource}
current={options.dataSource.name} current={options.dataSource}
metrics={true} metrics={true}
mixed={true} mixed={true}
dashboard={true} dashboard={true}
@ -258,7 +259,7 @@ export class QueryGroup extends PureComponent<Props, State> {
onAddQuery = (query: Partial<DataQuery>) => { onAddQuery = (query: Partial<DataQuery>) => {
const { dsSettings, queries } = this.state; 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(); this.onScrollBottom();
}; };

View File

@ -108,6 +108,7 @@ function describeQueryRunnerScenario(
const datasource: any = { const datasource: any = {
name: 'TestDB', name: 'TestDB',
uid: 'TestDB-uid',
interval: ctx.dsInterval, interval: ctx.dsInterval,
query: (options: grafanaData.DataQueryRequest) => { query: (options: grafanaData.DataQueryRequest) => {
ctx.queryCalledWith = options; ctx.queryCalledWith = options;
@ -156,8 +157,8 @@ describe('PanelQueryRunner', () => {
expect(ctx.queryCalledWith?.requestId).toBe('Q100'); expect(ctx.queryCalledWith?.requestId).toBe('Q100');
}); });
it('should set datasource name on request', async () => { it('should set datasource uid on request', async () => {
expect(ctx.queryCalledWith?.targets[0].datasource).toBe('TestDB'); expect(ctx.queryCalledWith?.targets[0].datasource?.uid).toBe('TestDB-uid');
}); });
it('should pass scopedVars to datasource with interval props', async () => { it('should pass scopedVars to datasource with interval props', async () => {

View File

@ -21,6 +21,7 @@ import {
DataQueryRequest, DataQueryRequest,
DataSourceApi, DataSourceApi,
DataSourceJsonData, DataSourceJsonData,
DataSourceRef,
DataTransformerConfig, DataTransformerConfig,
LoadingState, LoadingState,
PanelData, PanelData,
@ -38,7 +39,7 @@ export interface QueryRunnerOptions<
TQuery extends DataQuery = DataQuery, TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData TOptions extends DataSourceJsonData = DataSourceJsonData
> { > {
datasource: string | DataSourceApi<TQuery, TOptions> | null; datasource: DataSourceRef | DataSourceApi<TQuery, TOptions> | null;
queries: TQuery[]; queries: TQuery[];
panelId?: number; panelId?: number;
dashboardId?: number; dashboardId?: number;
@ -223,7 +224,7 @@ export class PanelQueryRunner {
// Attach the data source name to each query // Attach the data source name to each query
request.targets = request.targets.map((query) => { request.targets = request.targets.map((query) => {
if (!query.datasource) { if (!query.datasource) {
query.datasource = ds.name; query.datasource = { uid: ds.uid };
} }
return query; return query;
}); });
@ -326,7 +327,7 @@ export class PanelQueryRunner {
} }
async function getDataSource( async function getDataSource(
datasource: string | DataSourceApi | null, datasource: DataSourceRef | string | DataSourceApi | null,
scopedVars: ScopedVars scopedVars: ScopedVars
): Promise<DataSourceApi> { ): Promise<DataSourceApi> {
if (datasource && (datasource as any).query) { if (datasource && (datasource as any).query) {

View File

@ -8,6 +8,7 @@ import {
QueryRunnerOptions, QueryRunnerOptions,
QueryRunner as QueryRunnerSrv, QueryRunner as QueryRunnerSrv,
LoadingState, LoadingState,
DataSourceRef,
} from '@grafana/data'; } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime'; import { getTemplateSrv } from '@grafana/runtime';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
@ -78,7 +79,7 @@ export class QueryRunner implements QueryRunnerSrv {
// Attach the datasource name to each query // Attach the datasource name to each query
request.targets = request.targets.map((query) => { request.targets = request.targets.map((query) => {
if (!query.datasource) { if (!query.datasource) {
query.datasource = ds.name; query.datasource = ds.getRef();
} }
return query; return query;
}); });
@ -140,11 +141,11 @@ export class QueryRunner implements QueryRunnerSrv {
} }
async function getDataSource( async function getDataSource(
datasource: string | DataSourceApi | null, datasource: DataSourceRef | DataSourceApi | null,
scopedVars: ScopedVars scopedVars: ScopedVars
): Promise<DataSourceApi> { ): Promise<DataSourceApi> {
if (datasource && (datasource as any).query) { if (datasource && (datasource as any).query) {
return datasource as DataSourceApi; return datasource as DataSourceApi;
} }
return await getDatasourceSrv().get(datasource as string, scopedVars); return await getDatasourceSrv().get(datasource, scopedVars);
} }

View File

@ -32,7 +32,7 @@ export const TestStuffPage: FC = () => {
queryRunner.run({ queryRunner.run({
queries: queryOptions.queries, queries: queryOptions.queries,
datasource: queryOptions.dataSource.name!, datasource: queryOptions.dataSource,
timezone: 'browser', timezone: 'browser',
timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange }, timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange },
maxDataPoints: queryOptions.maxDataPoints ?? 100, maxDataPoints: queryOptions.maxDataPoints ?? 100,

View File

@ -6,6 +6,8 @@ import { createQueryVariableAdapter } from '../variables/query/adapter';
import { createAdHocVariableAdapter } from '../variables/adhoc/adapter'; import { createAdHocVariableAdapter } from '../variables/adhoc/adapter';
import { VariableModel } from '../variables/types'; import { VariableModel } from '../variables/types';
import { FormatRegistryID } from './formatRegistry'; import { FormatRegistryID } from './formatRegistry';
import { setDataSourceSrv } from '@grafana/runtime';
import { mockDataSource, MockDataSourceSrv } from '../alerting/unified/mocks';
variableAdapters.setInit(() => [ variableAdapters.setInit(() => [
(createQueryVariableAdapter() as unknown) as VariableAdapter<VariableModel>, (createQueryVariableAdapter() as unknown) as VariableAdapter<VariableModel>,
@ -119,9 +121,17 @@ describe('templateSrv', () => {
name: 'ds', name: 'ds',
current: { value: 'logstash', text: 'logstash' }, current: { value: 'logstash', text: 'logstash' },
}, },
{ type: 'adhoc', name: 'test', datasource: 'oogle', filters: [1] }, { type: 'adhoc', name: 'test', datasource: { uid: 'oogle' }, filters: [1] },
{ type: 'adhoc', name: 'test2', datasource: '$ds', filters: [2] }, { 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', () => { it('should return filters if datasourceName match', () => {

View File

@ -3,8 +3,8 @@ import { deprecationWarning, ScopedVars, TimeRange } from '@grafana/data';
import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors'; import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors';
import { variableRegex } from '../variables/utils'; import { variableRegex } from '../variables/utils';
import { isAdHoc } from '../variables/guard'; import { isAdHoc } from '../variables/guard';
import { VariableModel } from '../variables/types'; import { AdHocVariableFilter, AdHocVariableModel, VariableModel } from '../variables/types';
import { setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime'; import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
import { FormatOptions, formatRegistry, FormatRegistryID } from './formatRegistry'; import { FormatOptions, formatRegistry, FormatRegistryID } from './formatRegistry';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/state/types'; import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/state/types';
import { safeStringifyValue } from '../../core/utils/explore'; import { safeStringifyValue } from '../../core/utils/explore';
@ -92,14 +92,21 @@ export class TemplateSrv implements BaseTemplateSrv {
this.index[variable.name] = variable; this.index[variable.name] = variable;
} }
getAdhocFilters(datasourceName: string) { getAdhocFilters(datasourceName: string): AdHocVariableFilter[] {
let filters: any = []; let filters: any = [];
let ds = getDataSourceSrv().getInstanceSettings(datasourceName);
if (!ds) {
return [];
}
for (const variable of this.getAdHocVariables()) { 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); filters = filters.concat(variable.filters);
} else if (variable.datasource.indexOf('$') === 0) { } else if (variableUid?.indexOf('$') === 0) {
if (this.replace(variable.datasource) === datasourceName) { if (this.replace(variableUid) === datasourceName) {
filters = filters.concat(variable.filters); filters = filters.concat(variable.filters);
} }
} }
@ -334,8 +341,8 @@ export class TemplateSrv implements BaseTemplateSrv {
return this.index[name]; return this.index[name];
} }
private getAdHocVariables(): any[] { private getAdHocVariables(): AdHocVariableModel[] {
return this.dependencies.getFilteredVariables(isAdHoc); return this.dependencies.getFilteredVariables(isAdHoc) as AdHocVariableModel[];
} }
} }

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import { Alert, InlineFieldRow, VerticalGroup } from '@grafana/ui'; import { Alert, InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { SelectableValue } from '@grafana/data'; import { DataSourceRef, SelectableValue } from '@grafana/data';
import { AdHocVariableModel } from '../types'; import { AdHocVariableModel } from '../types';
import { VariableEditorProps } from '../editor/types'; import { VariableEditorProps } from '../editor/types';
@ -32,7 +32,7 @@ export class AdHocVariableEditorUnConnected extends PureComponent<Props> {
this.props.initAdHocVariableEditor(); this.props.initAdHocVariableEditor();
} }
onDatasourceChanged = (option: SelectableValue<string>) => { onDatasourceChanged = (option: SelectableValue<DataSourceRef>) => {
this.props.changeVariableDatasource(option.value); this.props.changeVariableDatasource(option.value);
}; };
@ -40,7 +40,7 @@ export class AdHocVariableEditorUnConnected extends PureComponent<Props> {
const { variable, editor } = this.props; const { variable, editor } = this.props;
const dataSources = editor.extended?.dataSources ?? []; const dataSources = editor.extended?.dataSources ?? [];
const infoText = editor.extended?.infoText ?? null; 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]; const value = options.find((o) => o.value === variable.datasource) ?? options[0];
return ( return (

View File

@ -39,7 +39,7 @@ describe('adhoc actions', () => {
describe('when applyFilterFromTable is dispatched and filter already exist', () => { describe('when applyFilterFromTable is dispatched and filter already exist', () => {
it('then correct actions are dispatched', async () => { it('then correct actions are dispatched', async () => {
const options: AdHocTableOptions = { const options: AdHocTableOptions = {
datasource: 'influxdb', datasource: { uid: 'influxdb' },
key: 'filter-key', key: 'filter-key',
value: 'filter-value', value: 'filter-value',
operator: '=', operator: '=',
@ -76,7 +76,7 @@ describe('adhoc actions', () => {
describe('when applyFilterFromTable is dispatched and previously no variable or filter exists', () => { describe('when applyFilterFromTable is dispatched and previously no variable or filter exists', () => {
it('then correct actions are dispatched', async () => { it('then correct actions are dispatched', async () => {
const options: AdHocTableOptions = { const options: AdHocTableOptions = {
datasource: 'influxdb', datasource: { uid: 'influxdb' },
key: 'filter-key', key: 'filter-key',
value: 'filter-value', value: 'filter-value',
operator: '=', operator: '=',
@ -103,7 +103,7 @@ describe('adhoc actions', () => {
describe('when applyFilterFromTable is dispatched and previously no filter exists', () => { describe('when applyFilterFromTable is dispatched and previously no filter exists', () => {
it('then correct actions are dispatched', async () => { it('then correct actions are dispatched', async () => {
const options: AdHocTableOptions = { const options: AdHocTableOptions = {
datasource: 'influxdb', datasource: { uid: 'influxdb' },
key: 'filter-key', key: 'filter-key',
value: 'filter-value', value: 'filter-value',
operator: '=', operator: '=',
@ -132,7 +132,7 @@ describe('adhoc actions', () => {
describe('when applyFilterFromTable is dispatched and adhoc variable with other datasource exists', () => { describe('when applyFilterFromTable is dispatched and adhoc variable with other datasource exists', () => {
it('then correct actions are dispatched', async () => { it('then correct actions are dispatched', async () => {
const options: AdHocTableOptions = { const options: AdHocTableOptions = {
datasource: 'influxdb', datasource: { uid: 'influxdb' },
key: 'filter-key', key: 'filter-key',
value: 'filter-value', value: 'filter-value',
operator: '=', operator: '=',
@ -141,7 +141,7 @@ describe('adhoc actions', () => {
const existing = adHocBuilder() const existing = adHocBuilder()
.withId('elastic-filter') .withId('elastic-filter')
.withName('elastic-filter') .withName('elastic-filter')
.withDatasource('elasticsearch') .withDatasource({ uid: 'elasticsearch' })
.build(); .build();
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource(options.datasource).build(); const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource(options.datasource).build();
@ -181,7 +181,7 @@ describe('adhoc actions', () => {
.withId('elastic-filter') .withId('elastic-filter')
.withName('elastic-filter') .withName('elastic-filter')
.withFilters([existing]) .withFilters([existing])
.withDatasource('elasticsearch') .withDatasource({ uid: 'elasticsearch' })
.build(); .build();
const update = { index: 0, filter: updated }; const update = { index: 0, filter: updated };
@ -218,7 +218,7 @@ describe('adhoc actions', () => {
.withId('elastic-filter') .withId('elastic-filter')
.withName('elastic-filter') .withName('elastic-filter')
.withFilters([existing]) .withFilters([existing])
.withDatasource('elasticsearch') .withDatasource({ uid: 'elasticsearch' })
.build(); .build();
const tester = await reduxTester<RootReducerType>() const tester = await reduxTester<RootReducerType>()
@ -247,7 +247,7 @@ describe('adhoc actions', () => {
.withId('elastic-filter') .withId('elastic-filter')
.withName('elastic-filter') .withName('elastic-filter')
.withFilters([]) .withFilters([])
.withDatasource('elasticsearch') .withDatasource({ uid: 'elasticsearch' })
.build(); .build();
const tester = await reduxTester<RootReducerType>() const tester = await reduxTester<RootReducerType>()
@ -268,7 +268,7 @@ describe('adhoc actions', () => {
.withId('elastic-filter') .withId('elastic-filter')
.withName('elastic-filter') .withName('elastic-filter')
.withFilters([]) .withFilters([])
.withDatasource('elasticsearch') .withDatasource({ uid: 'elasticsearch' })
.build(); .build();
const tester = await reduxTester<RootReducerType>() const tester = await reduxTester<RootReducerType>()
@ -296,7 +296,7 @@ describe('adhoc actions', () => {
.withId('elastic-filter') .withId('elastic-filter')
.withName('elastic-filter') .withName('elastic-filter')
.withFilters([filter]) .withFilters([filter])
.withDatasource('elasticsearch') .withDatasource({ uid: 'elasticsearch' })
.build(); .build();
const tester = await reduxTester<RootReducerType>() const tester = await reduxTester<RootReducerType>()
@ -324,7 +324,7 @@ describe('adhoc actions', () => {
.withId('elastic-filter') .withId('elastic-filter')
.withName('elastic-filter') .withName('elastic-filter')
.withFilters([existing]) .withFilters([existing])
.withDatasource('elasticsearch') .withDatasource({ uid: 'elasticsearch' })
.build(); .build();
const fromUrl = [ const fromUrl = [
@ -382,9 +382,9 @@ describe('adhoc actions', () => {
describe('when changeVariableDatasource is dispatched with unsupported datasource', () => { describe('when changeVariableDatasource is dispatched with unsupported datasource', () => {
it('then correct actions are dispatched', async () => { 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 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.mockRestore();
getDatasource.mockResolvedValue(null); getDatasource.mockResolvedValue(null);
@ -408,9 +408,9 @@ describe('adhoc actions', () => {
describe('when changeVariableDatasource is dispatched with datasource', () => { describe('when changeVariableDatasource is dispatched with datasource', () => {
it('then correct actions are dispatched', async () => { 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 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.mockRestore();
getDatasource.mockResolvedValue({ getDatasource.mockResolvedValue({

View File

@ -16,9 +16,10 @@ import {
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types'; import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types';
import { variableUpdated } from '../state/actions'; import { variableUpdated } from '../state/actions';
import { isAdHoc } from '../guard'; import { isAdHoc } from '../guard';
import { DataSourceRef } from '@grafana/data';
export interface AdHocTableOptions { export interface AdHocTableOptions {
datasource: string; datasource: DataSourceRef;
key: string; key: string;
value: string; value: string;
operator: string; operator: string;
@ -29,6 +30,7 @@ const filterTableName = 'Filters';
export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult<void> => { export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult<void> => {
return async (dispatch, getState) => { return async (dispatch, getState) => {
let variable = getVariableByOptions(options, getState()); let variable = getVariableByOptions(options, getState());
console.log('getVariableByOptions', options, getState().templating.variables);
if (!variable) { if (!variable) {
dispatch(createAdHocVariable(options)); 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) => { return async (dispatch, getState) => {
const { editor } = getState().templating; const { editor } = getState().templating;
const variable = getVariable(editor.id, getState()); const variable = getVariable(editor.id, getState());
@ -155,6 +157,6 @@ const createAdHocVariable = (options: AdHocTableOptions): ThunkResult<void> => {
const getVariableByOptions = (options: AdHocTableOptions, state: StoreState): AdHocVariableModel => { const getVariableByOptions = (options: AdHocTableOptions, state: StoreState): AdHocVariableModel => {
return Object.values(state.templating.variables).find( 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; ) as AdHocVariableModel;
}; };

View File

@ -1,11 +1,11 @@
import React, { FC, useCallback, useState } from 'react'; import React, { FC, useCallback, useState } from 'react';
import { AdHocVariableFilter } from 'app/features/variables/types'; 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 { AdHocFilterKey, REMOVE_FILTER_KEY } from './AdHocFilterKey';
import { AdHocFilterRenderer } from './AdHocFilterRenderer'; import { AdHocFilterRenderer } from './AdHocFilterRenderer';
interface Props { interface Props {
datasource: string; datasource: DataSourceRef;
onCompleted: (filter: AdHocVariableFilter) => void; onCompleted: (filter: AdHocVariableFilter) => void;
appendBefore?: React.ReactNode; appendBefore?: React.ReactNode;
} }

View File

@ -1,10 +1,10 @@
import React, { FC, ReactElement } from 'react'; import React, { FC, ReactElement } from 'react';
import { Icon, SegmentAsync } from '@grafana/ui'; import { Icon, SegmentAsync } from '@grafana/ui';
import { getDatasourceSrv } from '../../../plugins/datasource_srv'; import { getDatasourceSrv } from '../../../plugins/datasource_srv';
import { SelectableValue } from '@grafana/data'; import { DataSourceRef, SelectableValue } from '@grafana/data';
interface Props { interface Props {
datasource: string; datasource: DataSourceRef;
filterKey: string | null; filterKey: string | null;
onChange: (item: SelectableValue<string | null>) => void; onChange: (item: SelectableValue<string | null>) => void;
} }
@ -51,7 +51,7 @@ const plusSegment: ReactElement = (
</a> </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); const ds = await getDatasourceSrv().get(datasource);
if (!ds || !ds.getTagKeys) { 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 })); 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); const keys = await fetchFilterKeys(datasource);
return [REMOVE_VALUE, ...keys]; return [REMOVE_VALUE, ...keys];
}; };

View File

@ -1,12 +1,12 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { OperatorSegment } from './OperatorSegment'; import { OperatorSegment } from './OperatorSegment';
import { AdHocVariableFilter } from 'app/features/variables/types'; import { AdHocVariableFilter } from 'app/features/variables/types';
import { SelectableValue } from '@grafana/data'; import { DataSourceRef, SelectableValue } from '@grafana/data';
import { AdHocFilterKey } from './AdHocFilterKey'; import { AdHocFilterKey } from './AdHocFilterKey';
import { AdHocFilterValue } from './AdHocFilterValue'; import { AdHocFilterValue } from './AdHocFilterValue';
interface Props { interface Props {
datasource: string; datasource: DataSourceRef;
filter: AdHocVariableFilter; filter: AdHocVariableFilter;
onKeyChange: (item: SelectableValue<string | null>) => void; onKeyChange: (item: SelectableValue<string | null>) => void;
onOperatorChange: (item: SelectableValue<string>) => void; onOperatorChange: (item: SelectableValue<string>) => void;

View File

@ -1,10 +1,10 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { SegmentAsync } from '@grafana/ui'; import { SegmentAsync } from '@grafana/ui';
import { getDatasourceSrv } from '../../../plugins/datasource_srv'; import { getDatasourceSrv } from '../../../plugins/datasource_srv';
import { MetricFindValue, SelectableValue } from '@grafana/data'; import { DataSourceRef, MetricFindValue, SelectableValue } from '@grafana/data';
interface Props { interface Props {
datasource: string; datasource: DataSourceRef;
filterKey: string; filterKey: string;
filterValue?: string; filterValue?: string;
onChange: (item: SelectableValue<string>) => void; 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); const ds = await getDatasourceSrv().get(datasource);
if (!ds || !ds.getTagValues) { if (!ds || !ds.getTagValues) {

View File

@ -9,7 +9,8 @@ import { initialVariableEditorState } from '../editor/reducer';
import { describe, expect } from '../../../../test/lib/common'; import { describe, expect } from '../../../../test/lib/common';
import { NEW_VARIABLE_ID } from '../state/types'; import { NEW_VARIABLE_ID } from '../state/types';
import { LegacyVariableQueryEditor } from '../editor/LegacyVariableQueryEditor'; 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 setupTestContext = (options: Partial<Props>) => {
const defaults: Props = { const defaults: Props = {
@ -34,10 +35,20 @@ const setupTestContext = (options: Partial<Props>) => {
return { rerender, props }; return { rerender, props };
}; };
setDataSourceSrv({ const mockDS = mockDataSource({
getInstanceSettings: () => null, name: 'CloudManager',
getList: () => [], type: DataSourceType.Alertmanager,
} as any); });
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
return {
getDataSourceSrv: () => ({
get: () => Promise.resolve(mockDS),
getList: () => [mockDS],
getInstanceSettings: () => mockDS,
}),
};
});
describe('QueryVariableEditor', () => { describe('QueryVariableEditor', () => {
describe('when the component is mounted', () => { describe('when the component is mounted', () => {

View File

@ -1,8 +1,9 @@
import { DataSourceRef } from '@grafana/data';
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types'; import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types';
import { VariableBuilder } from './variableBuilder'; import { VariableBuilder } from './variableBuilder';
export class AdHocVariableBuilder extends VariableBuilder<AdHocVariableModel> { export class AdHocVariableBuilder extends VariableBuilder<AdHocVariableModel> {
withDatasource(datasource: string) { withDatasource(datasource: DataSourceRef) {
this.variable.datasource = datasource; this.variable.datasource = datasource;
return this; return this;
} }

View File

@ -2,6 +2,7 @@ import { ComponentType } from 'react';
import { import {
DataQuery, DataQuery,
DataSourceJsonData, DataSourceJsonData,
DataSourceRef,
LoadingState, LoadingState,
QueryEditorProps, QueryEditorProps,
VariableModel as BaseVariableModel, VariableModel as BaseVariableModel,
@ -48,7 +49,7 @@ export interface AdHocVariableFilter {
} }
export interface AdHocVariableModel extends VariableModel { export interface AdHocVariableModel extends VariableModel {
datasource: string | null; datasource: DataSourceRef | null;
filters: AdHocVariableFilter[]; filters: AdHocVariableFilter[];
} }

View File

@ -6,17 +6,18 @@ import {
DataQuery, DataQuery,
DataQueryRequest, DataQueryRequest,
DataSourceApi, DataSourceApi,
DataSourceRef,
getDefaultTimeRange, getDefaultTimeRange,
LoadingState, LoadingState,
PanelData, PanelData,
} from '@grafana/data'; } from '@grafana/data';
export function isSharedDashboardQuery(datasource: string | DataSourceApi | null) { export function isSharedDashboardQuery(datasource: string | DataSourceRef | DataSourceApi | null) {
if (!datasource) { if (!datasource) {
// default datasource // default datasource
return false; return false;
} }
if (datasource === SHARED_DASHBODARD_QUERY) { if (datasource === SHARED_DASHBODARD_QUERY || (datasource as any)?.uid === SHARED_DASHBODARD_QUERY) {
return true; return true;
} }
const ds = datasource as DataSourceApi; const ds = datasource as DataSourceApi;

View File

@ -385,7 +385,7 @@ export class ElasticDatasource
const expandedQueries = queries.map( const expandedQueries = queries.map(
(query): ElasticsearchQuery => ({ (query): ElasticsearchQuery => ({
...query, ...query,
datasource: this.name, datasource: this.getRef(),
query: this.interpolateLuceneQuery(query.query || '', scopedVars), query: this.interpolateLuceneQuery(query.query || '', scopedVars),
bucketAggs: query.bucketAggs?.map(interpolateBucketAgg), bucketAggs: query.bucketAggs?.map(interpolateBucketAgg),
}) })

View File

@ -7,7 +7,7 @@ import {
DataQueryRequest, DataQueryRequest,
DataQueryResponse, DataQueryResponse,
DataSourceInstanceSettings, DataSourceInstanceSettings,
DatasourceRef, DataSourceRef,
isValidLiveChannelAddress, isValidLiveChannelAddress,
parseLiveChannelAddress, parseLiveChannelAddress,
StreamingFrameOptions, StreamingFrameOptions,
@ -18,6 +18,7 @@ import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery, GrafanaQue
import AnnotationQueryEditor from './components/AnnotationQueryEditor'; import AnnotationQueryEditor from './components/AnnotationQueryEditor';
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv'; import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
import { isString } from 'lodash'; import { isString } from 'lodash';
import { migrateDatasourceNameToRef } from 'app/features/dashboard/state/DashboardMigrator';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
let counter = 100; let counter = 100;
@ -39,10 +40,16 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
return json; return json;
}, },
prepareQuery(anno: AnnotationQuery<GrafanaAnnotationQuery>): GrafanaQuery { prepareQuery(anno: AnnotationQuery<GrafanaAnnotationQuery>): GrafanaQuery {
let datasource: DatasourceRef | undefined | null = undefined; let datasource: DataSourceRef | undefined | null = undefined;
if (isString(anno.datasource)) { 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 }; return { ...anno, refId: anno.name, queryType: GrafanaQueryType.Annotations, datasource };
}, },
}; };

View File

@ -231,7 +231,7 @@ export class GraphiteDatasource extends DataSourceApi<
expandedQueries = queries.map((query) => { expandedQueries = queries.map((query) => {
const expandedQuery = { const expandedQuery = {
...query, ...query,
datasource: this.name, datasource: this.getRef(),
target: this.templateSrv.replace(query.target ?? '', scopedVars), target: this.templateSrv.replace(query.target ?? '', scopedVars),
}; };
return expandedQuery; return expandedQuery;

View File

@ -342,7 +342,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
expandedQueries = queries.map((query) => { expandedQueries = queries.map((query) => {
const expandedQuery = { const expandedQuery = {
...query, ...query,
datasource: this.name, datasource: this.getRef(),
measurement: this.templateSrv.replace(query.measurement ?? '', scopedVars, 'regex'), measurement: this.templateSrv.replace(query.measurement ?? '', scopedVars, 'regex'),
policy: this.templateSrv.replace(query.policy ?? '', scopedVars, 'regex'), policy: this.templateSrv.replace(query.policy ?? '', scopedVars, 'regex'),
}; };

View File

@ -311,7 +311,7 @@ export class LokiDatasource
if (queries && queries.length) { if (queries && queries.length) {
expandedQueries = queries.map((query) => ({ expandedQueries = queries.map((query) => ({
...query, ...query,
datasource: this.name, datasource: this.getRef(),
expr: this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr), expr: this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr),
})); }));
} }

View File

@ -30,9 +30,9 @@ describe('MixedDatasource', () => {
const ds = new MixedDatasource({} as any); const ds = new MixedDatasource({} as any);
const requestMixed = getQueryOptions({ const requestMixed = getQueryOptions({
targets: [ targets: [
{ refId: 'QA', datasource: 'A' }, // 1 { refId: 'QA', datasource: { uid: 'A' } }, // 1
{ refId: 'QB', datasource: 'B' }, // 2 { refId: 'QB', datasource: { uid: 'B' } }, // 2
{ refId: 'QC', datasource: 'C' }, // 3 { refId: 'QC', datasource: { uid: 'C' } }, // 3
], ],
}); });
@ -52,11 +52,11 @@ describe('MixedDatasource', () => {
const ds = new MixedDatasource({} as any); const ds = new MixedDatasource({} as any);
const requestMixed = getQueryOptions({ const requestMixed = getQueryOptions({
targets: [ targets: [
{ refId: 'QA', datasource: 'A' }, // 1 { refId: 'QA', datasource: { uid: 'A' } }, // 1
{ refId: 'QD', datasource: 'D' }, // 2 { refId: 'QD', datasource: { uid: 'D' } }, // 2
{ refId: 'QB', datasource: 'B' }, // 3 { refId: 'QB', datasource: { uid: 'B' } }, // 3
{ refId: 'QE', datasource: 'E' }, // 4 { refId: 'QE', datasource: { uid: 'E' } }, // 4
{ refId: 'QC', datasource: 'C' }, // 5 { refId: 'QC', datasource: { uid: 'C' } }, // 5
], ],
}); });
@ -84,9 +84,9 @@ describe('MixedDatasource', () => {
const ds = new MixedDatasource({} as any); const ds = new MixedDatasource({} as any);
const request: any = { const request: any = {
targets: [ targets: [
{ refId: 'A', datasource: 'Loki' }, { refId: 'A', datasource: { uid: 'Loki' } },
{ refId: 'B', datasource: 'Loki' }, { refId: 'B', datasource: { uid: 'Loki' } },
{ refId: 'C', datasource: 'A' }, { refId: 'C', datasource: { uid: 'A' } },
], ],
}; };
@ -115,8 +115,8 @@ describe('MixedDatasource', () => {
await expect( await expect(
ds.query({ ds.query({
targets: [ targets: [
{ refId: 'QA', datasource: 'A' }, { refId: 'QA', datasource: { uid: 'A' } },
{ refId: 'QB', datasource: 'B' }, { refId: 'QB', datasource: { uid: 'B' } },
], ],
} as any) } as any)
).toEmitValuesWith((results) => { ).toEmitValuesWith((results) => {

View File

@ -26,7 +26,7 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
query(request: DataQueryRequest<DataQuery>): Observable<DataQueryResponse> { query(request: DataQueryRequest<DataQuery>): Observable<DataQueryResponse> {
// Remove any invalid queries // Remove any invalid queries
const queries = request.targets.filter((t) => { const queries = request.targets.filter((t) => {
return t.datasource !== MIXED_DATASOURCE_NAME; return t.datasource?.type !== MIXED_DATASOURCE_NAME;
}); });
if (!queries.length) { if (!queries.length) {
@ -34,19 +34,23 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
} }
// Build groups of queries to run in parallel // 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[] = []; const mixed: BatchedQueries[] = [];
for (const key in sets) { for (const key in sets) {
const targets = sets[key]; const targets = sets[key];
const dsName = targets[0].datasource;
mixed.push({ mixed.push({
datasource: getDataSourceSrv().get(dsName, request.scopedVars), datasource: getDataSourceSrv().get(targets[0].datasource, request.scopedVars),
targets, targets,
}); });
} }
// Missing UIDs?
if (!mixed.length) {
return of({ data: [] } as DataQueryResponse); // nothing
}
return this.batchQueries(mixed, request); return this.batchQueries(mixed, request);
} }

View File

@ -61,7 +61,7 @@ export class MssqlDatasource extends DataSourceWithBackend<MssqlQuery, MssqlOpti
expandedQueries = queries.map((query) => { expandedQueries = queries.map((query) => {
const expandedQuery = { const expandedQuery = {
...query, ...query,
datasource: this.name, datasource: this.getRef(),
rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable), rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable),
rawQuery: true, rawQuery: true,
}; };

View File

@ -62,7 +62,7 @@ export class MysqlDatasource extends DataSourceWithBackend<MySQLQuery, MySQLOpti
expandedQueries = queries.map((query) => { expandedQueries = queries.map((query) => {
const expandedQuery = { const expandedQuery = {
...query, ...query,
datasource: this.name, datasource: this.getRef(),
rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable), rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable),
rawQuery: true, rawQuery: true,
}; };

View File

@ -64,7 +64,7 @@ export class PostgresDatasource extends DataSourceWithBackend<PostgresQuery, Pos
expandedQueries = queries.map((query) => { expandedQueries = queries.map((query) => {
const expandedQuery = { const expandedQuery = {
...query, ...query,
datasource: this.name, datasource: this.getRef(),
rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable), rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable),
rawQuery: true, rawQuery: true,
}; };

View File

@ -780,7 +780,7 @@ export class PrometheusDatasource extends DataSourceWithBackend<PromQuery, PromO
expandedQueries = queries.map((query) => { expandedQueries = queries.map((query) => {
const expandedQuery = { const expandedQuery = {
...query, ...query,
datasource: this.name, datasource: this.getRef(),
expr: this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr), expr: this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr),
interval: this.templateSrv.replace(query.interval, scopedVars), interval: this.templateSrv.replace(query.interval, scopedVars),
}; };

View File

@ -1,4 +1,4 @@
import { DataQuery } from '@grafana/data'; import { DataQuery, DataSourceRef } from '@grafana/data';
import { ExpressionQuery } from '../features/expressions/types'; import { ExpressionQuery } from '../features/expressions/types';
export interface QueryGroupOptions { export interface QueryGroupOptions {
@ -14,8 +14,7 @@ export interface QueryGroupOptions {
}; };
} }
export interface QueryGroupDataSource { export interface QueryGroupDataSource extends DataSourceRef {
name?: string | null; name?: string | null;
uid?: string;
default?: boolean; default?: boolean;
} }

View File

@ -4,6 +4,8 @@ import {
DataSourceApi, DataSourceApi,
DataSourceInstanceSettings, DataSourceInstanceSettings,
DataSourcePluginMeta, DataSourcePluginMeta,
DataSourceRef,
getDataSourceUID,
} from '@grafana/data'; } from '@grafana/data';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@ -12,15 +14,16 @@ export class DatasourceSrvMock {
// //
} }
get(name?: string): Promise<DataSourceApi> { get(ref?: DataSourceRef | string): Promise<DataSourceApi> {
if (!name) { if (!ref) {
return Promise.resolve(this.defaultDS); return Promise.resolve(this.defaultDS);
} }
const ds = this.datasources[name]; const uid = getDataSourceUID(ref) ?? '';
const ds = this.datasources[uid];
if (ds) { if (ds) {
return Promise.resolve(ds); return Promise.resolve(ds);
} }
return Promise.reject('Unknown Datasource: ' + name); return Promise.reject(`Unknown Datasource: ${JSON.stringify(ref)}`);
} }
} }