mirror of
https://github.com/grafana/grafana.git
synced 2025-01-10 08:03:58 -06:00
Plugin: PanelRenderer and simplified QueryRunner to be used from plugins. (#31901)
This commit is contained in:
parent
30e5afa18c
commit
cb2a63b5c6
@ -20,3 +20,4 @@ export {
|
|||||||
} from './transformations/matchers/valueMatchers/types';
|
} from './transformations/matchers/valueMatchers/types';
|
||||||
export { PanelPlugin, SetFieldConfigOptionsArgs, StandardOptionConfig } from './panel/PanelPlugin';
|
export { PanelPlugin, SetFieldConfigOptionsArgs, StandardOptionConfig } from './panel/PanelPlugin';
|
||||||
export { createFieldConfigRegistry } from './panel/registryFactories';
|
export { createFieldConfigRegistry } from './panel/registryFactories';
|
||||||
|
export { QueryRunner, QueryRunnerOptions } from './types/queryRunner';
|
||||||
|
@ -6,6 +6,7 @@ import { PluginMeta, GrafanaPlugin, PluginIncludeType } from './plugin';
|
|||||||
export enum CoreApp {
|
export enum CoreApp {
|
||||||
Dashboard = 'dashboard',
|
Dashboard = 'dashboard',
|
||||||
Explore = 'explore',
|
Explore = 'explore',
|
||||||
|
Unknown = 'unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppRootProps<T = KeyValue> {
|
export interface AppRootProps<T = KeyValue> {
|
||||||
|
38
packages/grafana-data/src/types/queryRunner.ts
Normal file
38
packages/grafana-data/src/types/queryRunner.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { DataQuery, DataSourceApi } from './datasource';
|
||||||
|
import { PanelData } from './panel';
|
||||||
|
import { ScopedVars } from './ScopedVars';
|
||||||
|
import { TimeRange, TimeZone } from './time';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the options used when triggering a query via the {@link QueryRunner}.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface QueryRunnerOptions {
|
||||||
|
datasource: string | DataSourceApi | null;
|
||||||
|
queries: DataQuery[];
|
||||||
|
panelId?: number;
|
||||||
|
dashboardId?: number;
|
||||||
|
timezone: TimeZone;
|
||||||
|
timeRange: TimeRange;
|
||||||
|
timeInfo?: string; // String description of time range for display
|
||||||
|
maxDataPoints: number;
|
||||||
|
minInterval: string | undefined | null;
|
||||||
|
scopedVars?: ScopedVars;
|
||||||
|
cacheTimeout?: string;
|
||||||
|
app?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the QueryRunner that can used to exectue queries in e.g. app plugins.
|
||||||
|
* QueryRunner instances can be created via the {@link @grafana/runtime#createQueryRunner | createQueryRunner}.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface QueryRunner {
|
||||||
|
get(): Observable<PanelData>;
|
||||||
|
run(options: QueryRunnerOptions): void;
|
||||||
|
cancel(): void;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
53
packages/grafana-runtime/src/components/PanelRenderer.tsx
Normal file
53
packages/grafana-runtime/src/components/PanelRenderer.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { AbsoluteTimeRange, FieldConfigSource, PanelData } from '@grafana/data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the properties that can be passed to the PanelRenderer.
|
||||||
|
*
|
||||||
|
* @typeParam P - Panel options type for the panel being rendered.
|
||||||
|
* @typeParam F - Field options type for the panel being rendered.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface PanelRendererProps<P extends object = any, F extends object = any> {
|
||||||
|
data: PanelData;
|
||||||
|
pluginId: string;
|
||||||
|
title: string;
|
||||||
|
fieldConfig?: FieldConfigSource<F>;
|
||||||
|
options?: P;
|
||||||
|
onOptionsChange: (options: P) => void;
|
||||||
|
onChangeTimeRange?: (timeRange: AbsoluteTimeRange) => void;
|
||||||
|
timeZone?: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified type with defaults that describes the PanelRenderer.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type PanelRendererType<P extends object = any, F extends object = any> = React.ComponentType<
|
||||||
|
PanelRendererProps<P, F>
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PanelRenderer component that will be set via the {@link setPanelRenderer} function
|
||||||
|
* when Grafana starts. The implementation being used during runtime lives in Grafana
|
||||||
|
* core.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export let PanelRenderer: PanelRendererType = () => {
|
||||||
|
return <div>PanelRenderer can only be used after Grafana instance has been started.</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to bootstrap the PanelRenderer during application start so the PanelRenderer
|
||||||
|
* is exposed via runtime.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function setPanelRenderer(renderer: PanelRendererType) {
|
||||||
|
PanelRenderer = renderer;
|
||||||
|
}
|
@ -12,4 +12,6 @@ export { reportMetaAnalytics } from './utils/analytics';
|
|||||||
export { logInfo, logDebug, logWarning, logError } from './utils/logging';
|
export { logInfo, logDebug, logWarning, logError } from './utils/logging';
|
||||||
export { DataSourceWithBackend, HealthCheckResult, HealthStatus } from './utils/DataSourceWithBackend';
|
export { DataSourceWithBackend, HealthCheckResult, HealthStatus } from './utils/DataSourceWithBackend';
|
||||||
export { toDataQueryError, toDataQueryResponse, frameToMetricFindValue } from './utils/queryResponse';
|
export { toDataQueryError, toDataQueryResponse, frameToMetricFindValue } from './utils/queryResponse';
|
||||||
|
export { PanelRenderer, PanelRendererProps, PanelRendererType, setPanelRenderer } from './components/PanelRenderer';
|
||||||
|
export { setQueryRunnerFactory, createQueryRunner, QueryRunnerFactory } from './services/QueryRunner';
|
||||||
export { DataSourcePicker, DataSourcePickerProps, DataSourcePickerState } from './components/DataSourcePicker';
|
export { DataSourcePicker, DataSourcePickerProps, DataSourcePickerState } from './components/DataSourcePicker';
|
||||||
|
33
packages/grafana-runtime/src/services/QueryRunner.ts
Normal file
33
packages/grafana-runtime/src/services/QueryRunner.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { QueryRunner } from '@grafana/data';
|
||||||
|
|
||||||
|
let factory: QueryRunnerFactory | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type QueryRunnerFactory = () => QueryRunner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to bootstrap the {@link createQueryRunner} during application start.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const setQueryRunnerFactory = (instance: QueryRunnerFactory): void => {
|
||||||
|
if (factory) {
|
||||||
|
throw new Error('Runner should only be set when Grafana is starting.');
|
||||||
|
}
|
||||||
|
factory = instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to create QueryRunner instances from outside the core Grafana application.
|
||||||
|
* This is helpful to be able to create a QueryRunner to execute queries in e.g. an app plugin.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const createQueryRunner = (): QueryRunner => {
|
||||||
|
if (!factory) {
|
||||||
|
throw new Error('`createQueryRunner` can only be used after Grafana instance has started.');
|
||||||
|
}
|
||||||
|
return factory();
|
||||||
|
};
|
@ -23,7 +23,7 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { arrayMove } from 'app/core/utils/arrayMove';
|
import { arrayMove } from 'app/core/utils/arrayMove';
|
||||||
import { importPluginModule } from 'app/features/plugins/plugin_loader';
|
import { importPluginModule } from 'app/features/plugins/plugin_loader';
|
||||||
import { registerEchoBackend, setEchoSrv } from '@grafana/runtime';
|
import { registerEchoBackend, setEchoSrv, setPanelRenderer, setQueryRunnerFactory } from '@grafana/runtime';
|
||||||
import { Echo } from './core/services/echo/Echo';
|
import { Echo } from './core/services/echo/Echo';
|
||||||
import { reportPerformance } from './core/services/echo/EchoSrv';
|
import { reportPerformance } from './core/services/echo/EchoSrv';
|
||||||
import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend';
|
import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend';
|
||||||
@ -39,6 +39,8 @@ import { configureStore } from './store/configureStore';
|
|||||||
import { AppWrapper } from './AppWrapper';
|
import { AppWrapper } from './AppWrapper';
|
||||||
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
||||||
import { AngularApp } from './angular/AngularApp';
|
import { AngularApp } from './angular/AngularApp';
|
||||||
|
import { PanelRenderer } from './features/panel/PanelRenderer';
|
||||||
|
import { QueryRunner } from './features/query/state/QueryRunner';
|
||||||
|
|
||||||
// add move to lodash for backward compatabilty with plugins
|
// add move to lodash for backward compatabilty with plugins
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -65,6 +67,7 @@ export class GrafanaApp {
|
|||||||
initEchoSrv();
|
initEchoSrv();
|
||||||
addClassIfNoOverlayScrollbar();
|
addClassIfNoOverlayScrollbar();
|
||||||
setLocale(config.bootData.user.locale);
|
setLocale(config.bootData.user.locale);
|
||||||
|
setPanelRenderer(PanelRenderer);
|
||||||
setTimeZoneResolver(() => config.bootData.user.timezone);
|
setTimeZoneResolver(() => config.bootData.user.timezone);
|
||||||
// Important that extensions are initialized before store
|
// Important that extensions are initialized before store
|
||||||
initExtensions();
|
initExtensions();
|
||||||
@ -75,6 +78,7 @@ export class GrafanaApp {
|
|||||||
standardTransformersRegistry.setInit(getStandardTransformers);
|
standardTransformersRegistry.setInit(getStandardTransformers);
|
||||||
variableAdapters.setInit(getDefaultVariableAdapters);
|
variableAdapters.setInit(getDefaultVariableAdapters);
|
||||||
|
|
||||||
|
setQueryRunnerFactory(() => new QueryRunner());
|
||||||
setVariableQueryRunner(new VariableQueryRunner());
|
setVariableQueryRunner(new VariableQueryRunner());
|
||||||
|
|
||||||
// intercept anchor clicks and forward it to custom history instead of relying on browser's history
|
// intercept anchor clicks and forward it to custom history instead of relying on browser's history
|
||||||
|
115
public/app/features/panel/PanelRenderer.tsx
Normal file
115
public/app/features/panel/PanelRenderer.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { applyFieldOverrides, FieldConfigSource, getTimeZone, PanelData, PanelPlugin } from '@grafana/data';
|
||||||
|
import { PanelRendererProps } from '@grafana/runtime';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
|
import { appEvents } from 'app/core/core';
|
||||||
|
import { useAsync } from 'react-use';
|
||||||
|
import { getPanelOptionsWithDefaults, OptionDefaults } from '../dashboard/state/getPanelOptionsWithDefaults';
|
||||||
|
import { importPanelPlugin } from '../plugins/plugin_loader';
|
||||||
|
|
||||||
|
export function PanelRenderer<P extends object = any, F extends object = any>(props: PanelRendererProps<P, F>) {
|
||||||
|
const {
|
||||||
|
pluginId,
|
||||||
|
data,
|
||||||
|
timeZone = getTimeZone(),
|
||||||
|
options = {},
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
title,
|
||||||
|
onOptionsChange,
|
||||||
|
onChangeTimeRange = () => {},
|
||||||
|
fieldConfig: config = { defaults: {}, overrides: [] },
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [fieldConfig, setFieldConfig] = useState<FieldConfigSource>(config);
|
||||||
|
const { value: plugin, error, loading } = useAsync(() => importPanelPlugin(pluginId), [pluginId]);
|
||||||
|
const defaultOptions = useOptionDefaults(plugin, options, fieldConfig);
|
||||||
|
const dataWithOverrides = useFieldOverrides(plugin, defaultOptions, data, timeZone);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>Failed to load plugin: {error.message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div>Loading plugin panel...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plugin || !plugin.panel) {
|
||||||
|
return <div>Seems like the plugin you are trying to load does not have a panel component.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dataWithOverrides) {
|
||||||
|
return <div>No panel data</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PanelComponent = plugin.panel;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelComponent
|
||||||
|
id={1}
|
||||||
|
data={dataWithOverrides}
|
||||||
|
title={title}
|
||||||
|
timeRange={dataWithOverrides.timeRange}
|
||||||
|
timeZone={timeZone}
|
||||||
|
options={options}
|
||||||
|
fieldConfig={fieldConfig}
|
||||||
|
transparent={false}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
renderCounter={0}
|
||||||
|
replaceVariables={(str: string) => str}
|
||||||
|
onOptionsChange={onOptionsChange}
|
||||||
|
onFieldConfigChange={setFieldConfig}
|
||||||
|
onChangeTimeRange={onChangeTimeRange}
|
||||||
|
eventBus={appEvents}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useOptionDefaults = <P extends object = any, F extends object = any>(
|
||||||
|
plugin: PanelPlugin | undefined,
|
||||||
|
options: P,
|
||||||
|
fieldConfig: FieldConfigSource<F>
|
||||||
|
): OptionDefaults | undefined => {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!plugin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPanelOptionsWithDefaults({
|
||||||
|
plugin,
|
||||||
|
currentOptions: options,
|
||||||
|
currentFieldConfig: fieldConfig,
|
||||||
|
isAfterPluginChange: false,
|
||||||
|
});
|
||||||
|
}, [plugin, fieldConfig, options]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFieldOverrides = (
|
||||||
|
plugin: PanelPlugin | undefined,
|
||||||
|
defaultOptions: OptionDefaults | undefined,
|
||||||
|
data: PanelData | undefined,
|
||||||
|
timeZone: string
|
||||||
|
): PanelData | undefined => {
|
||||||
|
const fieldConfig = defaultOptions?.fieldConfig;
|
||||||
|
const series = data?.series;
|
||||||
|
const fieldConfigRegistry = plugin?.fieldConfigRegistry;
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!fieldConfigRegistry || !fieldConfig || !data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
series: applyFieldOverrides({
|
||||||
|
data: series,
|
||||||
|
fieldConfig,
|
||||||
|
fieldConfigRegistry,
|
||||||
|
replaceVariables: (str: string) => str,
|
||||||
|
theme: config.theme,
|
||||||
|
timeZone,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}, [fieldConfigRegistry, timeZone, fieldConfig, series]);
|
||||||
|
};
|
@ -43,12 +43,11 @@ export interface QueryRunnerOptions<
|
|||||||
minInterval: string | undefined | null;
|
minInterval: string | undefined | null;
|
||||||
scopedVars?: ScopedVars;
|
scopedVars?: ScopedVars;
|
||||||
cacheTimeout?: string;
|
cacheTimeout?: string;
|
||||||
delayStateNotification?: number; // default 100ms.
|
|
||||||
transformations?: DataTransformerConfig[];
|
transformations?: DataTransformerConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
let counter = 100;
|
let counter = 100;
|
||||||
function getNextRequestId() {
|
export function getNextRequestId() {
|
||||||
return 'Q' + counter++;
|
return 'Q' + counter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
148
public/app/features/query/state/QueryRunner.ts
Normal file
148
public/app/features/query/state/QueryRunner.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import {
|
||||||
|
CoreApp,
|
||||||
|
DataQueryRequest,
|
||||||
|
DataSourceApi,
|
||||||
|
PanelData,
|
||||||
|
rangeUtil,
|
||||||
|
ScopedVars,
|
||||||
|
QueryRunnerOptions,
|
||||||
|
QueryRunner as QueryRunnerSrv,
|
||||||
|
LoadingState,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { from, Observable, ReplaySubject, Unsubscribable } from 'rxjs';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { getNextRequestId } from './PanelQueryRunner';
|
||||||
|
import { preProcessPanelData, runRequest } from './runRequest';
|
||||||
|
|
||||||
|
export class QueryRunner implements QueryRunnerSrv {
|
||||||
|
private subject: ReplaySubject<PanelData>;
|
||||||
|
private subscription?: Unsubscribable;
|
||||||
|
private lastResult?: PanelData;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.subject = new ReplaySubject(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): Observable<PanelData> {
|
||||||
|
return this.subject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
run(options: QueryRunnerOptions): void {
|
||||||
|
const {
|
||||||
|
queries,
|
||||||
|
timezone,
|
||||||
|
datasource,
|
||||||
|
panelId,
|
||||||
|
app,
|
||||||
|
dashboardId,
|
||||||
|
timeRange,
|
||||||
|
timeInfo,
|
||||||
|
cacheTimeout,
|
||||||
|
maxDataPoints,
|
||||||
|
scopedVars,
|
||||||
|
minInterval,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
if (this.subscription) {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
const request: DataQueryRequest = {
|
||||||
|
app: app ?? CoreApp.Unknown,
|
||||||
|
requestId: getNextRequestId(),
|
||||||
|
timezone,
|
||||||
|
panelId,
|
||||||
|
dashboardId,
|
||||||
|
range: timeRange,
|
||||||
|
timeInfo,
|
||||||
|
interval: '',
|
||||||
|
intervalMs: 0,
|
||||||
|
targets: cloneDeep(queries),
|
||||||
|
maxDataPoints: maxDataPoints,
|
||||||
|
scopedVars: scopedVars || {},
|
||||||
|
cacheTimeout,
|
||||||
|
startTime: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add deprecated property
|
||||||
|
(request as any).rangeRaw = timeRange.raw;
|
||||||
|
|
||||||
|
from(getDataSource(datasource, request.scopedVars))
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe({
|
||||||
|
next: (ds) => {
|
||||||
|
// Attach the datasource name to each query
|
||||||
|
request.targets = request.targets.map((query) => {
|
||||||
|
if (!query.datasource) {
|
||||||
|
query.datasource = ds.name;
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
});
|
||||||
|
|
||||||
|
const lowerIntervalLimit = minInterval
|
||||||
|
? getTemplateSrv().replace(minInterval, request.scopedVars)
|
||||||
|
: ds.interval;
|
||||||
|
const norm = rangeUtil.calculateInterval(timeRange, maxDataPoints, lowerIntervalLimit);
|
||||||
|
|
||||||
|
// make shallow copy of scoped vars,
|
||||||
|
// and add built in variables interval and interval_ms
|
||||||
|
request.scopedVars = Object.assign({}, request.scopedVars, {
|
||||||
|
__interval: { text: norm.interval, value: norm.interval },
|
||||||
|
__interval_ms: { text: norm.intervalMs.toString(), value: norm.intervalMs },
|
||||||
|
});
|
||||||
|
|
||||||
|
request.interval = norm.interval;
|
||||||
|
request.intervalMs = norm.intervalMs;
|
||||||
|
|
||||||
|
this.subscription = runRequest(ds, request).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.lastResult = preProcessPanelData(data, this.lastResult);
|
||||||
|
// Store preprocessed query results for applying overrides later on in the pipeline
|
||||||
|
this.subject.next(this.lastResult);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (error) => console.error('PanelQueryRunner Error', error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
if (!this.subscription) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
|
||||||
|
// If we have an old result with loading state, send it with done state
|
||||||
|
if (this.lastResult && this.lastResult.state === LoadingState.Loading) {
|
||||||
|
this.subject.next({
|
||||||
|
...this.lastResult,
|
||||||
|
state: LoadingState.Done,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
// Tell anyone listening that we are done
|
||||||
|
if (this.subject) {
|
||||||
|
this.subject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.subscription) {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDataSource(
|
||||||
|
datasource: string | DataSourceApi | null,
|
||||||
|
scopedVars: ScopedVars
|
||||||
|
): Promise<DataSourceApi> {
|
||||||
|
if (datasource && (datasource as any).query) {
|
||||||
|
return datasource as DataSourceApi;
|
||||||
|
}
|
||||||
|
return await getDatasourceSrv().get(datasource as string, scopedVars);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user