mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard: replace datasource name with a reference object (#33817)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: Elfo404 <me@giordanoricci.com>
This commit is contained in:
parent
61fbdb60ff
commit
7319efe077
@ -3,13 +3,13 @@ import { ComponentType } from 'react';
|
||||
|
||||
import { QueryEditorProps } from './datasource';
|
||||
import { DataFrame } from './dataFrame';
|
||||
import { DataQuery, DatasourceRef } from './query';
|
||||
import { DataQuery, DataSourceRef } from './query';
|
||||
|
||||
/**
|
||||
* This JSON object is stored in the dashboard json model.
|
||||
*/
|
||||
export interface AnnotationQuery<TQuery extends DataQuery = DataQuery> {
|
||||
datasource?: DatasourceRef | string | null;
|
||||
datasource?: DataSourceRef | string | null;
|
||||
|
||||
enable: boolean;
|
||||
name: string;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FieldConfigSource } from './fieldOverrides';
|
||||
import { DataQuery, DatasourceRef } from './query';
|
||||
import { DataQuery, DataSourceRef } from './query';
|
||||
|
||||
export enum DashboardCursorSync {
|
||||
Off,
|
||||
@ -30,7 +30,7 @@ export interface PanelModel<TOptions = any, TCustomFieldConfig = any> {
|
||||
pluginVersion?: string;
|
||||
|
||||
/** The datasource used in all targets */
|
||||
datasource?: DatasourceRef | null;
|
||||
datasource?: DataSourceRef | null;
|
||||
|
||||
/** The queries in a panel */
|
||||
targets?: DataQuery[];
|
||||
|
@ -13,6 +13,7 @@ import { LiveChannelSupport } from './live';
|
||||
import { CustomVariableSupport, DataSourceVariableSupport, StandardVariableSupport } from './variables';
|
||||
import { makeClassES5Compatible } from '../utils/makeClassES5Compatible';
|
||||
import { DataQuery } from './query';
|
||||
import { DataSourceRef } from '.';
|
||||
|
||||
export interface DataSourcePluginOptionsEditorProps<JSONData = DataSourceJsonData, SecureJSONData = {}> {
|
||||
options: DataSourceSettings<JSONData, SecureJSONData>;
|
||||
@ -315,6 +316,11 @@ abstract class DataSourceApi<
|
||||
*/
|
||||
getHighlighterExpression?(query: TQuery): string[];
|
||||
|
||||
/** Get an identifier object for this datasource instance */
|
||||
getRef(): DataSourceRef {
|
||||
return { type: this.type, uid: this.uid };
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in explore
|
||||
*/
|
||||
|
@ -8,11 +8,15 @@ export enum DataTopic {
|
||||
}
|
||||
|
||||
/**
|
||||
* In 8.2, this will become an interface
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type DatasourceRef = string;
|
||||
export interface DataSourceRef {
|
||||
/** The plugin type-id */
|
||||
type?: string;
|
||||
|
||||
/** Specific datasource instance */
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* These are the common properties available to all queries in all datasources
|
||||
@ -46,5 +50,5 @@ export interface DataQuery {
|
||||
* For mixed data sources the selected datasource is on the query level.
|
||||
* For non mixed scenarios this is undefined.
|
||||
*/
|
||||
datasource?: DatasourceRef;
|
||||
datasource?: DataSourceRef;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { DataQuery, DatasourceRef } from './query';
|
||||
import { DataQuery, DataSourceRef } from './query';
|
||||
import { DataSourceApi } from './datasource';
|
||||
import { PanelData } from './panel';
|
||||
import { ScopedVars } from './ScopedVars';
|
||||
@ -11,7 +11,7 @@ import { TimeRange, TimeZone } from './time';
|
||||
* @internal
|
||||
*/
|
||||
export interface QueryRunnerOptions {
|
||||
datasource: DatasourceRef | DataSourceApi | null;
|
||||
datasource: DataSourceRef | DataSourceApi | null;
|
||||
queries: DataQuery[];
|
||||
panelId?: number;
|
||||
dashboardId?: number;
|
||||
|
@ -1,4 +1,40 @@
|
||||
import { DataSourcePluginOptionsEditorProps, SelectableValue, KeyValue, DataSourceSettings } from '../types';
|
||||
import { isString } from 'lodash';
|
||||
import {
|
||||
DataSourcePluginOptionsEditorProps,
|
||||
SelectableValue,
|
||||
KeyValue,
|
||||
DataSourceSettings,
|
||||
DataSourceInstanceSettings,
|
||||
DataSourceRef,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Convert instance settings to a reference
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getDataSourceRef(ds: DataSourceInstanceSettings): DataSourceRef {
|
||||
return { uid: ds.uid, type: ds.type };
|
||||
}
|
||||
|
||||
function isDataSourceRef(ref: DataSourceRef | string | null): ref is DataSourceRef {
|
||||
return typeof ref === 'object' && (typeof ref?.uid === 'string' || typeof ref?.uid === 'undefined');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the UID from a string of reference
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getDataSourceUID(ref: DataSourceRef | string | null): string | undefined {
|
||||
if (isDataSourceRef(ref)) {
|
||||
return ref.uid;
|
||||
}
|
||||
if (isString(ref)) {
|
||||
return ref;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const onUpdateDatasourceOption = (props: DataSourcePluginOptionsEditorProps, key: keyof DataSourceSettings) => (
|
||||
event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>
|
||||
|
@ -3,7 +3,13 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { HorizontalGroup, PluginSignatureBadge, Select, stylesFactory } from '@grafana/ui';
|
||||
import { DataSourceInstanceSettings, isUnsignedPluginSignature, SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
DataSourceInstanceSettings,
|
||||
DataSourceRef,
|
||||
getDataSourceUID,
|
||||
isUnsignedPluginSignature,
|
||||
SelectableValue,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { getDataSourceSrv } from '../services/dataSourceSrv';
|
||||
import { css, cx } from '@emotion/css';
|
||||
@ -15,7 +21,7 @@ import { css, cx } from '@emotion/css';
|
||||
*/
|
||||
export interface DataSourcePickerProps {
|
||||
onChange: (ds: DataSourceInstanceSettings) => void;
|
||||
current: string | null;
|
||||
current: DataSourceRef | string | null; // uid
|
||||
hideTextValue?: boolean;
|
||||
onBlur?: () => void;
|
||||
autoFocus?: boolean;
|
||||
@ -85,7 +91,6 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
|
||||
|
||||
private getCurrentValue(): SelectableValue<string> | undefined {
|
||||
const { current, hideTextValue, noDefault } = this.props;
|
||||
|
||||
if (!current && noDefault) {
|
||||
return;
|
||||
}
|
||||
@ -95,16 +100,17 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
|
||||
if (ds) {
|
||||
return {
|
||||
label: ds.name.substr(0, 37),
|
||||
value: ds.name,
|
||||
value: ds.uid,
|
||||
imgUrl: ds.meta.info.logos.small,
|
||||
hideText: hideTextValue,
|
||||
meta: ds.meta,
|
||||
};
|
||||
}
|
||||
|
||||
const uid = getDataSourceUID(current);
|
||||
return {
|
||||
label: (current ?? 'no name') + ' - not found',
|
||||
value: current === null ? undefined : current,
|
||||
label: (uid ?? 'no name') + ' - not found',
|
||||
value: uid ?? undefined,
|
||||
imgUrl: '',
|
||||
hideText: hideTextValue,
|
||||
};
|
||||
|
@ -34,7 +34,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
externalUserMngInfo = '';
|
||||
allowOrgCreate = false;
|
||||
disableLoginForm = false;
|
||||
defaultDatasource = '';
|
||||
defaultDatasource = ''; // UID
|
||||
alertingEnabled = false;
|
||||
alertingErrorOrTimeout = '';
|
||||
alertingNoDataOrNullValues = '';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ScopedVars, DataSourceApi, DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { ScopedVars, DataSourceApi, DataSourceInstanceSettings, DataSourceRef } from '@grafana/data';
|
||||
|
||||
/**
|
||||
* This is the entry point for communicating with a datasource that is added as
|
||||
@ -11,10 +11,10 @@ import { ScopedVars, DataSourceApi, DataSourceInstanceSettings } from '@grafana/
|
||||
export interface DataSourceSrv {
|
||||
/**
|
||||
* Returns the requested dataSource. If it cannot be found it rejects the promise.
|
||||
* @param nameOrUid - name or Uid of the datasource plugin you want to use.
|
||||
* @param ref - The datasource identifier, typically an object with UID and type,
|
||||
* @param scopedVars - variables used to interpolate a templated passed as name.
|
||||
*/
|
||||
get(nameOrUid?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi>;
|
||||
get(ref?: DataSourceRef | string | null, scopedVars?: ScopedVars): Promise<DataSourceApi>;
|
||||
|
||||
/**
|
||||
* Get a list of data sources
|
||||
@ -24,7 +24,7 @@ export interface DataSourceSrv {
|
||||
/**
|
||||
* Get settings and plugin metadata by name or uid
|
||||
*/
|
||||
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined;
|
||||
getInstanceSettings(ref?: DataSourceRef | string | null): DataSourceInstanceSettings | undefined;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -102,7 +102,7 @@ class DataSourceWithBackend<
|
||||
const ds = getDataSourceSrv().getInstanceSettings(q.datasource);
|
||||
|
||||
if (!ds) {
|
||||
throw new Error('Unknown Datasource: ' + q.datasource);
|
||||
throw new Error(`Unknown Datasource: ${JSON.stringify(q.datasource)}`);
|
||||
}
|
||||
|
||||
datasourceId = ds.id;
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/expr/mathexp"
|
||||
|
||||
"gonum.org/v1/gonum/graph"
|
||||
"gonum.org/v1/gonum/graph/simple"
|
||||
"gonum.org/v1/gonum/graph/topo"
|
||||
)
|
||||
@ -129,9 +128,11 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) {
|
||||
for _, query := range req.Queries {
|
||||
rawQueryProp := make(map[string]interface{})
|
||||
queryBytes, err := query.JSON.MarshalJSON()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(queryBytes, &rawQueryProp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -145,23 +146,23 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) {
|
||||
DatasourceUID: query.DatasourceUID,
|
||||
}
|
||||
|
||||
dsName, err := rn.GetDatasourceName()
|
||||
isExpr, err := rn.IsExpressionQuery()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dsUID := rn.DatasourceUID
|
||||
var node Node
|
||||
|
||||
var node graph.Node
|
||||
switch {
|
||||
case dsName == DatasourceName || dsUID == DatasourceUID:
|
||||
if isExpr {
|
||||
node, err = buildCMDNode(dp, rn)
|
||||
default: // If it's not an expression query, it's a data source query.
|
||||
} else {
|
||||
node, err = s.buildDSNode(dp, rn, req)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dp.AddNode(node)
|
||||
}
|
||||
return dp, nil
|
||||
|
@ -195,6 +195,33 @@ func TestServicebuildPipeLine(t *testing.T) {
|
||||
},
|
||||
expectErrContains: "classic conditions may not be the input for other expressions",
|
||||
},
|
||||
{
|
||||
name: "Queries with new datasource ref object",
|
||||
req: &Request{
|
||||
Queries: []Query{
|
||||
{
|
||||
RefID: "A",
|
||||
JSON: json.RawMessage(`{
|
||||
"datasource": {
|
||||
"uid": "MyDS"
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
RefID: "B",
|
||||
JSON: json.RawMessage(`{
|
||||
"datasource": {
|
||||
"uid": "MyDS"
|
||||
},
|
||||
"expression": "A",
|
||||
"reducer": "mean",
|
||||
"type": "reduce"
|
||||
}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedOrder: []string{"B", "A"},
|
||||
},
|
||||
}
|
||||
s := Service{}
|
||||
for _, tt := range tests {
|
||||
|
@ -33,16 +33,59 @@ type rawNode struct {
|
||||
DatasourceUID string
|
||||
}
|
||||
|
||||
func (rn *rawNode) GetDatasourceName() (string, error) {
|
||||
func (rn *rawNode) GetDatasourceUID() (string, error) {
|
||||
if rn.DatasourceUID != "" {
|
||||
return rn.DatasourceUID, nil
|
||||
}
|
||||
|
||||
rawDs, ok := rn.Query["datasource"]
|
||||
if !ok {
|
||||
return "", nil
|
||||
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
|
||||
|
@ -30,8 +30,24 @@ func NewDashAlertExtractor(dash *models.Dashboard, orgID int64, user *models.Sig
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*models.DataSource, error) {
|
||||
if dsName == "" {
|
||||
func (e *DashAlertExtractor) lookupQueryDataSource(panel *simplejson.Json, panelQuery *simplejson.Json) (*models.DataSource, error) {
|
||||
dsName := ""
|
||||
dsUid := ""
|
||||
|
||||
datasource, ok := panelQuery.CheckGet("datasource")
|
||||
|
||||
if !ok {
|
||||
fmt.Printf("no query level data soure \n")
|
||||
datasource = panel.Get("datasource")
|
||||
}
|
||||
|
||||
if name, err := datasource.String(); err == nil {
|
||||
dsName = name
|
||||
} else if uid, ok := datasource.CheckGet("uid"); ok {
|
||||
dsUid = uid.MustString()
|
||||
}
|
||||
|
||||
if dsName == "" && dsUid == "" {
|
||||
query := &models.GetDefaultDataSourceQuery{OrgId: e.OrgID}
|
||||
if err := bus.DispatchCtx(context.TODO(), query); err != nil {
|
||||
return nil, err
|
||||
@ -39,7 +55,7 @@ func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*models.DataSour
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
query := &models.GetDataSourceQuery{Name: dsName, OrgId: e.OrgID}
|
||||
query := &models.GetDataSourceQuery{Name: dsName, Uid: dsUid, OrgId: e.OrgID}
|
||||
if err := bus.DispatchCtx(context.TODO(), query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -159,17 +175,9 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
||||
return nil, ValidationError{Reason: reason}
|
||||
}
|
||||
|
||||
dsName := ""
|
||||
if panelQuery.Get("datasource").MustString() != "" {
|
||||
dsName = panelQuery.Get("datasource").MustString()
|
||||
} else if panel.Get("datasource").MustString() != "" {
|
||||
dsName = panel.Get("datasource").MustString()
|
||||
}
|
||||
|
||||
datasource, err := e.lookupDatasourceID(dsName)
|
||||
datasource, err := e.lookupQueryDataSource(panel, panelQuery)
|
||||
if err != nil {
|
||||
e.log.Debug("Error looking up datasource", "error", err)
|
||||
return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dsFilterQuery := models.DatasourcesPermissionFilterQuery{
|
||||
|
@ -18,10 +18,10 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
// mock data
|
||||
defaultDs := &models.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
|
||||
graphite2Ds := &models.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
|
||||
influxDBDs := &models.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"}
|
||||
prom := &models.DataSource{Id: 17, OrgId: 1, Name: "Prometheus"}
|
||||
defaultDs := &models.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true, Uid: "def-uid"}
|
||||
graphite2Ds := &models.DataSource{Id: 15, OrgId: 1, Name: "graphite2", Uid: "graphite2-uid"}
|
||||
influxDBDs := &models.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB", Uid: "InfluxDB-uid"}
|
||||
prom := &models.DataSource{Id: 17, OrgId: 1, Name: "Prometheus", Uid: "Prometheus-uid"}
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetDefaultDataSourceQuery) error {
|
||||
query.Result = defaultDs
|
||||
@ -29,16 +29,16 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetDataSourceQuery) error {
|
||||
if query.Name == defaultDs.Name {
|
||||
if query.Name == defaultDs.Name || query.Uid == defaultDs.Uid {
|
||||
query.Result = defaultDs
|
||||
}
|
||||
if query.Name == graphite2Ds.Name {
|
||||
if query.Name == graphite2Ds.Name || query.Uid == graphite2Ds.Uid {
|
||||
query.Result = graphite2Ds
|
||||
}
|
||||
if query.Name == influxDBDs.Name {
|
||||
if query.Name == influxDBDs.Name || query.Uid == influxDBDs.Uid {
|
||||
query.Result = influxDBDs
|
||||
}
|
||||
if query.Name == prom.Name {
|
||||
if query.Name == prom.Name || query.Uid == prom.Uid {
|
||||
query.Result = prom
|
||||
}
|
||||
|
||||
@ -246,4 +246,25 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
_, err = extractor.GetAlerts()
|
||||
require.Equal(t, err.Error(), "alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
|
||||
})
|
||||
|
||||
t.Run("Extract data source given new DataSourceRef object model", func(t *testing.T) {
|
||||
json, err := ioutil.ReadFile("./testdata/panel-with-datasource-ref.json")
|
||||
require.Nil(t, err)
|
||||
|
||||
dashJSON, err := simplejson.NewJson(json)
|
||||
require.Nil(t, err)
|
||||
dash := models.NewDashboardFromJson(dashJSON)
|
||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||
|
||||
err = extractor.ValidateAlerts()
|
||||
|
||||
require.Nil(t, err)
|
||||
|
||||
alerts, err := extractor.GetAlerts()
|
||||
require.Nil(t, err)
|
||||
|
||||
condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
|
||||
query := condition.Get("query")
|
||||
require.EqualValues(t, 15, query.Get("datasourceId").MustInt64())
|
||||
})
|
||||
}
|
||||
|
38
pkg/services/alerting/testdata/panel-with-datasource-ref.json
vendored
Normal file
38
pkg/services/alerting/testdata/panel-with-datasource-ref.json
vendored
Normal 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] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -198,7 +198,9 @@ describe('getExploreUrl', () => {
|
||||
},
|
||||
datasourceSrv: {
|
||||
get() {
|
||||
return {};
|
||||
return {
|
||||
getRef: jest.fn(),
|
||||
};
|
||||
},
|
||||
getDataSourceById: jest.fn(),
|
||||
},
|
||||
@ -239,7 +241,7 @@ describe('hasNonEmptyQuery', () => {
|
||||
});
|
||||
|
||||
test('should return false if query is empty', () => {
|
||||
expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'panel', datasource: 'some-ds' }])).toBeFalsy();
|
||||
expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'panel', datasource: { uid: 'some-ds' } }])).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should return false if no queries exist', () => {
|
||||
|
@ -99,7 +99,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise<strin
|
||||
...state,
|
||||
datasource: exploreDatasource.name,
|
||||
context: 'explore',
|
||||
queries: exploreTargets.map((t) => ({ ...t, datasource: exploreDatasource.name })),
|
||||
queries: exploreTargets.map((t) => ({ ...t, datasource: exploreDatasource.getRef() })),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataQuery, DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { DataQuery, DataSourceInstanceSettings, DataSourceRef, getDataSourceRef } from '@grafana/data';
|
||||
|
||||
export const getNextRefIdChar = (queries: DataQuery[]): string => {
|
||||
for (let num = 0; ; num++) {
|
||||
@ -9,7 +9,7 @@ export const getNextRefIdChar = (queries: DataQuery[]): string => {
|
||||
}
|
||||
};
|
||||
|
||||
export function addQuery(queries: DataQuery[], query?: Partial<DataQuery>, datasource?: string): DataQuery[] {
|
||||
export function addQuery(queries: DataQuery[], query?: Partial<DataQuery>, datasource?: DataSourceRef): DataQuery[] {
|
||||
const q = query || {};
|
||||
q.refId = getNextRefIdChar(queries);
|
||||
q.hide = false;
|
||||
@ -27,16 +27,18 @@ export function updateQueries(
|
||||
extensionID: string, // pass this in because importing it creates a circular dependency
|
||||
dsSettings?: DataSourceInstanceSettings
|
||||
): DataQuery[] {
|
||||
const datasource = getDataSourceRef(newSettings);
|
||||
|
||||
if (!newSettings.meta.mixed && dsSettings?.meta.mixed) {
|
||||
return queries.map((q) => {
|
||||
if (q.datasource !== extensionID) {
|
||||
q.datasource = newSettings.name;
|
||||
q.datasource = datasource;
|
||||
}
|
||||
return q;
|
||||
});
|
||||
} else if (!newSettings.meta.mixed && dsSettings?.meta.id !== newSettings.meta.id) {
|
||||
// we are changing data source type, clear queries
|
||||
return [{ refId: 'A', datasource: newSettings.name }];
|
||||
return [{ refId: 'A', datasource }];
|
||||
}
|
||||
|
||||
return queries;
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { DataSourceSrv } from '@grafana/runtime';
|
||||
import { DataSourceApi, PluginMeta, DataTransformerConfig, DataSourceInstanceSettings } from '@grafana/data';
|
||||
import {
|
||||
DataSourceApi,
|
||||
PluginMeta,
|
||||
DataTransformerConfig,
|
||||
DataSourceInstanceSettings,
|
||||
DataSourceRef,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { ElasticsearchQuery } from '../../plugins/datasource/elasticsearch/types';
|
||||
import { getAlertingValidationMessage } from './getAlertingValidationMessage';
|
||||
@ -18,10 +24,13 @@ describe('getAlertingValidationMessage', () => {
|
||||
return false;
|
||||
},
|
||||
name: 'some name',
|
||||
uid: 'some uid',
|
||||
} as any) as DataSourceApi;
|
||||
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||
const datasourceSrv: DataSourceSrv = {
|
||||
get: getMock,
|
||||
get: (ref: DataSourceRef) => {
|
||||
return getMock(ref.uid);
|
||||
},
|
||||
getList(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
},
|
||||
@ -33,11 +42,13 @@ describe('getAlertingValidationMessage', () => {
|
||||
];
|
||||
const transformations: DataTransformerConfig[] = [];
|
||||
|
||||
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name);
|
||||
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, {
|
||||
uid: datasource.uid,
|
||||
});
|
||||
|
||||
expect(result).toBe('');
|
||||
expect(getMock).toHaveBeenCalledTimes(2);
|
||||
expect(getMock).toHaveBeenCalledWith(datasource.name);
|
||||
expect(getMock).toHaveBeenCalledWith(datasource.uid);
|
||||
});
|
||||
});
|
||||
|
||||
@ -73,7 +84,9 @@ describe('getAlertingValidationMessage', () => {
|
||||
];
|
||||
const transformations: DataTransformerConfig[] = [];
|
||||
|
||||
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name);
|
||||
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, {
|
||||
uid: datasource.name,
|
||||
});
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
@ -88,7 +101,9 @@ describe('getAlertingValidationMessage', () => {
|
||||
} as any) as DataSourceApi;
|
||||
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||
const datasourceSrv: DataSourceSrv = {
|
||||
get: getMock,
|
||||
get: (ref: DataSourceRef) => {
|
||||
return getMock(ref.uid);
|
||||
},
|
||||
getInstanceSettings: (() => {}) as any,
|
||||
getList(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
@ -100,7 +115,9 @@ describe('getAlertingValidationMessage', () => {
|
||||
];
|
||||
const transformations: DataTransformerConfig[] = [];
|
||||
|
||||
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name);
|
||||
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, {
|
||||
uid: datasource.name,
|
||||
});
|
||||
|
||||
expect(result).toBe('Template variables are not supported in alert queries');
|
||||
expect(getMock).toHaveBeenCalledTimes(2);
|
||||
@ -114,10 +131,13 @@ describe('getAlertingValidationMessage', () => {
|
||||
meta: ({ alerting: false } as any) as PluginMeta,
|
||||
targetContainsTemplate: () => false,
|
||||
name: 'some name',
|
||||
uid: 'theid',
|
||||
} as any) as DataSourceApi;
|
||||
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||
const datasourceSrv: DataSourceSrv = {
|
||||
get: getMock,
|
||||
get: (ref: DataSourceRef) => {
|
||||
return getMock(ref.uid);
|
||||
},
|
||||
getInstanceSettings: (() => {}) as any,
|
||||
getList(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
@ -129,11 +149,13 @@ describe('getAlertingValidationMessage', () => {
|
||||
];
|
||||
const transformations: DataTransformerConfig[] = [];
|
||||
|
||||
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name);
|
||||
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, {
|
||||
uid: datasource.uid,
|
||||
});
|
||||
|
||||
expect(result).toBe('The datasource does not support alerting queries');
|
||||
expect(getMock).toHaveBeenCalledTimes(2);
|
||||
expect(getMock).toHaveBeenCalledWith(datasource.name);
|
||||
expect(getMock).toHaveBeenCalledWith(datasource.uid);
|
||||
});
|
||||
});
|
||||
|
||||
@ -146,7 +168,9 @@ describe('getAlertingValidationMessage', () => {
|
||||
} as any) as DataSourceApi;
|
||||
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||
const datasourceSrv: DataSourceSrv = {
|
||||
get: getMock,
|
||||
get: (ref: DataSourceRef) => {
|
||||
return getMock(ref.uid);
|
||||
},
|
||||
getInstanceSettings: (() => {}) as any,
|
||||
getList(): DataSourceInstanceSettings[] {
|
||||
return [];
|
||||
@ -158,7 +182,9 @@ describe('getAlertingValidationMessage', () => {
|
||||
];
|
||||
const transformations: DataTransformerConfig[] = [{ id: 'A', options: null }];
|
||||
|
||||
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name);
|
||||
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, {
|
||||
uid: datasource.uid,
|
||||
});
|
||||
|
||||
expect(result).toBe('Transformations are not supported in alert queries');
|
||||
expect(getMock).toHaveBeenCalledTimes(0);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataQuery, DataTransformerConfig } from '@grafana/data';
|
||||
import { DataQuery, DataSourceRef, DataTransformerConfig } from '@grafana/data';
|
||||
import { DataSourceSrv } from '@grafana/runtime';
|
||||
|
||||
export const getDefaultCondition = () => ({
|
||||
@ -13,7 +13,7 @@ export const getAlertingValidationMessage = async (
|
||||
transformations: DataTransformerConfig[] | undefined,
|
||||
targets: DataQuery[],
|
||||
datasourceSrv: DataSourceSrv,
|
||||
datasourceName: string | null
|
||||
datasource: DataSourceRef | null
|
||||
): Promise<string> => {
|
||||
if (targets.length === 0) {
|
||||
return 'Could not find any metric queries';
|
||||
@ -27,8 +27,8 @@ export const getAlertingValidationMessage = async (
|
||||
let templateVariablesNotSupported = 0;
|
||||
|
||||
for (const target of targets) {
|
||||
const dsName = target.datasource || datasourceName;
|
||||
const ds = await datasourceSrv.get(dsName);
|
||||
const dsRef = target.datasource || datasource;
|
||||
const ds = await datasourceSrv.get(dsRef);
|
||||
if (!ds.meta.alerting) {
|
||||
alertingNotSupported++;
|
||||
} else if (ds.targetContainsTemplate && ds.targetContainsTemplate(target)) {
|
||||
|
@ -148,7 +148,10 @@ const dashboard = {
|
||||
} as DashboardModel;
|
||||
|
||||
const panel = ({
|
||||
datasource: dataSources.prometheus.uid,
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: dataSources.prometheus.uid,
|
||||
},
|
||||
title: 'mypanel',
|
||||
id: 34,
|
||||
targets: [
|
||||
@ -169,10 +172,10 @@ describe('PanelAlertTabContent', () => {
|
||||
jest.resetAllMocks();
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||
const dsService = new MockDataSourceSrv(dataSources);
|
||||
dsService.datasources[dataSources.prometheus.name] = new PrometheusDatasource(
|
||||
dsService.datasources[dataSources.prometheus.uid] = new PrometheusDatasource(
|
||||
dataSources.prometheus
|
||||
) as DataSourceApi<any, any>;
|
||||
dsService.datasources[dataSources.default.name] = new PrometheusDatasource(dataSources.default) as DataSourceApi<
|
||||
dsService.datasources[dataSources.default.uid] = new PrometheusDatasource(dataSources.default) as DataSourceApi<
|
||||
any,
|
||||
any
|
||||
>;
|
||||
@ -185,15 +188,20 @@ describe('PanelAlertTabContent', () => {
|
||||
maxDataPoints: 100,
|
||||
interval: '10s',
|
||||
} as any) as PanelModel);
|
||||
|
||||
const button = await ui.createButton.find();
|
||||
const href = button.href;
|
||||
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
|
||||
expect(match).toHaveLength(2);
|
||||
|
||||
const defaults = JSON.parse(decodeURIComponent(match![1]));
|
||||
expect(defaults.queries[0].model).toEqual({
|
||||
expr: 'sum(some_metric [5m])) by (app)',
|
||||
refId: 'A',
|
||||
datasource: 'Prometheus',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'mock-ds-2',
|
||||
},
|
||||
interval: '',
|
||||
intervalMs: 300000,
|
||||
maxDataPoints: 100,
|
||||
@ -207,15 +215,20 @@ describe('PanelAlertTabContent', () => {
|
||||
maxDataPoints: 100,
|
||||
interval: '10s',
|
||||
} as any) as PanelModel);
|
||||
|
||||
const button = await ui.createButton.find();
|
||||
const href = button.href;
|
||||
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
|
||||
expect(match).toHaveLength(2);
|
||||
|
||||
const defaults = JSON.parse(decodeURIComponent(match![1]));
|
||||
expect(defaults.queries[0].model).toEqual({
|
||||
expr: 'sum(some_metric [5m])) by (app)',
|
||||
refId: 'A',
|
||||
datasource: 'Default',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'mock-ds-3',
|
||||
},
|
||||
interval: '',
|
||||
intervalMs: 300000,
|
||||
maxDataPoints: 100,
|
||||
@ -223,21 +236,26 @@ describe('PanelAlertTabContent', () => {
|
||||
});
|
||||
|
||||
it('Will take into account datasource minInterval', async () => {
|
||||
((getDatasourceSrv() as any) as MockDataSourceSrv).datasources[dataSources.prometheus.name].interval = '7m';
|
||||
((getDatasourceSrv() as any) as MockDataSourceSrv).datasources[dataSources.prometheus.uid].interval = '7m';
|
||||
|
||||
await renderAlertTabContent(dashboard, ({
|
||||
...panel,
|
||||
maxDataPoints: 100,
|
||||
} as any) as PanelModel);
|
||||
|
||||
const button = await ui.createButton.find();
|
||||
const href = button.href;
|
||||
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
|
||||
expect(match).toHaveLength(2);
|
||||
|
||||
const defaults = JSON.parse(decodeURIComponent(match![1]));
|
||||
expect(defaults.queries[0].model).toEqual({
|
||||
expr: 'sum(some_metric [7m])) by (app)',
|
||||
refId: 'A',
|
||||
datasource: 'Prometheus',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'mock-ds-2',
|
||||
},
|
||||
interval: '',
|
||||
intervalMs: 420000,
|
||||
maxDataPoints: 100,
|
||||
@ -254,10 +272,12 @@ describe('PanelAlertTabContent', () => {
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toHaveTextContent(/dashboardrule1/);
|
||||
expect(rows[0]).not.toHaveTextContent(/dashboardrule2/);
|
||||
|
||||
const button = await ui.createButton.find();
|
||||
const href = button.href;
|
||||
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
|
||||
expect(match).toHaveLength(2);
|
||||
|
||||
const defaults = JSON.parse(decodeURIComponent(match![1]));
|
||||
expect(defaults).toEqual({
|
||||
type: 'grafana',
|
||||
@ -271,7 +291,10 @@ describe('PanelAlertTabContent', () => {
|
||||
model: {
|
||||
expr: 'sum(some_metric [15s])) by (app)',
|
||||
refId: 'A',
|
||||
datasource: 'Prometheus',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'mock-ds-2',
|
||||
},
|
||||
interval: '',
|
||||
intervalMs: 15000,
|
||||
},
|
||||
@ -284,7 +307,10 @@ describe('PanelAlertTabContent', () => {
|
||||
refId: 'B',
|
||||
hide: false,
|
||||
type: 'classic_conditions',
|
||||
datasource: '__expr__',
|
||||
datasource: {
|
||||
type: 'grafana-expression',
|
||||
uid: '-100',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
type: 'query',
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Field, InputControl, Select } from '@grafana/ui';
|
||||
import { ExpressionDatasourceID } from 'app/features/expressions/ExpressionDatasource';
|
||||
import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
|
||||
import React, { FC, useEffect, useMemo } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
@ -28,7 +28,7 @@ export const ConditionField: FC = () => {
|
||||
|
||||
// reset condition if option no longer exists or if it is unset, but there are options available
|
||||
useEffect(() => {
|
||||
const expressions = queries.filter((query) => query.model.datasource === ExpressionDatasourceID);
|
||||
const expressions = queries.filter((query) => query.datasourceUid === ExpressionDatasourceUID);
|
||||
if (condition && !options.find(({ value }) => value === condition)) {
|
||||
setValue('condition', expressions.length ? expressions[expressions.length - 1].refId : null);
|
||||
} else if (!condition && expressions.length) {
|
||||
|
@ -85,7 +85,10 @@ export class QueryEditor extends PureComponent<Props, State> {
|
||||
datasourceUid: defaultDataSource.uid,
|
||||
model: {
|
||||
refId: '',
|
||||
datasource: defaultDataSource.name,
|
||||
datasource: {
|
||||
type: defaultDataSource.type,
|
||||
uid: defaultDataSource.uid,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
DataSourceInstanceSettings,
|
||||
DataSourceJsonData,
|
||||
DataSourcePluginMeta,
|
||||
DataSourceRef,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
@ -227,6 +228,7 @@ export const mockSilence = (partial: Partial<Silence> = {}): Silence => {
|
||||
...partial,
|
||||
};
|
||||
};
|
||||
|
||||
export class MockDataSourceSrv implements DataSourceSrv {
|
||||
datasources: Record<string, DataSourceApi> = {};
|
||||
// @ts-ignore
|
||||
@ -238,6 +240,7 @@ export class MockDataSourceSrv implements DataSourceSrv {
|
||||
getVariables: () => [],
|
||||
replace: (name: any) => name,
|
||||
};
|
||||
|
||||
defaultName = '';
|
||||
|
||||
constructor(datasources: Record<string, DataSourceInstanceSettings>) {
|
||||
@ -249,6 +252,7 @@ export class MockDataSourceSrv implements DataSourceSrv {
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
for (const dsSettings of Object.values(this.settingsMapByName)) {
|
||||
this.settingsMapByUid[dsSettings.uid] = dsSettings;
|
||||
this.settingsMapById[dsSettings.id] = dsSettings;
|
||||
@ -258,7 +262,7 @@ export class MockDataSourceSrv implements DataSourceSrv {
|
||||
}
|
||||
}
|
||||
|
||||
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
|
||||
get(name?: string | null | DataSourceRef, scopedVars?: ScopedVars): Promise<DataSourceApi> {
|
||||
return DatasourceSrv.prototype.get.call(this, name, scopedVars);
|
||||
//return Promise.reject(new Error('not implemented'));
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { alertRuleToQueries } from './query';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
|
||||
import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
|
||||
|
||||
describe('alertRuleToQueries', () => {
|
||||
it('it should convert grafana alert', () => {
|
||||
@ -110,7 +111,9 @@ const grafanaAlert = {
|
||||
type: 'query',
|
||||
},
|
||||
],
|
||||
datasource: '__expr__',
|
||||
datasource: {
|
||||
uid: ExpressionDatasourceUID,
|
||||
},
|
||||
hide: false,
|
||||
refId: 'B',
|
||||
type: 'classic_conditions',
|
||||
|
@ -6,12 +6,13 @@ import {
|
||||
getDefaultRelativeTimeRange,
|
||||
TimeRange,
|
||||
IntervalValues,
|
||||
DataSourceRef,
|
||||
} from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { ExpressionDatasourceID, ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
|
||||
import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
|
||||
import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types';
|
||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||
import {
|
||||
@ -189,7 +190,10 @@ const getDefaultExpression = (refId: string): AlertQuery => {
|
||||
refId,
|
||||
hide: false,
|
||||
type: ExpressionQueryType.classic,
|
||||
datasource: ExpressionDatasourceID,
|
||||
datasource: {
|
||||
uid: ExpressionDatasourceUID,
|
||||
type: 'grafana-expression',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
type: 'query',
|
||||
@ -223,14 +227,15 @@ const dataQueriesToGrafanaQueries = async (
|
||||
queries: DataQuery[],
|
||||
relativeTimeRange: RelativeTimeRange,
|
||||
scopedVars: ScopedVars | {},
|
||||
datasourceName?: string,
|
||||
panelDataSourceRef?: DataSourceRef,
|
||||
maxDataPoints?: number,
|
||||
minInterval?: string
|
||||
): Promise<AlertQuery[]> => {
|
||||
const result: AlertQuery[] = [];
|
||||
|
||||
for (const target of queries) {
|
||||
const datasource = await getDataSourceSrv().get(target.datasource || datasourceName);
|
||||
const dsName = datasource.name;
|
||||
const datasource = await getDataSourceSrv().get(target.datasource?.uid ? target.datasource : panelDataSourceRef);
|
||||
const dsRef = { uid: datasource.uid, type: datasource.type };
|
||||
|
||||
const range = rangeUtil.relativeToTimeRange(relativeTimeRange);
|
||||
const { interval, intervalMs } = getIntervals(range, minInterval ?? datasource.interval, maxDataPoints);
|
||||
@ -239,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,17 +2,13 @@ import { find } from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import { DashboardExporter, LibraryElementExport } from './DashboardExporter';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { PanelPluginMeta } from '@grafana/data';
|
||||
import { DataSourceInstanceSettings, DataSourceRef, PanelPluginMeta } from '@grafana/data';
|
||||
import { variableAdapters } from '../../../variables/adapters';
|
||||
import { createConstantVariableAdapter } from '../../../variables/constant/adapter';
|
||||
import { createQueryVariableAdapter } from '../../../variables/query/adapter';
|
||||
import { createDataSourceVariableAdapter } from '../../../variables/datasource/adapter';
|
||||
import { LibraryElementKind } from '../../../library-panels/types';
|
||||
|
||||
function getStub(arg: string) {
|
||||
return Promise.resolve(stubs[arg || 'gfdb']);
|
||||
}
|
||||
|
||||
jest.mock('app/core/store', () => {
|
||||
return {
|
||||
getBool: jest.fn(),
|
||||
@ -22,9 +18,16 @@ jest.mock('app/core/store', () => {
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||
getDataSourceSrv: () => ({
|
||||
get: jest.fn((arg) => getStub(arg)),
|
||||
}),
|
||||
getDataSourceSrv: () => {
|
||||
return {
|
||||
get: (v: any) => {
|
||||
const s = getStubInstanceSettings(v);
|
||||
// console.log('GET', v, s);
|
||||
return Promise.resolve(s);
|
||||
},
|
||||
getInstanceSettings: getStubInstanceSettings,
|
||||
};
|
||||
},
|
||||
config: {
|
||||
buildInfo: {},
|
||||
panels: {},
|
||||
@ -48,7 +51,7 @@ describe('given dashboard with repeated panels', () => {
|
||||
{
|
||||
name: 'apps',
|
||||
type: 'query',
|
||||
datasource: 'gfdb',
|
||||
datasource: { uid: 'gfdb', type: 'testdb' },
|
||||
current: { value: 'Asd', text: 'Asd' },
|
||||
options: [{ value: 'Asd', text: 'Asd' }],
|
||||
},
|
||||
@ -72,22 +75,22 @@ describe('given dashboard with repeated panels', () => {
|
||||
list: [
|
||||
{
|
||||
name: 'logs',
|
||||
datasource: 'gfdb',
|
||||
datasource: { uid: 'gfdb', type: 'testdb' },
|
||||
},
|
||||
],
|
||||
},
|
||||
panels: [
|
||||
{ id: 6, datasource: 'gfdb', type: 'graph' },
|
||||
{ id: 6, datasource: { uid: 'gfdb', type: 'testdb' }, type: 'graph' },
|
||||
{ id: 7 },
|
||||
{
|
||||
id: 8,
|
||||
datasource: '-- Mixed --',
|
||||
targets: [{ datasource: 'other' }],
|
||||
datasource: { uid: '-- Mixed --', type: 'mixed' },
|
||||
targets: [{ datasource: { uid: 'other', type: 'other' } }],
|
||||
},
|
||||
{ id: 9, datasource: '$ds' },
|
||||
{ id: 9, datasource: { uid: '$ds', type: 'other2' } },
|
||||
{
|
||||
id: 17,
|
||||
datasource: '$ds',
|
||||
datasource: { uid: '$ds', type: 'other2' },
|
||||
type: 'graph',
|
||||
libraryPanel: {
|
||||
name: 'Library Panel 2',
|
||||
@ -97,7 +100,7 @@ describe('given dashboard with repeated panels', () => {
|
||||
{
|
||||
id: 2,
|
||||
repeat: 'apps',
|
||||
datasource: 'gfdb',
|
||||
datasource: { uid: 'gfdb', type: 'testdb' },
|
||||
type: 'graph',
|
||||
},
|
||||
{ id: 3, repeat: null, repeatPanelId: 2 },
|
||||
@ -105,24 +108,24 @@ describe('given dashboard with repeated panels', () => {
|
||||
id: 4,
|
||||
collapsed: true,
|
||||
panels: [
|
||||
{ id: 10, datasource: 'gfdb', type: 'table' },
|
||||
{ id: 10, datasource: { uid: 'gfdb', type: 'testdb' }, type: 'table' },
|
||||
{ id: 11 },
|
||||
{
|
||||
id: 12,
|
||||
datasource: '-- Mixed --',
|
||||
targets: [{ datasource: 'other' }],
|
||||
datasource: { uid: '-- Mixed --', type: 'mixed' },
|
||||
targets: [{ datasource: { uid: 'other', type: 'other' } }],
|
||||
},
|
||||
{ id: 13, datasource: '$ds' },
|
||||
{ id: 13, datasource: { uid: '$uid', type: 'other' } },
|
||||
{
|
||||
id: 14,
|
||||
repeat: 'apps',
|
||||
datasource: 'gfdb',
|
||||
datasource: { uid: 'gfdb', type: 'testdb' },
|
||||
type: 'heatmap',
|
||||
},
|
||||
{ id: 15, repeat: null, repeatPanelId: 14 },
|
||||
{
|
||||
id: 16,
|
||||
datasource: 'gfdb',
|
||||
datasource: { uid: 'gfdb', type: 'testdb' },
|
||||
type: 'graph',
|
||||
libraryPanel: {
|
||||
name: 'Library Panel',
|
||||
@ -264,7 +267,7 @@ describe('given dashboard with repeated panels', () => {
|
||||
expect(element.kind).toBe(LibraryElementKind.Panel);
|
||||
expect(element.model).toEqual({
|
||||
id: 17,
|
||||
datasource: '$ds',
|
||||
datasource: '${DS_OTHER2}',
|
||||
type: 'graph',
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
@ -287,6 +290,11 @@ describe('given dashboard with repeated panels', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function getStubInstanceSettings(v: string | DataSourceRef): DataSourceInstanceSettings {
|
||||
let key = (v as DataSourceRef)?.type ?? v;
|
||||
return (stubs[(key as any) ?? 'gfdb'] ?? stubs['gfdb']) as any;
|
||||
}
|
||||
|
||||
// Stub responses
|
||||
const stubs: { [key: string]: {} } = {};
|
||||
stubs['gfdb'] = {
|
||||
|
@ -75,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
import { QueryGroup } from 'app/features/query/components/QueryGroup';
|
||||
import { PanelModel } from '../../state';
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { QueryGroupOptions } from 'app/types';
|
||||
import { QueryGroupDataSource, QueryGroupOptions } from 'app/types';
|
||||
import { DataQuery } from '@grafana/data';
|
||||
|
||||
interface Props {
|
||||
@ -18,10 +18,17 @@ export class PanelEditorQueries extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
buildQueryOptions(panel: PanelModel): QueryGroupOptions {
|
||||
const dataSource: QueryGroupDataSource = panel.datasource?.uid
|
||||
? {
|
||||
default: false,
|
||||
...panel.datasource,
|
||||
}
|
||||
: {
|
||||
default: true,
|
||||
};
|
||||
|
||||
return {
|
||||
dataSource: {
|
||||
name: panel.datasource,
|
||||
},
|
||||
dataSource,
|
||||
queries: panel.targets,
|
||||
maxDataPoints: panel.maxDataPoints,
|
||||
minInterval: panel.interval,
|
||||
@ -47,8 +54,8 @@ export class PanelEditorQueries extends PureComponent<Props> {
|
||||
onOptionsChange = (options: QueryGroupOptions) => {
|
||||
const { panel } = this.props;
|
||||
|
||||
const newDataSourceName = options.dataSource.default ? null : options.dataSource.name!;
|
||||
const dataSourceChanged = newDataSourceName !== panel.datasource;
|
||||
const newDataSourceID = options.dataSource.default ? null : options.dataSource.uid!;
|
||||
const dataSourceChanged = newDataSourceID !== panel.datasource?.uid;
|
||||
panel.updateQueries(options);
|
||||
|
||||
if (dataSourceChanged) {
|
||||
|
@ -162,7 +162,7 @@ describe('DashboardModel', () => {
|
||||
});
|
||||
|
||||
it('dashboard schema version should be set to latest', () => {
|
||||
expect(model.schemaVersion).toBe(32);
|
||||
expect(model.schemaVersion).toBe(33);
|
||||
});
|
||||
|
||||
it('graph thresholds should be migrated', () => {
|
||||
|
@ -9,6 +9,7 @@ import { DashboardModel } from './DashboardModel';
|
||||
import {
|
||||
DataLink,
|
||||
DataLinkBuiltInVars,
|
||||
DataSourceRef,
|
||||
MappingType,
|
||||
SpecialValueMatch,
|
||||
PanelPlugin,
|
||||
@ -39,6 +40,8 @@ import { config } from 'app/core/config';
|
||||
import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module';
|
||||
import { plugin as gaugePanelPlugin } from 'app/plugins/panel/gauge/module';
|
||||
import { getStandardFieldConfigs, getStandardOptionEditors } from '@grafana/ui';
|
||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { labelsToFieldsTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/labelsToFields';
|
||||
import { mergeTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/merge';
|
||||
import {
|
||||
@ -62,7 +65,7 @@ export class DashboardMigrator {
|
||||
let i, j, k, n;
|
||||
const oldVersion = this.dashboard.schemaVersion;
|
||||
const panelUpgrades: PanelSchemeUpgradeHandler[] = [];
|
||||
this.dashboard.schemaVersion = 32;
|
||||
this.dashboard.schemaVersion = 33;
|
||||
|
||||
if (oldVersion === this.dashboard.schemaVersion) {
|
||||
return;
|
||||
@ -695,6 +698,45 @@ export class DashboardMigrator {
|
||||
this.migrateCloudWatchAnnotationQuery();
|
||||
}
|
||||
|
||||
// Replace datasource name with reference, uid and type
|
||||
if (oldVersion < 33) {
|
||||
for (const variable of this.dashboard.templating.list) {
|
||||
if (variable.type !== 'query') {
|
||||
continue;
|
||||
}
|
||||
let name = (variable as any).datasource as string;
|
||||
if (name) {
|
||||
variable.datasource = migrateDatasourceNameToRef(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Mutate panel models
|
||||
for (const panel of this.dashboard.panels) {
|
||||
let name = (panel as any).datasource as string;
|
||||
if (!name) {
|
||||
panel.datasource = null; // use default
|
||||
} else if (name === MIXED_DATASOURCE_NAME) {
|
||||
panel.datasource = { type: MIXED_DATASOURCE_NAME };
|
||||
for (const target of panel.targets) {
|
||||
name = (target as any).datasource as string;
|
||||
panel.datasource = migrateDatasourceNameToRef(name);
|
||||
}
|
||||
continue; // do not cleanup targets
|
||||
} else {
|
||||
panel.datasource = migrateDatasourceNameToRef(name);
|
||||
}
|
||||
|
||||
// cleanup query datasource references
|
||||
if (!panel.targets) {
|
||||
panel.targets = [];
|
||||
} else {
|
||||
for (const target of panel.targets) {
|
||||
delete target.datasource;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (panelUpgrades.length === 0) {
|
||||
return;
|
||||
}
|
||||
@ -1009,6 +1051,19 @@ function migrateSinglestat(panel: PanelModel) {
|
||||
return panel;
|
||||
}
|
||||
|
||||
export function migrateDatasourceNameToRef(name: string): DataSourceRef | null {
|
||||
if (!name || name === 'default') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ds = getDataSourceSrv().getInstanceSettings(name);
|
||||
if (!ds) {
|
||||
return { uid: name }; // not found
|
||||
}
|
||||
|
||||
return { type: ds.meta.id, uid: ds.uid };
|
||||
}
|
||||
|
||||
// mutates transformations appending a new transformer after the existing one
|
||||
function appendTransformerAfter(panel: PanelModel, id: string, cfg: DataTransformerConfig) {
|
||||
if (panel.transformations) {
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
ScopedVars,
|
||||
urlUtil,
|
||||
PanelModel as IPanelModel,
|
||||
DatasourceRef,
|
||||
DataSourceRef,
|
||||
} from '@grafana/data';
|
||||
import config from 'app/core/config';
|
||||
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
|
||||
@ -144,7 +144,7 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
panels?: any;
|
||||
declare targets: DataQuery[];
|
||||
transformations?: DataTransformerConfig[];
|
||||
datasource: DatasourceRef | null = null;
|
||||
datasource: DataSourceRef | null = null;
|
||||
thresholds?: any;
|
||||
pluginVersion?: string;
|
||||
|
||||
@ -442,7 +442,13 @@ export class PanelModel implements DataConfigSource, IPanelModel {
|
||||
}
|
||||
|
||||
updateQueries(options: QueryGroupOptions) {
|
||||
this.datasource = options.dataSource.default ? null : options.dataSource.name!;
|
||||
const { dataSource } = options;
|
||||
this.datasource = dataSource.default
|
||||
? null
|
||||
: {
|
||||
uid: dataSource.uid,
|
||||
type: dataSource.type,
|
||||
};
|
||||
this.timeFrom = options.timeRange?.from;
|
||||
this.timeShift = options.timeRange?.shift;
|
||||
this.hideTimeOverride = options.timeRange?.hide;
|
||||
|
@ -12,13 +12,15 @@ import { DataQuery } from '../../../../packages/grafana-data/src';
|
||||
function setup(queries: DataQuery[]) {
|
||||
const defaultDs = {
|
||||
name: 'newDs',
|
||||
uid: 'newDs-uid',
|
||||
meta: { id: 'newDs' },
|
||||
};
|
||||
|
||||
const datasources: Record<string, any> = {
|
||||
newDs: defaultDs,
|
||||
someDs: {
|
||||
'newDs-uid': defaultDs,
|
||||
'someDs-uid': {
|
||||
name: 'someDs',
|
||||
uid: 'someDs-uid',
|
||||
meta: { id: 'someDs' },
|
||||
components: {
|
||||
QueryEditor: () => 'someDs query editor',
|
||||
@ -30,11 +32,11 @@ function setup(queries: DataQuery[]) {
|
||||
getList() {
|
||||
return Object.values(datasources).map((d) => ({ name: d.name }));
|
||||
},
|
||||
getInstanceSettings(name: string) {
|
||||
return datasources[name] || defaultDs;
|
||||
getInstanceSettings(uid: string) {
|
||||
return datasources[uid] || defaultDs;
|
||||
},
|
||||
get(name?: string) {
|
||||
return Promise.resolve(name ? datasources[name] || defaultDs : defaultDs);
|
||||
get(uid?: string) {
|
||||
return Promise.resolve(uid ? datasources[uid] || defaultDs : defaultDs);
|
||||
},
|
||||
} as any);
|
||||
|
||||
@ -42,7 +44,7 @@ function setup(queries: DataQuery[]) {
|
||||
const initialState: ExploreState = {
|
||||
left: {
|
||||
...leftState,
|
||||
datasourceInstance: datasources.someDs,
|
||||
datasourceInstance: datasources['someDs-uid'],
|
||||
queries,
|
||||
},
|
||||
syncedTimes: false,
|
||||
|
@ -22,7 +22,7 @@ const makeSelectors = (exploreId: ExploreId) => {
|
||||
getEventBridge: createSelector(exploreItemSelector, (s) => s!.eventBridge),
|
||||
getDatasourceInstanceSettings: createSelector(
|
||||
exploreItemSelector,
|
||||
(s) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.name)!
|
||||
(s) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.uid)!
|
||||
),
|
||||
};
|
||||
};
|
||||
|
@ -317,10 +317,12 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
|
||||
return dsSettings.map((d) => d.settings);
|
||||
},
|
||||
getInstanceSettings(name: string) {
|
||||
return dsSettings.map((d) => d.settings).find((x) => x.name === name);
|
||||
return dsSettings.map((d) => d.settings).find((x) => x.name === name || x.uid === name);
|
||||
},
|
||||
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
|
||||
return Promise.resolve((name ? dsSettings.find((d) => d.api.name === name) : dsSettings[0])!.api);
|
||||
return Promise.resolve(
|
||||
(name ? dsSettings.find((d) => d.api.name === name || d.api.uid === name) : dsSettings[0])!.api
|
||||
);
|
||||
},
|
||||
} as any);
|
||||
|
||||
@ -392,7 +394,9 @@ function makeDatasourceSetup({ name = 'loki', id = 1 }: { name?: string; id?: nu
|
||||
},
|
||||
},
|
||||
name: name,
|
||||
uid: name,
|
||||
query: jest.fn(),
|
||||
getRef: jest.fn(),
|
||||
meta,
|
||||
} as any,
|
||||
};
|
||||
|
@ -62,6 +62,7 @@ function setup(state?: any) {
|
||||
query: jest.fn(),
|
||||
name: 'newDs',
|
||||
meta: { id: 'newDs' },
|
||||
getRef: () => ({ uid: 'newDs' }),
|
||||
},
|
||||
someDs: {
|
||||
testDatasource: jest.fn(),
|
||||
@ -69,6 +70,7 @@ function setup(state?: any) {
|
||||
query: jest.fn(),
|
||||
name: 'someDs',
|
||||
meta: { id: 'someDs' },
|
||||
getRef: () => ({ uid: 'someDs' }),
|
||||
},
|
||||
};
|
||||
|
||||
@ -77,7 +79,7 @@ function setup(state?: any) {
|
||||
return Object.values(datasources).map((d) => ({ name: d.name }));
|
||||
},
|
||||
getInstanceSettings(name: string) {
|
||||
return { name: 'hello' };
|
||||
return { name, getRef: () => ({ uid: name }) };
|
||||
},
|
||||
get(name?: string) {
|
||||
return Promise.resolve(
|
||||
|
@ -11,10 +11,10 @@ import { locationService } from '@grafana/runtime';
|
||||
const getNavigateToExploreContext = async (openInNewWindow?: (url: string) => void) => {
|
||||
const url = '/explore';
|
||||
const panel: Partial<PanelModel> = {
|
||||
datasource: 'mocked datasource',
|
||||
datasource: { uid: 'mocked datasource' },
|
||||
targets: [{ refId: 'A' }],
|
||||
};
|
||||
const datasource = new MockDataSourceApi(panel.datasource!);
|
||||
const datasource = new MockDataSourceApi(panel.datasource!.uid!);
|
||||
const get = jest.fn().mockResolvedValue(datasource);
|
||||
const getDataSourceSrv = jest.fn().mockReturnValue({ get });
|
||||
const getTimeSrv = jest.fn();
|
||||
|
@ -64,6 +64,7 @@ const defaultInitialState = {
|
||||
[ExploreId.left]: {
|
||||
datasourceInstance: {
|
||||
query: jest.fn(),
|
||||
getRef: jest.fn(),
|
||||
meta: {
|
||||
id: 'something',
|
||||
},
|
||||
@ -160,8 +161,8 @@ describe('importing queries', () => {
|
||||
importQueries(
|
||||
ExploreId.left,
|
||||
[
|
||||
{ datasource: 'postgres1', refId: 'refId_A' },
|
||||
{ datasource: 'postgres1', refId: 'refId_B' },
|
||||
{ datasource: { type: 'postgresql' }, refId: 'refId_A' },
|
||||
{ datasource: { type: 'postgresql' }, refId: 'refId_B' },
|
||||
],
|
||||
{ name: 'Postgres1', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
|
||||
{ name: 'Postgres2', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}>
|
||||
@ -342,6 +343,7 @@ describe('reducer', () => {
|
||||
...defaultInitialState.explore[ExploreId.left],
|
||||
datasourceInstance: {
|
||||
query: jest.fn(),
|
||||
getRef: jest.fn(),
|
||||
meta: {
|
||||
id: 'something',
|
||||
},
|
||||
|
@ -342,7 +342,7 @@ export const runQueries = (
|
||||
|
||||
const queries = exploreItemState.queries.map((query) => ({
|
||||
...query,
|
||||
datasource: query.datasource || datasourceInstance?.name,
|
||||
datasource: query.datasource || datasourceInstance?.getRef(),
|
||||
}));
|
||||
|
||||
const cachedValue = getResultsFromCache(cache, absoluteRange);
|
||||
|
@ -7,7 +7,7 @@ import { DataSourceWithBackend } from '@grafana/runtime';
|
||||
* This is a singleton instance that just pretends to be a DataSource
|
||||
*/
|
||||
export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQuery> {
|
||||
constructor(instanceSettings: DataSourceInstanceSettings) {
|
||||
constructor(public instanceSettings: DataSourceInstanceSettings) {
|
||||
super(instanceSettings);
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQue
|
||||
return {
|
||||
refId: '--', // Replaced with query
|
||||
type: query?.type ?? ExpressionQueryType.math,
|
||||
datasource: ExpressionDatasourceID,
|
||||
datasource: ExpressionDatasourceRef,
|
||||
conditions: query?.conditions ?? undefined,
|
||||
};
|
||||
}
|
||||
@ -28,6 +28,10 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQue
|
||||
// MATCHES the constant in DataSourceWithBackend
|
||||
export const ExpressionDatasourceID = '__expr__';
|
||||
export const ExpressionDatasourceUID = '-100';
|
||||
export const ExpressionDatasourceRef = Object.freeze({
|
||||
type: ExpressionDatasourceID,
|
||||
uid: ExpressionDatasourceID,
|
||||
});
|
||||
|
||||
export const instanceSettings: DataSourceInstanceSettings = {
|
||||
id: -100,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Input, Field, Button, ValuePicker, HorizontalGroup } from '@grafana/ui';
|
||||
import { DataSourcePicker, getBackendSrv } from '@grafana/runtime';
|
||||
import { AppEvents, DatasourceRef, LiveChannelScope, SelectableValue } from '@grafana/data';
|
||||
import { AppEvents, DataSourceRef, LiveChannelScope, SelectableValue } from '@grafana/data';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { Rule } from './types';
|
||||
|
||||
@ -28,7 +28,7 @@ export function AddNewRule({ onRuleAdded }: Props) {
|
||||
const [patternType, setPatternType] = useState<PatternType>();
|
||||
const [pattern, setPattern] = useState<string>();
|
||||
const [patternPrefix, setPatternPrefix] = useState<string>('');
|
||||
const [datasource, setDatasource] = useState<DatasourceRef>();
|
||||
const [datasource, setDatasource] = useState<DataSourceRef>();
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!pattern) {
|
||||
@ -85,7 +85,7 @@ export function AddNewRule({ onRuleAdded }: Props) {
|
||||
<DataSourcePicker
|
||||
current={datasource}
|
||||
onChange={(ds) => {
|
||||
setDatasource(ds.name);
|
||||
setDatasource(ds);
|
||||
setPatternPrefix(`${LiveChannelScope.DataSource}/${ds.uid}/`);
|
||||
}}
|
||||
/>
|
||||
|
@ -9,7 +9,14 @@ import {
|
||||
TemplateSrv,
|
||||
} from '@grafana/runtime';
|
||||
// Types
|
||||
import { AppEvents, DataSourceApi, DataSourceInstanceSettings, DataSourceSelectItem, ScopedVars } from '@grafana/data';
|
||||
import {
|
||||
AppEvents,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataSourceRef,
|
||||
DataSourceSelectItem,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { auto } from 'angular';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
// Pretend Datasource
|
||||
@ -23,11 +30,11 @@ import { DataSourceVariableModel } from '../variables/types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
export class DatasourceSrv implements DataSourceService {
|
||||
private datasources: Record<string, DataSourceApi> = {};
|
||||
private datasources: Record<string, DataSourceApi> = {}; // UID
|
||||
private settingsMapByName: Record<string, DataSourceInstanceSettings> = {};
|
||||
private settingsMapByUid: Record<string, DataSourceInstanceSettings> = {};
|
||||
private settingsMapById: Record<string, DataSourceInstanceSettings> = {};
|
||||
private defaultName = '';
|
||||
private defaultName = ''; // actually UID
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
@ -43,22 +50,39 @@ export class DatasourceSrv implements DataSourceService {
|
||||
this.defaultName = defaultName;
|
||||
|
||||
for (const dsSettings of Object.values(settingsMapByName)) {
|
||||
if (!dsSettings.uid) {
|
||||
dsSettings.uid = dsSettings.name; // -- Grafana --, -- Mixed etc
|
||||
}
|
||||
|
||||
this.settingsMapByUid[dsSettings.uid] = dsSettings;
|
||||
this.settingsMapById[dsSettings.id] = dsSettings;
|
||||
}
|
||||
|
||||
// Preload expressions
|
||||
this.datasources[ExpressionDatasourceID] = expressionDatasource as any;
|
||||
this.datasources[ExpressionDatasourceUID] = expressionDatasource as any;
|
||||
this.settingsMapByUid[ExpressionDatasourceID] = expressionInstanceSettings;
|
||||
this.settingsMapByUid[ExpressionDatasourceUID] = expressionInstanceSettings;
|
||||
}
|
||||
|
||||
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined {
|
||||
return this.settingsMapByUid[uid];
|
||||
}
|
||||
|
||||
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined {
|
||||
if (nameOrUid === 'default' || nameOrUid === null || nameOrUid === undefined) {
|
||||
return this.settingsMapByName[this.defaultName];
|
||||
}
|
||||
getInstanceSettings(ref: string | null | undefined | DataSourceRef): DataSourceInstanceSettings | undefined {
|
||||
const isstring = typeof ref === 'string';
|
||||
let nameOrUid = isstring ? (ref as string) : ((ref as any)?.uid as string | undefined);
|
||||
|
||||
if (nameOrUid === ExpressionDatasourceID || nameOrUid === ExpressionDatasourceUID) {
|
||||
return expressionInstanceSettings;
|
||||
if (nameOrUid === 'default' || nameOrUid === null || nameOrUid === undefined) {
|
||||
if (!isstring && ref) {
|
||||
const type = (ref as any)?.type as string;
|
||||
if (type === ExpressionDatasourceID) {
|
||||
return expressionDatasource.instanceSettings;
|
||||
} else if (type) {
|
||||
console.log('FIND Default instance for datasource type?', ref);
|
||||
}
|
||||
}
|
||||
return this.settingsMapByUid[this.defaultName] ?? this.settingsMapByName[this.defaultName];
|
||||
}
|
||||
|
||||
// Complex logic to support template variable data source names
|
||||
@ -89,15 +113,16 @@ export class DatasourceSrv implements DataSourceService {
|
||||
return this.settingsMapByUid[nameOrUid] ?? this.settingsMapByName[nameOrUid];
|
||||
}
|
||||
|
||||
get(nameOrUid?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
|
||||
get(ref?: string | DataSourceRef | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
|
||||
let nameOrUid = typeof ref === 'string' ? (ref as string) : ((ref as any)?.uid as string | undefined);
|
||||
if (!nameOrUid) {
|
||||
return this.get(this.defaultName);
|
||||
}
|
||||
|
||||
// Check if nameOrUid matches a uid and then get the name
|
||||
const byUid = this.settingsMapByUid[nameOrUid];
|
||||
if (byUid) {
|
||||
nameOrUid = byUid.name;
|
||||
const byName = this.settingsMapByName[nameOrUid];
|
||||
if (byName) {
|
||||
nameOrUid = byName.uid;
|
||||
}
|
||||
|
||||
// This check is duplicated below, this is here mainly as performance optimization to skip interpolation
|
||||
@ -119,26 +144,22 @@ export class DatasourceSrv implements DataSourceService {
|
||||
return this.loadDatasource(nameOrUid);
|
||||
}
|
||||
|
||||
async loadDatasource(name: string): Promise<DataSourceApi<any, any>> {
|
||||
// Expression Datasource (not a real datasource)
|
||||
if (name === ExpressionDatasourceID || name === ExpressionDatasourceUID) {
|
||||
this.datasources[name] = expressionDatasource as any;
|
||||
return Promise.resolve(expressionDatasource);
|
||||
async loadDatasource(key: string): Promise<DataSourceApi<any, any>> {
|
||||
if (this.datasources[key]) {
|
||||
return Promise.resolve(this.datasources[key]);
|
||||
}
|
||||
|
||||
let dsConfig = this.settingsMapByName[name];
|
||||
// find the metadata
|
||||
const dsConfig = this.settingsMapByUid[key] ?? this.settingsMapByName[key] ?? this.settingsMapById[key];
|
||||
if (!dsConfig) {
|
||||
dsConfig = this.settingsMapById[name];
|
||||
if (!dsConfig) {
|
||||
return Promise.reject({ message: `Datasource named ${name} was not found` });
|
||||
}
|
||||
return Promise.reject({ message: `Datasource ${key} was not found` });
|
||||
}
|
||||
|
||||
try {
|
||||
const dsPlugin = await importDataSourcePlugin(dsConfig.meta);
|
||||
// check if its in cache now
|
||||
if (this.datasources[name]) {
|
||||
return this.datasources[name];
|
||||
if (this.datasources[key]) {
|
||||
return this.datasources[key];
|
||||
}
|
||||
|
||||
// If there is only one constructor argument it is instanceSettings
|
||||
@ -153,11 +174,14 @@ export class DatasourceSrv implements DataSourceService {
|
||||
instance.meta = dsConfig.meta;
|
||||
|
||||
// store in instance cache
|
||||
this.datasources[name] = instance;
|
||||
this.datasources[key] = instance;
|
||||
this.datasources[instance.uid] = instance;
|
||||
return instance;
|
||||
} catch (err) {
|
||||
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` });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -221,6 +221,7 @@ describe('datasource_srv', () => {
|
||||
},
|
||||
"name": "-- Mixed --",
|
||||
"type": "test-db",
|
||||
"uid": "-- Mixed --",
|
||||
},
|
||||
Object {
|
||||
"meta": Object {
|
||||
@ -230,6 +231,7 @@ describe('datasource_srv', () => {
|
||||
},
|
||||
"name": "-- Dashboard --",
|
||||
"type": "dashboard",
|
||||
"uid": "-- Dashboard --",
|
||||
},
|
||||
Object {
|
||||
"meta": Object {
|
||||
@ -239,6 +241,7 @@ describe('datasource_srv', () => {
|
||||
},
|
||||
"name": "-- Grafana --",
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -121,7 +121,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
|
||||
getQueryDataSourceIdentifier(): string | null | undefined {
|
||||
const { query, dataSource: dsSettings } = this.props;
|
||||
return query.datasource ?? dsSettings.name;
|
||||
return query.datasource?.uid ?? dsSettings.uid;
|
||||
}
|
||||
|
||||
async loadDatasource() {
|
||||
|
@ -3,12 +3,20 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { Props, QueryEditorRowHeader } from './QueryEditorRowHeader';
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
|
||||
|
||||
const mockDS = mockDataSource({
|
||||
name: 'CloudManager',
|
||||
type: DataSourceType.Alertmanager,
|
||||
});
|
||||
|
||||
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
|
||||
return {
|
||||
getDataSourceSrv: () => ({
|
||||
getInstanceSettings: jest.fn(),
|
||||
getList: jest.fn().mockReturnValue([]),
|
||||
get: () => Promise.resolve(mockDS),
|
||||
getList: () => [mockDS],
|
||||
getInstanceSettings: () => mockDS,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
@ -67,7 +67,7 @@ export class QueryEditorRows extends PureComponent<Props> {
|
||||
if (previous?.type === dataSource.type) {
|
||||
return {
|
||||
...item,
|
||||
datasource: dataSource.name,
|
||||
datasource: { uid: dataSource.uid },
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -75,7 +75,7 @@ export class QueryEditorRows extends PureComponent<Props> {
|
||||
return {
|
||||
refId: item.refId,
|
||||
hide: item.hide,
|
||||
datasource: dataSource.name,
|
||||
datasource: { uid: dataSource.uid },
|
||||
};
|
||||
})
|
||||
);
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
DataQuery,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataSourceRef,
|
||||
getDefaultTimeRange,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
@ -91,7 +92,8 @@ export class QueryGroup extends PureComponent<Props, State> {
|
||||
const ds = await this.dataSourceSrv.get(options.dataSource.name);
|
||||
const dsSettings = this.dataSourceSrv.getInstanceSettings(options.dataSource.name);
|
||||
const defaultDataSource = await this.dataSourceSrv.get();
|
||||
const queries = options.queries.map((q) => (q.datasource ? q : { ...q, datasource: dsSettings?.name }));
|
||||
const datasource: DataSourceRef = { type: ds.type, uid: ds.uid };
|
||||
const queries = options.queries.map((q) => (q.datasource ? q : { ...q, datasource }));
|
||||
this.setState({ queries, dataSource: ds, dsSettings, defaultDataSource });
|
||||
} catch (error) {
|
||||
console.log('failed to load data source', error);
|
||||
@ -119,6 +121,7 @@ export class QueryGroup extends PureComponent<Props, State> {
|
||||
dataSource: {
|
||||
name: newSettings.name,
|
||||
uid: newSettings.uid,
|
||||
type: newSettings.meta.id,
|
||||
default: newSettings.isDefault,
|
||||
},
|
||||
});
|
||||
@ -139,12 +142,10 @@ export class QueryGroup extends PureComponent<Props, State> {
|
||||
newQuery(): Partial<DataQuery> {
|
||||
const { dsSettings, defaultDataSource } = this.state;
|
||||
|
||||
if (!dsSettings?.meta.mixed) {
|
||||
return { datasource: dsSettings?.name };
|
||||
}
|
||||
const ds = !dsSettings?.meta.mixed ? dsSettings : defaultDataSource;
|
||||
|
||||
return {
|
||||
datasource: defaultDataSource?.name,
|
||||
datasource: { uid: ds?.uid, type: ds?.type },
|
||||
};
|
||||
}
|
||||
|
||||
@ -182,7 +183,7 @@ export class QueryGroup extends PureComponent<Props, State> {
|
||||
<div className={styles.dataSourceRowItem}>
|
||||
<DataSourcePicker
|
||||
onChange={this.onChangeDataSource}
|
||||
current={options.dataSource.name}
|
||||
current={options.dataSource}
|
||||
metrics={true}
|
||||
mixed={true}
|
||||
dashboard={true}
|
||||
@ -258,7 +259,7 @@ export class QueryGroup extends PureComponent<Props, State> {
|
||||
|
||||
onAddQuery = (query: Partial<DataQuery>) => {
|
||||
const { dsSettings, queries } = this.state;
|
||||
this.onQueriesChange(addQuery(queries, query, dsSettings?.name));
|
||||
this.onQueriesChange(addQuery(queries, query, { type: dsSettings?.type, uid: dsSettings?.uid }));
|
||||
this.onScrollBottom();
|
||||
};
|
||||
|
||||
|
@ -108,6 +108,7 @@ function describeQueryRunnerScenario(
|
||||
|
||||
const datasource: any = {
|
||||
name: 'TestDB',
|
||||
uid: 'TestDB-uid',
|
||||
interval: ctx.dsInterval,
|
||||
query: (options: grafanaData.DataQueryRequest) => {
|
||||
ctx.queryCalledWith = options;
|
||||
@ -156,8 +157,8 @@ describe('PanelQueryRunner', () => {
|
||||
expect(ctx.queryCalledWith?.requestId).toBe('Q100');
|
||||
});
|
||||
|
||||
it('should set datasource name on request', async () => {
|
||||
expect(ctx.queryCalledWith?.targets[0].datasource).toBe('TestDB');
|
||||
it('should set datasource uid on request', async () => {
|
||||
expect(ctx.queryCalledWith?.targets[0].datasource?.uid).toBe('TestDB-uid');
|
||||
});
|
||||
|
||||
it('should pass scopedVars to datasource with interval props', async () => {
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
DataQueryRequest,
|
||||
DataSourceApi,
|
||||
DataSourceJsonData,
|
||||
DataSourceRef,
|
||||
DataTransformerConfig,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
@ -38,7 +39,7 @@ export interface QueryRunnerOptions<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
> {
|
||||
datasource: string | DataSourceApi<TQuery, TOptions> | null;
|
||||
datasource: DataSourceRef | DataSourceApi<TQuery, TOptions> | null;
|
||||
queries: TQuery[];
|
||||
panelId?: number;
|
||||
dashboardId?: number;
|
||||
@ -223,7 +224,7 @@ export class PanelQueryRunner {
|
||||
// Attach the data source name to each query
|
||||
request.targets = request.targets.map((query) => {
|
||||
if (!query.datasource) {
|
||||
query.datasource = ds.name;
|
||||
query.datasource = { uid: ds.uid };
|
||||
}
|
||||
return query;
|
||||
});
|
||||
@ -326,7 +327,7 @@ export class PanelQueryRunner {
|
||||
}
|
||||
|
||||
async function getDataSource(
|
||||
datasource: string | DataSourceApi | null,
|
||||
datasource: DataSourceRef | string | DataSourceApi | null,
|
||||
scopedVars: ScopedVars
|
||||
): Promise<DataSourceApi> {
|
||||
if (datasource && (datasource as any).query) {
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
QueryRunnerOptions,
|
||||
QueryRunner as QueryRunnerSrv,
|
||||
LoadingState,
|
||||
DataSourceRef,
|
||||
} from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
@ -78,7 +79,7 @@ export class QueryRunner implements QueryRunnerSrv {
|
||||
// Attach the datasource name to each query
|
||||
request.targets = request.targets.map((query) => {
|
||||
if (!query.datasource) {
|
||||
query.datasource = ds.name;
|
||||
query.datasource = ds.getRef();
|
||||
}
|
||||
return query;
|
||||
});
|
||||
@ -140,11 +141,11 @@ export class QueryRunner implements QueryRunnerSrv {
|
||||
}
|
||||
|
||||
async function getDataSource(
|
||||
datasource: string | DataSourceApi | null,
|
||||
datasource: DataSourceRef | DataSourceApi | null,
|
||||
scopedVars: ScopedVars
|
||||
): Promise<DataSourceApi> {
|
||||
if (datasource && (datasource as any).query) {
|
||||
return datasource as DataSourceApi;
|
||||
}
|
||||
return await getDatasourceSrv().get(datasource as string, scopedVars);
|
||||
return await getDatasourceSrv().get(datasource, scopedVars);
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export const TestStuffPage: FC = () => {
|
||||
|
||||
queryRunner.run({
|
||||
queries: queryOptions.queries,
|
||||
datasource: queryOptions.dataSource.name!,
|
||||
datasource: queryOptions.dataSource,
|
||||
timezone: 'browser',
|
||||
timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange },
|
||||
maxDataPoints: queryOptions.maxDataPoints ?? 100,
|
||||
|
@ -6,6 +6,8 @@ import { createQueryVariableAdapter } from '../variables/query/adapter';
|
||||
import { createAdHocVariableAdapter } from '../variables/adhoc/adapter';
|
||||
import { VariableModel } from '../variables/types';
|
||||
import { FormatRegistryID } from './formatRegistry';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { mockDataSource, MockDataSourceSrv } from '../alerting/unified/mocks';
|
||||
|
||||
variableAdapters.setInit(() => [
|
||||
(createQueryVariableAdapter() as unknown) as VariableAdapter<VariableModel>,
|
||||
@ -119,9 +121,17 @@ describe('templateSrv', () => {
|
||||
name: 'ds',
|
||||
current: { value: 'logstash', text: 'logstash' },
|
||||
},
|
||||
{ type: 'adhoc', name: 'test', datasource: 'oogle', filters: [1] },
|
||||
{ type: 'adhoc', name: 'test2', datasource: '$ds', filters: [2] },
|
||||
{ type: 'adhoc', name: 'test', datasource: { uid: 'oogle' }, filters: [1] },
|
||||
{ type: 'adhoc', name: 'test2', datasource: { uid: '$ds' }, filters: [2] },
|
||||
]);
|
||||
setDataSourceSrv(
|
||||
new MockDataSourceSrv({
|
||||
oogle: mockDataSource({
|
||||
name: 'oogle',
|
||||
uid: 'oogle',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return filters if datasourceName match', () => {
|
||||
|
@ -3,8 +3,8 @@ import { deprecationWarning, ScopedVars, TimeRange } from '@grafana/data';
|
||||
import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors';
|
||||
import { variableRegex } from '../variables/utils';
|
||||
import { isAdHoc } from '../variables/guard';
|
||||
import { VariableModel } from '../variables/types';
|
||||
import { setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
|
||||
import { AdHocVariableFilter, AdHocVariableModel, VariableModel } from '../variables/types';
|
||||
import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
|
||||
import { FormatOptions, formatRegistry, FormatRegistryID } from './formatRegistry';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/state/types';
|
||||
import { safeStringifyValue } from '../../core/utils/explore';
|
||||
@ -92,14 +92,21 @@ export class TemplateSrv implements BaseTemplateSrv {
|
||||
this.index[variable.name] = variable;
|
||||
}
|
||||
|
||||
getAdhocFilters(datasourceName: string) {
|
||||
getAdhocFilters(datasourceName: string): AdHocVariableFilter[] {
|
||||
let filters: any = [];
|
||||
let ds = getDataSourceSrv().getInstanceSettings(datasourceName);
|
||||
|
||||
if (!ds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const variable of this.getAdHocVariables()) {
|
||||
if (variable.datasource === null || variable.datasource === datasourceName) {
|
||||
const variableUid = variable.datasource?.uid;
|
||||
|
||||
if (variableUid === ds.uid || (variable.datasource == null && ds?.isDefault)) {
|
||||
filters = filters.concat(variable.filters);
|
||||
} else if (variable.datasource.indexOf('$') === 0) {
|
||||
if (this.replace(variable.datasource) === datasourceName) {
|
||||
} else if (variableUid?.indexOf('$') === 0) {
|
||||
if (this.replace(variableUid) === datasourceName) {
|
||||
filters = filters.concat(variable.filters);
|
||||
}
|
||||
}
|
||||
@ -334,8 +341,8 @@ export class TemplateSrv implements BaseTemplateSrv {
|
||||
return this.index[name];
|
||||
}
|
||||
|
||||
private getAdHocVariables(): any[] {
|
||||
return this.dependencies.getFilteredVariables(isAdHoc);
|
||||
private getAdHocVariables(): AdHocVariableModel[] {
|
||||
return this.dependencies.getFilteredVariables(isAdHoc) as AdHocVariableModel[];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { Alert, InlineFieldRow, VerticalGroup } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { DataSourceRef, SelectableValue } from '@grafana/data';
|
||||
|
||||
import { AdHocVariableModel } from '../types';
|
||||
import { VariableEditorProps } from '../editor/types';
|
||||
@ -32,7 +32,7 @@ export class AdHocVariableEditorUnConnected extends PureComponent<Props> {
|
||||
this.props.initAdHocVariableEditor();
|
||||
}
|
||||
|
||||
onDatasourceChanged = (option: SelectableValue<string>) => {
|
||||
onDatasourceChanged = (option: SelectableValue<DataSourceRef>) => {
|
||||
this.props.changeVariableDatasource(option.value);
|
||||
};
|
||||
|
||||
@ -40,7 +40,7 @@ export class AdHocVariableEditorUnConnected extends PureComponent<Props> {
|
||||
const { variable, editor } = this.props;
|
||||
const dataSources = editor.extended?.dataSources ?? [];
|
||||
const infoText = editor.extended?.infoText ?? null;
|
||||
const options = dataSources.map((ds) => ({ label: ds.text, value: ds.value }));
|
||||
const options = dataSources.map((ds) => ({ label: ds.text, value: { uid: ds.value } }));
|
||||
const value = options.find((o) => o.value === variable.datasource) ?? options[0];
|
||||
|
||||
return (
|
||||
|
@ -39,7 +39,7 @@ describe('adhoc actions', () => {
|
||||
describe('when applyFilterFromTable is dispatched and filter already exist', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const options: AdHocTableOptions = {
|
||||
datasource: 'influxdb',
|
||||
datasource: { uid: 'influxdb' },
|
||||
key: 'filter-key',
|
||||
value: 'filter-value',
|
||||
operator: '=',
|
||||
@ -76,7 +76,7 @@ describe('adhoc actions', () => {
|
||||
describe('when applyFilterFromTable is dispatched and previously no variable or filter exists', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const options: AdHocTableOptions = {
|
||||
datasource: 'influxdb',
|
||||
datasource: { uid: 'influxdb' },
|
||||
key: 'filter-key',
|
||||
value: 'filter-value',
|
||||
operator: '=',
|
||||
@ -103,7 +103,7 @@ describe('adhoc actions', () => {
|
||||
describe('when applyFilterFromTable is dispatched and previously no filter exists', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const options: AdHocTableOptions = {
|
||||
datasource: 'influxdb',
|
||||
datasource: { uid: 'influxdb' },
|
||||
key: 'filter-key',
|
||||
value: 'filter-value',
|
||||
operator: '=',
|
||||
@ -132,7 +132,7 @@ describe('adhoc actions', () => {
|
||||
describe('when applyFilterFromTable is dispatched and adhoc variable with other datasource exists', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const options: AdHocTableOptions = {
|
||||
datasource: 'influxdb',
|
||||
datasource: { uid: 'influxdb' },
|
||||
key: 'filter-key',
|
||||
value: 'filter-value',
|
||||
operator: '=',
|
||||
@ -141,7 +141,7 @@ describe('adhoc actions', () => {
|
||||
const existing = adHocBuilder()
|
||||
.withId('elastic-filter')
|
||||
.withName('elastic-filter')
|
||||
.withDatasource('elasticsearch')
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
.build();
|
||||
|
||||
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource(options.datasource).build();
|
||||
@ -181,7 +181,7 @@ describe('adhoc actions', () => {
|
||||
.withId('elastic-filter')
|
||||
.withName('elastic-filter')
|
||||
.withFilters([existing])
|
||||
.withDatasource('elasticsearch')
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
.build();
|
||||
|
||||
const update = { index: 0, filter: updated };
|
||||
@ -218,7 +218,7 @@ describe('adhoc actions', () => {
|
||||
.withId('elastic-filter')
|
||||
.withName('elastic-filter')
|
||||
.withFilters([existing])
|
||||
.withDatasource('elasticsearch')
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
@ -247,7 +247,7 @@ describe('adhoc actions', () => {
|
||||
.withId('elastic-filter')
|
||||
.withName('elastic-filter')
|
||||
.withFilters([])
|
||||
.withDatasource('elasticsearch')
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
@ -268,7 +268,7 @@ describe('adhoc actions', () => {
|
||||
.withId('elastic-filter')
|
||||
.withName('elastic-filter')
|
||||
.withFilters([])
|
||||
.withDatasource('elasticsearch')
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
@ -296,7 +296,7 @@ describe('adhoc actions', () => {
|
||||
.withId('elastic-filter')
|
||||
.withName('elastic-filter')
|
||||
.withFilters([filter])
|
||||
.withDatasource('elasticsearch')
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
@ -324,7 +324,7 @@ describe('adhoc actions', () => {
|
||||
.withId('elastic-filter')
|
||||
.withName('elastic-filter')
|
||||
.withFilters([existing])
|
||||
.withDatasource('elasticsearch')
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
.build();
|
||||
|
||||
const fromUrl = [
|
||||
@ -382,9 +382,9 @@ describe('adhoc actions', () => {
|
||||
|
||||
describe('when changeVariableDatasource is dispatched with unsupported datasource', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const datasource = 'mysql';
|
||||
const datasource = { uid: 'mysql' };
|
||||
const loadingText = 'Ad hoc filters are applied automatically to all queries that target this data source';
|
||||
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource('influxdb').build();
|
||||
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource({ uid: 'influxdb' }).build();
|
||||
|
||||
getDatasource.mockRestore();
|
||||
getDatasource.mockResolvedValue(null);
|
||||
@ -408,9 +408,9 @@ describe('adhoc actions', () => {
|
||||
|
||||
describe('when changeVariableDatasource is dispatched with datasource', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const datasource = 'elasticsearch';
|
||||
const datasource = { uid: 'elasticsearch' };
|
||||
const loadingText = 'Ad hoc filters are applied automatically to all queries that target this data source';
|
||||
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource('influxdb').build();
|
||||
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource({ uid: 'influxdb' }).build();
|
||||
|
||||
getDatasource.mockRestore();
|
||||
getDatasource.mockResolvedValue({
|
||||
|
@ -16,9 +16,10 @@ import {
|
||||
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types';
|
||||
import { variableUpdated } from '../state/actions';
|
||||
import { isAdHoc } from '../guard';
|
||||
import { DataSourceRef } from '@grafana/data';
|
||||
|
||||
export interface AdHocTableOptions {
|
||||
datasource: string;
|
||||
datasource: DataSourceRef;
|
||||
key: string;
|
||||
value: string;
|
||||
operator: string;
|
||||
@ -29,6 +30,7 @@ const filterTableName = 'Filters';
|
||||
export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
let variable = getVariableByOptions(options, getState());
|
||||
console.log('getVariableByOptions', options, getState().templating.variables);
|
||||
|
||||
if (!variable) {
|
||||
dispatch(createAdHocVariable(options));
|
||||
@ -80,7 +82,7 @@ export const setFiltersFromUrl = (id: string, filters: AdHocVariableFilter[]): T
|
||||
};
|
||||
};
|
||||
|
||||
export const changeVariableDatasource = (datasource?: string): ThunkResult<void> => {
|
||||
export const changeVariableDatasource = (datasource?: DataSourceRef): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const { editor } = getState().templating;
|
||||
const variable = getVariable(editor.id, getState());
|
||||
@ -155,6 +157,6 @@ const createAdHocVariable = (options: AdHocTableOptions): ThunkResult<void> => {
|
||||
|
||||
const getVariableByOptions = (options: AdHocTableOptions, state: StoreState): AdHocVariableModel => {
|
||||
return Object.values(state.templating.variables).find(
|
||||
(v) => isAdHoc(v) && v.datasource === options.datasource
|
||||
(v) => isAdHoc(v) && v.datasource?.uid === options.datasource.uid
|
||||
) as AdHocVariableModel;
|
||||
};
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { AdHocVariableFilter } from 'app/features/variables/types';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { DataSourceRef, SelectableValue } from '@grafana/data';
|
||||
import { AdHocFilterKey, REMOVE_FILTER_KEY } from './AdHocFilterKey';
|
||||
import { AdHocFilterRenderer } from './AdHocFilterRenderer';
|
||||
|
||||
interface Props {
|
||||
datasource: string;
|
||||
datasource: DataSourceRef;
|
||||
onCompleted: (filter: AdHocVariableFilter) => void;
|
||||
appendBefore?: React.ReactNode;
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { FC, ReactElement } from 'react';
|
||||
import { Icon, SegmentAsync } from '@grafana/ui';
|
||||
import { getDatasourceSrv } from '../../../plugins/datasource_srv';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { DataSourceRef, SelectableValue } from '@grafana/data';
|
||||
|
||||
interface Props {
|
||||
datasource: string;
|
||||
datasource: DataSourceRef;
|
||||
filterKey: string | null;
|
||||
onChange: (item: SelectableValue<string | null>) => void;
|
||||
}
|
||||
@ -51,7 +51,7 @@ const plusSegment: ReactElement = (
|
||||
</a>
|
||||
);
|
||||
|
||||
const fetchFilterKeys = async (datasource: string): Promise<Array<SelectableValue<string>>> => {
|
||||
const fetchFilterKeys = async (datasource: DataSourceRef): Promise<Array<SelectableValue<string>>> => {
|
||||
const ds = await getDatasourceSrv().get(datasource);
|
||||
|
||||
if (!ds || !ds.getTagKeys) {
|
||||
@ -62,7 +62,7 @@ const fetchFilterKeys = async (datasource: string): Promise<Array<SelectableValu
|
||||
return metrics.map((m) => ({ label: m.text, value: m.text }));
|
||||
};
|
||||
|
||||
const fetchFilterKeysWithRemove = async (datasource: string): Promise<Array<SelectableValue<string>>> => {
|
||||
const fetchFilterKeysWithRemove = async (datasource: DataSourceRef): Promise<Array<SelectableValue<string>>> => {
|
||||
const keys = await fetchFilterKeys(datasource);
|
||||
return [REMOVE_VALUE, ...keys];
|
||||
};
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React, { FC } from 'react';
|
||||
import { OperatorSegment } from './OperatorSegment';
|
||||
import { AdHocVariableFilter } from 'app/features/variables/types';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { DataSourceRef, SelectableValue } from '@grafana/data';
|
||||
import { AdHocFilterKey } from './AdHocFilterKey';
|
||||
import { AdHocFilterValue } from './AdHocFilterValue';
|
||||
|
||||
interface Props {
|
||||
datasource: string;
|
||||
datasource: DataSourceRef;
|
||||
filter: AdHocVariableFilter;
|
||||
onKeyChange: (item: SelectableValue<string | null>) => void;
|
||||
onOperatorChange: (item: SelectableValue<string>) => void;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { FC } from 'react';
|
||||
import { SegmentAsync } from '@grafana/ui';
|
||||
import { getDatasourceSrv } from '../../../plugins/datasource_srv';
|
||||
import { MetricFindValue, SelectableValue } from '@grafana/data';
|
||||
import { DataSourceRef, MetricFindValue, SelectableValue } from '@grafana/data';
|
||||
|
||||
interface Props {
|
||||
datasource: string;
|
||||
datasource: DataSourceRef;
|
||||
filterKey: string;
|
||||
filterValue?: string;
|
||||
onChange: (item: SelectableValue<string>) => void;
|
||||
@ -27,7 +27,7 @@ export const AdHocFilterValue: FC<Props> = ({ datasource, onChange, filterKey, f
|
||||
);
|
||||
};
|
||||
|
||||
const fetchFilterValues = async (datasource: string, key: string): Promise<Array<SelectableValue<string>>> => {
|
||||
const fetchFilterValues = async (datasource: DataSourceRef, key: string): Promise<Array<SelectableValue<string>>> => {
|
||||
const ds = await getDatasourceSrv().get(datasource);
|
||||
|
||||
if (!ds || !ds.getTagValues) {
|
||||
|
@ -9,7 +9,8 @@ import { initialVariableEditorState } from '../editor/reducer';
|
||||
import { describe, expect } from '../../../../test/lib/common';
|
||||
import { NEW_VARIABLE_ID } from '../state/types';
|
||||
import { LegacyVariableQueryEditor } from '../editor/LegacyVariableQueryEditor';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
|
||||
|
||||
const setupTestContext = (options: Partial<Props>) => {
|
||||
const defaults: Props = {
|
||||
@ -34,10 +35,20 @@ const setupTestContext = (options: Partial<Props>) => {
|
||||
return { rerender, props };
|
||||
};
|
||||
|
||||
setDataSourceSrv({
|
||||
getInstanceSettings: () => null,
|
||||
getList: () => [],
|
||||
} as any);
|
||||
const mockDS = mockDataSource({
|
||||
name: 'CloudManager',
|
||||
type: DataSourceType.Alertmanager,
|
||||
});
|
||||
|
||||
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
|
||||
return {
|
||||
getDataSourceSrv: () => ({
|
||||
get: () => Promise.resolve(mockDS),
|
||||
getList: () => [mockDS],
|
||||
getInstanceSettings: () => mockDS,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('QueryVariableEditor', () => {
|
||||
describe('when the component is mounted', () => {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { DataSourceRef } from '@grafana/data';
|
||||
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types';
|
||||
import { VariableBuilder } from './variableBuilder';
|
||||
|
||||
export class AdHocVariableBuilder extends VariableBuilder<AdHocVariableModel> {
|
||||
withDatasource(datasource: string) {
|
||||
withDatasource(datasource: DataSourceRef) {
|
||||
this.variable.datasource = datasource;
|
||||
return this;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { ComponentType } from 'react';
|
||||
import {
|
||||
DataQuery,
|
||||
DataSourceJsonData,
|
||||
DataSourceRef,
|
||||
LoadingState,
|
||||
QueryEditorProps,
|
||||
VariableModel as BaseVariableModel,
|
||||
@ -48,7 +49,7 @@ export interface AdHocVariableFilter {
|
||||
}
|
||||
|
||||
export interface AdHocVariableModel extends VariableModel {
|
||||
datasource: string | null;
|
||||
datasource: DataSourceRef | null;
|
||||
filters: AdHocVariableFilter[];
|
||||
}
|
||||
|
||||
|
@ -6,17 +6,18 @@ import {
|
||||
DataQuery,
|
||||
DataQueryRequest,
|
||||
DataSourceApi,
|
||||
DataSourceRef,
|
||||
getDefaultTimeRange,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
} from '@grafana/data';
|
||||
|
||||
export function isSharedDashboardQuery(datasource: string | DataSourceApi | null) {
|
||||
export function isSharedDashboardQuery(datasource: string | DataSourceRef | DataSourceApi | null) {
|
||||
if (!datasource) {
|
||||
// default datasource
|
||||
return false;
|
||||
}
|
||||
if (datasource === SHARED_DASHBODARD_QUERY) {
|
||||
if (datasource === SHARED_DASHBODARD_QUERY || (datasource as any)?.uid === SHARED_DASHBODARD_QUERY) {
|
||||
return true;
|
||||
}
|
||||
const ds = datasource as DataSourceApi;
|
||||
|
@ -385,7 +385,7 @@ export class ElasticDatasource
|
||||
const expandedQueries = queries.map(
|
||||
(query): ElasticsearchQuery => ({
|
||||
...query,
|
||||
datasource: this.name,
|
||||
datasource: this.getRef(),
|
||||
query: this.interpolateLuceneQuery(query.query || '', scopedVars),
|
||||
bucketAggs: query.bucketAggs?.map(interpolateBucketAgg),
|
||||
})
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataSourceInstanceSettings,
|
||||
DatasourceRef,
|
||||
DataSourceRef,
|
||||
isValidLiveChannelAddress,
|
||||
parseLiveChannelAddress,
|
||||
StreamingFrameOptions,
|
||||
@ -18,6 +18,7 @@ import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery, GrafanaQue
|
||||
import AnnotationQueryEditor from './components/AnnotationQueryEditor';
|
||||
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
|
||||
import { isString } from 'lodash';
|
||||
import { migrateDatasourceNameToRef } from 'app/features/dashboard/state/DashboardMigrator';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
let counter = 100;
|
||||
@ -39,10 +40,16 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
|
||||
return json;
|
||||
},
|
||||
prepareQuery(anno: AnnotationQuery<GrafanaAnnotationQuery>): GrafanaQuery {
|
||||
let datasource: DatasourceRef | undefined | null = undefined;
|
||||
let datasource: DataSourceRef | undefined | null = undefined;
|
||||
if (isString(anno.datasource)) {
|
||||
datasource = anno.datasource as DatasourceRef;
|
||||
const ref = migrateDatasourceNameToRef(anno.datasource);
|
||||
if (ref) {
|
||||
datasource = ref;
|
||||
}
|
||||
} else {
|
||||
datasource = anno.datasource as DataSourceRef;
|
||||
}
|
||||
|
||||
return { ...anno, refId: anno.name, queryType: GrafanaQueryType.Annotations, datasource };
|
||||
},
|
||||
};
|
||||
|
@ -231,7 +231,7 @@ export class GraphiteDatasource extends DataSourceApi<
|
||||
expandedQueries = queries.map((query) => {
|
||||
const expandedQuery = {
|
||||
...query,
|
||||
datasource: this.name,
|
||||
datasource: this.getRef(),
|
||||
target: this.templateSrv.replace(query.target ?? '', scopedVars),
|
||||
};
|
||||
return expandedQuery;
|
||||
|
@ -342,7 +342,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
expandedQueries = queries.map((query) => {
|
||||
const expandedQuery = {
|
||||
...query,
|
||||
datasource: this.name,
|
||||
datasource: this.getRef(),
|
||||
measurement: this.templateSrv.replace(query.measurement ?? '', scopedVars, 'regex'),
|
||||
policy: this.templateSrv.replace(query.policy ?? '', scopedVars, 'regex'),
|
||||
};
|
||||
|
@ -311,7 +311,7 @@ export class LokiDatasource
|
||||
if (queries && queries.length) {
|
||||
expandedQueries = queries.map((query) => ({
|
||||
...query,
|
||||
datasource: this.name,
|
||||
datasource: this.getRef(),
|
||||
expr: this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr),
|
||||
}));
|
||||
}
|
||||
|
@ -30,9 +30,9 @@ describe('MixedDatasource', () => {
|
||||
const ds = new MixedDatasource({} as any);
|
||||
const requestMixed = getQueryOptions({
|
||||
targets: [
|
||||
{ refId: 'QA', datasource: 'A' }, // 1
|
||||
{ refId: 'QB', datasource: 'B' }, // 2
|
||||
{ refId: 'QC', datasource: 'C' }, // 3
|
||||
{ refId: 'QA', datasource: { uid: 'A' } }, // 1
|
||||
{ refId: 'QB', datasource: { uid: 'B' } }, // 2
|
||||
{ refId: 'QC', datasource: { uid: 'C' } }, // 3
|
||||
],
|
||||
});
|
||||
|
||||
@ -52,11 +52,11 @@ describe('MixedDatasource', () => {
|
||||
const ds = new MixedDatasource({} as any);
|
||||
const requestMixed = getQueryOptions({
|
||||
targets: [
|
||||
{ refId: 'QA', datasource: 'A' }, // 1
|
||||
{ refId: 'QD', datasource: 'D' }, // 2
|
||||
{ refId: 'QB', datasource: 'B' }, // 3
|
||||
{ refId: 'QE', datasource: 'E' }, // 4
|
||||
{ refId: 'QC', datasource: 'C' }, // 5
|
||||
{ refId: 'QA', datasource: { uid: 'A' } }, // 1
|
||||
{ refId: 'QD', datasource: { uid: 'D' } }, // 2
|
||||
{ refId: 'QB', datasource: { uid: 'B' } }, // 3
|
||||
{ refId: 'QE', datasource: { uid: 'E' } }, // 4
|
||||
{ refId: 'QC', datasource: { uid: 'C' } }, // 5
|
||||
],
|
||||
});
|
||||
|
||||
@ -84,9 +84,9 @@ describe('MixedDatasource', () => {
|
||||
const ds = new MixedDatasource({} as any);
|
||||
const request: any = {
|
||||
targets: [
|
||||
{ refId: 'A', datasource: 'Loki' },
|
||||
{ refId: 'B', datasource: 'Loki' },
|
||||
{ refId: 'C', datasource: 'A' },
|
||||
{ refId: 'A', datasource: { uid: 'Loki' } },
|
||||
{ refId: 'B', datasource: { uid: 'Loki' } },
|
||||
{ refId: 'C', datasource: { uid: 'A' } },
|
||||
],
|
||||
};
|
||||
|
||||
@ -115,8 +115,8 @@ describe('MixedDatasource', () => {
|
||||
await expect(
|
||||
ds.query({
|
||||
targets: [
|
||||
{ refId: 'QA', datasource: 'A' },
|
||||
{ refId: 'QB', datasource: 'B' },
|
||||
{ refId: 'QA', datasource: { uid: 'A' } },
|
||||
{ refId: 'QB', datasource: { uid: 'B' } },
|
||||
],
|
||||
} as any)
|
||||
).toEmitValuesWith((results) => {
|
||||
|
@ -26,7 +26,7 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
|
||||
query(request: DataQueryRequest<DataQuery>): Observable<DataQueryResponse> {
|
||||
// Remove any invalid queries
|
||||
const queries = request.targets.filter((t) => {
|
||||
return t.datasource !== MIXED_DATASOURCE_NAME;
|
||||
return t.datasource?.type !== MIXED_DATASOURCE_NAME;
|
||||
});
|
||||
|
||||
if (!queries.length) {
|
||||
@ -34,19 +34,23 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
|
||||
}
|
||||
|
||||
// Build groups of queries to run in parallel
|
||||
const sets: { [key: string]: DataQuery[] } = groupBy(queries, 'datasource');
|
||||
const sets: { [key: string]: DataQuery[] } = groupBy(queries, 'datasource.uid');
|
||||
const mixed: BatchedQueries[] = [];
|
||||
|
||||
for (const key in sets) {
|
||||
const targets = sets[key];
|
||||
const dsName = targets[0].datasource;
|
||||
|
||||
mixed.push({
|
||||
datasource: getDataSourceSrv().get(dsName, request.scopedVars),
|
||||
datasource: getDataSourceSrv().get(targets[0].datasource, request.scopedVars),
|
||||
targets,
|
||||
});
|
||||
}
|
||||
|
||||
// Missing UIDs?
|
||||
if (!mixed.length) {
|
||||
return of({ data: [] } as DataQueryResponse); // nothing
|
||||
}
|
||||
|
||||
return this.batchQueries(mixed, request);
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ export class MssqlDatasource extends DataSourceWithBackend<MssqlQuery, MssqlOpti
|
||||
expandedQueries = queries.map((query) => {
|
||||
const expandedQuery = {
|
||||
...query,
|
||||
datasource: this.name,
|
||||
datasource: this.getRef(),
|
||||
rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable),
|
||||
rawQuery: true,
|
||||
};
|
||||
|
@ -62,7 +62,7 @@ export class MysqlDatasource extends DataSourceWithBackend<MySQLQuery, MySQLOpti
|
||||
expandedQueries = queries.map((query) => {
|
||||
const expandedQuery = {
|
||||
...query,
|
||||
datasource: this.name,
|
||||
datasource: this.getRef(),
|
||||
rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable),
|
||||
rawQuery: true,
|
||||
};
|
||||
|
@ -64,7 +64,7 @@ export class PostgresDatasource extends DataSourceWithBackend<PostgresQuery, Pos
|
||||
expandedQueries = queries.map((query) => {
|
||||
const expandedQuery = {
|
||||
...query,
|
||||
datasource: this.name,
|
||||
datasource: this.getRef(),
|
||||
rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable),
|
||||
rawQuery: true,
|
||||
};
|
||||
|
@ -780,7 +780,7 @@ export class PrometheusDatasource extends DataSourceWithBackend<PromQuery, PromO
|
||||
expandedQueries = queries.map((query) => {
|
||||
const expandedQuery = {
|
||||
...query,
|
||||
datasource: this.name,
|
||||
datasource: this.getRef(),
|
||||
expr: this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr),
|
||||
interval: this.templateSrv.replace(query.interval, scopedVars),
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataQuery } from '@grafana/data';
|
||||
import { DataQuery, DataSourceRef } from '@grafana/data';
|
||||
import { ExpressionQuery } from '../features/expressions/types';
|
||||
|
||||
export interface QueryGroupOptions {
|
||||
@ -14,8 +14,7 @@ export interface QueryGroupOptions {
|
||||
};
|
||||
}
|
||||
|
||||
export interface QueryGroupDataSource {
|
||||
export interface QueryGroupDataSource extends DataSourceRef {
|
||||
name?: string | null;
|
||||
uid?: string;
|
||||
default?: boolean;
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import {
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataSourcePluginMeta,
|
||||
DataSourceRef,
|
||||
getDataSourceUID,
|
||||
} from '@grafana/data';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@ -12,15 +14,16 @@ export class DatasourceSrvMock {
|
||||
//
|
||||
}
|
||||
|
||||
get(name?: string): Promise<DataSourceApi> {
|
||||
if (!name) {
|
||||
get(ref?: DataSourceRef | string): Promise<DataSourceApi> {
|
||||
if (!ref) {
|
||||
return Promise.resolve(this.defaultDS);
|
||||
}
|
||||
const ds = this.datasources[name];
|
||||
const uid = getDataSourceUID(ref) ?? '';
|
||||
const ds = this.datasources[uid];
|
||||
if (ds) {
|
||||
return Promise.resolve(ds);
|
||||
}
|
||||
return Promise.reject('Unknown Datasource: ' + name);
|
||||
return Promise.reject(`Unknown Datasource: ${JSON.stringify(ref)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user