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

View File

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

View File

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

View File

@ -8,11 +8,15 @@ export enum DataTopic {
}
/**
* In 8.2, this will become an interface
*
* @public
*/
export type DatasourceRef = string;
export interface DataSourceRef {
/** The plugin type-id */
type?: string;
/** Specific datasource instance */
uid?: string;
}
/**
* These are the common properties available to all queries in all datasources
@ -46,5 +50,5 @@ export interface DataQuery {
* For mixed data sources the selected datasource is on the query level.
* For non mixed scenarios this is undefined.
*/
datasource?: DatasourceRef;
datasource?: DataSourceRef;
}

View File

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

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) => (
event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>

View File

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

View File

@ -34,7 +34,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
externalUserMngInfo = '';
allowOrgCreate = false;
disableLoginForm = false;
defaultDatasource = '';
defaultDatasource = ''; // UID
alertingEnabled = false;
alertingErrorOrTimeout = '';
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
@ -11,10 +11,10 @@ import { ScopedVars, DataSourceApi, DataSourceInstanceSettings } from '@grafana/
export interface DataSourceSrv {
/**
* Returns the requested dataSource. If it cannot be found it rejects the promise.
* @param nameOrUid - name or Uid of the datasource plugin you want to use.
* @param ref - The datasource identifier, typically an object with UID and type,
* @param scopedVars - variables used to interpolate a templated passed as name.
*/
get(nameOrUid?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi>;
get(ref?: DataSourceRef | string | null, scopedVars?: ScopedVars): Promise<DataSourceApi>;
/**
* Get a list of data sources
@ -24,7 +24,7 @@ export interface DataSourceSrv {
/**
* Get settings and plugin metadata by name or uid
*/
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined;
getInstanceSettings(ref?: DataSourceRef | string | null): DataSourceInstanceSettings | undefined;
}
/** @public */

View File

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

View File

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

View File

@ -195,6 +195,33 @@ func TestServicebuildPipeLine(t *testing.T) {
},
expectErrContains: "classic conditions may not be the input for other expressions",
},
{
name: "Queries with new datasource ref object",
req: &Request{
Queries: []Query{
{
RefID: "A",
JSON: json.RawMessage(`{
"datasource": {
"uid": "MyDS"
}
}`),
},
{
RefID: "B",
JSON: json.RawMessage(`{
"datasource": {
"uid": "MyDS"
},
"expression": "A",
"reducer": "mean",
"type": "reduce"
}`),
},
},
},
expectedOrder: []string{"B", "A"},
},
}
s := Service{}
for _, tt := range tests {

View File

@ -33,16 +33,59 @@ type rawNode struct {
DatasourceUID string
}
func (rn *rawNode) GetDatasourceName() (string, error) {
func (rn *rawNode) GetDatasourceUID() (string, error) {
if rn.DatasourceUID != "" {
return rn.DatasourceUID, nil
}
rawDs, ok := rn.Query["datasource"]
if !ok {
return "", nil
return "", fmt.Errorf("no datasource property found in query model")
}
dsName, ok := rawDs.(string)
// For old queries with string datasource prop representing data source name
if dsName, ok := rawDs.(string); ok {
return dsName, nil
}
dsRef, ok := rawDs.(map[string]interface{})
if !ok {
return "", fmt.Errorf("expted datasource identifier to be a string, got %T", rawDs)
return "", fmt.Errorf("data source property is not an object nor string, got %T", rawDs)
}
return dsName, nil
if dsUid, ok := dsRef["uid"].(string); ok {
return dsUid, nil
}
return "", fmt.Errorf("no datasource uid found for query, got %T", rn.Query)
}
func (rn *rawNode) IsExpressionQuery() (bool, error) {
if rn.DatasourceUID != "" {
return rn.DatasourceUID == DatasourceUID, nil
}
rawDs, ok := rn.Query["datasource"]
if !ok {
return false, fmt.Errorf("no datasource property found in query model")
}
// For old queries with string datasource prop representing data source name
dsName, ok := rawDs.(string)
if ok && dsName == DatasourceName {
return true, nil
}
dsRef, ok := rawDs.(map[string]interface{})
if !ok {
return false, nil
}
if dsRef["uid"].(string) == DatasourceUID {
return true, nil
}
return false, nil
}
func (rn *rawNode) GetCommandType() (c CommandType, err error) {
@ -171,18 +214,18 @@ func (s *Service) buildDSNode(dp *simple.DirectedGraph, rn *rawNode, req *Reques
}
rawDsID, ok := rn.Query["datasourceId"]
switch ok {
case true:
if ok {
floatDsID, ok := rawDsID.(float64)
if !ok {
return nil, fmt.Errorf("expected datasourceId to be a float64, got type %T for refId %v", rawDsID, rn.RefID)
}
dsNode.datasourceID = int64(floatDsID)
default:
if rn.DatasourceUID == "" {
} else {
dsUid, err := rn.GetDatasourceUID()
if err != nil {
return nil, fmt.Errorf("neither datasourceId or datasourceUid in expression data source request for refId %v", rn.RefID)
}
dsNode.datasourceUID = rn.DatasourceUID
dsNode.datasourceUID = dsUid
}
var floatIntervalMS float64

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) {
if dsName == "" {
func (e *DashAlertExtractor) lookupQueryDataSource(panel *simplejson.Json, panelQuery *simplejson.Json) (*models.DataSource, error) {
dsName := ""
dsUid := ""
datasource, ok := panelQuery.CheckGet("datasource")
if !ok {
fmt.Printf("no query level data soure \n")
datasource = panel.Get("datasource")
}
if name, err := datasource.String(); err == nil {
dsName = name
} else if uid, ok := datasource.CheckGet("uid"); ok {
dsUid = uid.MustString()
}
if dsName == "" && dsUid == "" {
query := &models.GetDefaultDataSourceQuery{OrgId: e.OrgID}
if err := bus.DispatchCtx(context.TODO(), query); err != nil {
return nil, err
@ -39,7 +55,7 @@ func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*models.DataSour
return query.Result, nil
}
query := &models.GetDataSourceQuery{Name: dsName, OrgId: e.OrgID}
query := &models.GetDataSourceQuery{Name: dsName, Uid: dsUid, OrgId: e.OrgID}
if err := bus.DispatchCtx(context.TODO(), query); err != nil {
return nil, err
}
@ -159,17 +175,9 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
return nil, ValidationError{Reason: reason}
}
dsName := ""
if panelQuery.Get("datasource").MustString() != "" {
dsName = panelQuery.Get("datasource").MustString()
} else if panel.Get("datasource").MustString() != "" {
dsName = panel.Get("datasource").MustString()
}
datasource, err := e.lookupDatasourceID(dsName)
datasource, err := e.lookupQueryDataSource(panel, panelQuery)
if err != nil {
e.log.Debug("Error looking up datasource", "error", err)
return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)}
return nil, err
}
dsFilterQuery := models.DatasourcesPermissionFilterQuery{

View File

@ -18,10 +18,10 @@ func TestAlertRuleExtraction(t *testing.T) {
})
// mock data
defaultDs := &models.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
graphite2Ds := &models.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
influxDBDs := &models.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"}
prom := &models.DataSource{Id: 17, OrgId: 1, Name: "Prometheus"}
defaultDs := &models.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true, Uid: "def-uid"}
graphite2Ds := &models.DataSource{Id: 15, OrgId: 1, Name: "graphite2", Uid: "graphite2-uid"}
influxDBDs := &models.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB", Uid: "InfluxDB-uid"}
prom := &models.DataSource{Id: 17, OrgId: 1, Name: "Prometheus", Uid: "Prometheus-uid"}
bus.AddHandler("test", func(query *models.GetDefaultDataSourceQuery) error {
query.Result = defaultDs
@ -29,16 +29,16 @@ func TestAlertRuleExtraction(t *testing.T) {
})
bus.AddHandler("test", func(query *models.GetDataSourceQuery) error {
if query.Name == defaultDs.Name {
if query.Name == defaultDs.Name || query.Uid == defaultDs.Uid {
query.Result = defaultDs
}
if query.Name == graphite2Ds.Name {
if query.Name == graphite2Ds.Name || query.Uid == graphite2Ds.Uid {
query.Result = graphite2Ds
}
if query.Name == influxDBDs.Name {
if query.Name == influxDBDs.Name || query.Uid == influxDBDs.Uid {
query.Result = influxDBDs
}
if query.Name == prom.Name {
if query.Name == prom.Name || query.Uid == prom.Uid {
query.Result = prom
}
@ -246,4 +246,25 @@ func TestAlertRuleExtraction(t *testing.T) {
_, err = extractor.GetAlerts()
require.Equal(t, err.Error(), "alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
})
t.Run("Extract data source given new DataSourceRef object model", func(t *testing.T) {
json, err := ioutil.ReadFile("./testdata/panel-with-datasource-ref.json")
require.Nil(t, err)
dashJSON, err := simplejson.NewJson(json)
require.Nil(t, err)
dash := models.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1, nil)
err = extractor.ValidateAlerts()
require.Nil(t, err)
alerts, err := extractor.GetAlerts()
require.Nil(t, err)
condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
query := condition.Get("query")
require.EqualValues(t, 15, query.Get("datasourceId").MustInt64())
})
}

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: {
get() {
return {};
return {
getRef: jest.fn(),
};
},
getDataSourceById: jest.fn(),
},
@ -239,7 +241,7 @@ describe('hasNonEmptyQuery', () => {
});
test('should return false if query is empty', () => {
expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'panel', datasource: 'some-ds' }])).toBeFalsy();
expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'panel', datasource: { uid: 'some-ds' } }])).toBeFalsy();
});
test('should return false if no queries exist', () => {

View File

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

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { SelectableValue } from '@grafana/data';
import { Field, InputControl, Select } from '@grafana/ui';
import { ExpressionDatasourceID } from 'app/features/expressions/ExpressionDatasource';
import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
import React, { FC, useEffect, useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { RuleFormValues } from '../../types/rule-form';
@ -28,7 +28,7 @@ export const ConditionField: FC = () => {
// reset condition if option no longer exists or if it is unset, but there are options available
useEffect(() => {
const expressions = queries.filter((query) => query.model.datasource === ExpressionDatasourceID);
const expressions = queries.filter((query) => query.datasourceUid === ExpressionDatasourceUID);
if (condition && !options.find(({ value }) => value === condition)) {
setValue('condition', expressions.length ? expressions[expressions.length - 1].refId : null);
} else if (!condition && expressions.length) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -162,7 +162,7 @@ describe('DashboardModel', () => {
});
it('dashboard schema version should be set to latest', () => {
expect(model.schemaVersion).toBe(32);
expect(model.schemaVersion).toBe(33);
});
it('graph thresholds should be migrated', () => {

View File

@ -9,6 +9,7 @@ import { DashboardModel } from './DashboardModel';
import {
DataLink,
DataLinkBuiltInVars,
DataSourceRef,
MappingType,
SpecialValueMatch,
PanelPlugin,
@ -39,6 +40,8 @@ import { config } from 'app/core/config';
import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module';
import { plugin as gaugePanelPlugin } from 'app/plugins/panel/gauge/module';
import { getStandardFieldConfigs, getStandardOptionEditors } from '@grafana/ui';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { getDataSourceSrv } from '@grafana/runtime';
import { labelsToFieldsTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/labelsToFields';
import { mergeTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/merge';
import {
@ -62,7 +65,7 @@ export class DashboardMigrator {
let i, j, k, n;
const oldVersion = this.dashboard.schemaVersion;
const panelUpgrades: PanelSchemeUpgradeHandler[] = [];
this.dashboard.schemaVersion = 32;
this.dashboard.schemaVersion = 33;
if (oldVersion === this.dashboard.schemaVersion) {
return;
@ -695,6 +698,45 @@ export class DashboardMigrator {
this.migrateCloudWatchAnnotationQuery();
}
// Replace datasource name with reference, uid and type
if (oldVersion < 33) {
for (const variable of this.dashboard.templating.list) {
if (variable.type !== 'query') {
continue;
}
let name = (variable as any).datasource as string;
if (name) {
variable.datasource = migrateDatasourceNameToRef(name);
}
}
// Mutate panel models
for (const panel of this.dashboard.panels) {
let name = (panel as any).datasource as string;
if (!name) {
panel.datasource = null; // use default
} else if (name === MIXED_DATASOURCE_NAME) {
panel.datasource = { type: MIXED_DATASOURCE_NAME };
for (const target of panel.targets) {
name = (target as any).datasource as string;
panel.datasource = migrateDatasourceNameToRef(name);
}
continue; // do not cleanup targets
} else {
panel.datasource = migrateDatasourceNameToRef(name);
}
// cleanup query datasource references
if (!panel.targets) {
panel.targets = [];
} else {
for (const target of panel.targets) {
delete target.datasource;
}
}
}
}
if (panelUpgrades.length === 0) {
return;
}
@ -1009,6 +1051,19 @@ function migrateSinglestat(panel: PanelModel) {
return panel;
}
export function migrateDatasourceNameToRef(name: string): DataSourceRef | null {
if (!name || name === 'default') {
return null;
}
const ds = getDataSourceSrv().getInstanceSettings(name);
if (!ds) {
return { uid: name }; // not found
}
return { type: ds.meta.id, uid: ds.uid };
}
// mutates transformations appending a new transformer after the existing one
function appendTransformerAfter(panel: PanelModel, id: string, cfg: DataTransformerConfig) {
if (panel.transformations) {

View File

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

View File

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

View File

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

View File

@ -317,10 +317,12 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
return dsSettings.map((d) => d.settings);
},
getInstanceSettings(name: string) {
return dsSettings.map((d) => d.settings).find((x) => x.name === name);
return dsSettings.map((d) => d.settings).find((x) => x.name === name || x.uid === name);
},
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
return Promise.resolve((name ? dsSettings.find((d) => d.api.name === name) : dsSettings[0])!.api);
return Promise.resolve(
(name ? dsSettings.find((d) => d.api.name === name || d.api.uid === name) : dsSettings[0])!.api
);
},
} as any);
@ -392,7 +394,9 @@ function makeDatasourceSetup({ name = 'loki', id = 1 }: { name?: string; id?: nu
},
},
name: name,
uid: name,
query: jest.fn(),
getRef: jest.fn(),
meta,
} as any,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,12 +3,20 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { Props, QueryEditorRowHeader } from './QueryEditorRowHeader';
import { DataSourceInstanceSettings } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
const mockDS = mockDataSource({
name: 'CloudManager',
type: DataSourceType.Alertmanager,
});
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
return {
getDataSourceSrv: () => ({
getInstanceSettings: jest.fn(),
getList: jest.fn().mockReturnValue([]),
get: () => Promise.resolve(mockDS),
getList: () => [mockDS],
getInstanceSettings: () => mockDS,
}),
};
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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