mirror of
https://github.com/grafana/grafana.git
synced 2026-08-17 00:24:57 -05:00
QueryProcessing: Observable query interface and RxJS for query & stream processing (#18899)
* I needed to learn some rxjs and understand this more, so just playing around * Updated * Removed all the complete calls * Refactoring * StreamHandler -> observable start * progress * simple singal works * Handle update time range * added error handling * wrap old function * minor changes * handle data format in the subscribe function * Use replay subject to return last value to subscribers * Set loading state after no response in 50ms * added missing file * updated comment * Added cancelation of network requests * runRequest: Added unit test scenario framework * Progress on tests * minor refactor of unit tests * updated test * removed some old code * Shared queries work again, and also became so much simplier * unified query and observe methods * implict any fix * Fixed closed subject issue * removed comment * Use last returned data for loading state * WIP: Explore to runRequest makover step1 * Minor progress * Minor progress on explore and runRequest * minor progress * Things are starting to work in explore * Updated prometheus to use new observable query response, greatly simplified code * Revert refId change * Found better solution for key/refId/requestId problem * use observable with loki * tests compile * fix loki query prep * Explore: correct first response handling * Refactorings * Refactoring * Explore: Fixes LoadingState and GraphResults between runs (#18986) * Refactor: Adds state to DataQueryResponse * Fix: Fixes so we do not empty results before new data arrives Fixes: #17409 * Transformations work * observable test data * remove single() from loki promise * Fixed comment * Explore: Fixes failing Loki and Prometheus unit tests (#18995) * Tests: Makes datasource tests work again * Fix: Fixes loki datasource so highligthing works * Chore: Runs Prettier * Fixed query runner tests * Delay loading state indication to 200ms * Fixed test * fixed unit tests * Clear cached calcs * Fixed bug getProcesedDataFrames * Fix the correct test is a better idea * Fix: Fixes so queries in Explore are only run if Graph/Table is shown (#19000) * Fix: Fixes so queries in Explore are only run if Graph/Table is shown Fixes: #18618 * Refactor: Removes unnecessary condition * PanelData: provide legacy data only when needed (#19018) * no legacy * invert logic... now compiles * merge getQueryResponseData and getDataRaw * update comment about query editor * use single getData() function * only send legacy when it is used in explore * pre process rather than post process * pre process rather than post process * Minor refactoring * Add missing tags to test datasource response * MixedDatasource: Adds query observable pattern to MixedDatasource (#19037) * start mixed datasource * Refactor: Refactors into observable parttern * Tests: Fixes tests * Tests: Removes console.log * Refactor: Adds unique requestId
This commit is contained in:
@@ -393,8 +393,8 @@ export class CircularDataFrame<T = any> extends MutableDataFrame<T> {
|
||||
constructor(options: CircularOptions) {
|
||||
super(undefined, (buffer?: any[]) => {
|
||||
return new CircularVector({
|
||||
buffer,
|
||||
...options,
|
||||
buffer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
|
||||
|
||||
// For now everything can use the standard stats
|
||||
let values = doStandardCalcs(field, ignoreNulls, nullAsZero);
|
||||
|
||||
for (const reducer of queue) {
|
||||
if (!values.hasOwnProperty(reducer.id) && reducer.reduce) {
|
||||
values = {
|
||||
@@ -109,10 +110,12 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
field.calcs = {
|
||||
...field.calcs,
|
||||
...values,
|
||||
};
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
|
||||
@@ -238,16 +238,19 @@ export const toDataFrame = (data: any): DataFrame => {
|
||||
// This will convert the array values into Vectors
|
||||
return new MutableDataFrame(data as DataFrameDTO);
|
||||
}
|
||||
|
||||
if (data.hasOwnProperty('datapoints')) {
|
||||
return convertTimeSeriesToDataFrame(data);
|
||||
}
|
||||
|
||||
if (data.hasOwnProperty('data')) {
|
||||
return convertGraphSeriesToDataFrame(data);
|
||||
}
|
||||
|
||||
if (data.hasOwnProperty('columns')) {
|
||||
return convertTableToDataFrame(data);
|
||||
}
|
||||
// TODO, try to convert JSON/Array to table?
|
||||
|
||||
console.warn('Can not convert', data);
|
||||
throw new Error('Unsupported data format');
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ interface TransformationsEditorState {
|
||||
interface TransformationsEditorProps {
|
||||
onChange: (transformations: DataTransformerConfig[]) => void;
|
||||
transformations: DataTransformerConfig[];
|
||||
getCurrentData: (applyTransformations?: boolean) => DataFrame[];
|
||||
dataFrames: DataFrame[];
|
||||
}
|
||||
|
||||
export class TransformationsEditor extends React.PureComponent<TransformationsEditorProps, TransformationsEditorState> {
|
||||
@@ -46,9 +46,9 @@ export class TransformationsEditor extends React.PureComponent<TransformationsEd
|
||||
};
|
||||
|
||||
renderTransformationEditors = () => {
|
||||
const { transformations, getCurrentData } = this.props;
|
||||
const { transformations, dataFrames } = this.props;
|
||||
const hasTransformations = transformations.length > 0;
|
||||
const preTransformData = getCurrentData(false);
|
||||
const preTransformData = dataFrames;
|
||||
|
||||
if (!hasTransformations) {
|
||||
return undefined;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { PluginMeta, GrafanaPlugin } from './plugin';
|
||||
import { PanelData } from './panel';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
// NOTE: this seems more general than just DataSource
|
||||
export interface DataSourcePluginOptionsEditorProps<TOptions> {
|
||||
@@ -189,26 +190,9 @@ export abstract class DataSourceApi<
|
||||
init?: () => void;
|
||||
|
||||
/**
|
||||
* Query for data, and optionally stream results to an observer.
|
||||
*
|
||||
* Are you reading these docs aiming to execute a query?
|
||||
* +-> If Yes, then consider using panelQueryRunner/State instead. see:
|
||||
* * {@link https://github.com/grafana/grafana/blob/master/public/app/features/dashboard/state/PanelQueryRunner.ts PanelQueryRunner.ts}
|
||||
* * {@link https://github.com/grafana/grafana/blob/master/public/app/features/dashboard/state/PanelQueryState.ts PanelQueryState.ts}
|
||||
*
|
||||
* If you are implementing a simple request-response query,
|
||||
* then you can ignore the `observer` entirely.
|
||||
*
|
||||
* When streaming behavior is required, the Promise can return at any time
|
||||
* with empty or partial data in the response and optionally a state.
|
||||
* NOTE: The data in this initial response will not be replaced with any
|
||||
* data from subsequent events. {@see DataStreamState}
|
||||
*
|
||||
* The request object will be passed in each observer callback
|
||||
* so the callback could assert that the correct events are streaming and
|
||||
* unsubscribe if unexpected results are returned.
|
||||
* Query for data, and optionally stream results
|
||||
*/
|
||||
abstract query(request: DataQueryRequest<TQuery>, observer?: DataStreamObserver): Promise<DataQueryResponse>;
|
||||
abstract query(request: DataQueryRequest<TQuery>): Promise<DataQueryResponse> | Observable<DataQueryResponse>;
|
||||
|
||||
/**
|
||||
* Test & verify datasource settings & connection details
|
||||
@@ -397,6 +381,19 @@ export interface DataQueryResponse {
|
||||
* or a partial result set
|
||||
*/
|
||||
data: DataQueryResponseData[];
|
||||
|
||||
/**
|
||||
* When returning multiple partial responses or streams
|
||||
* Use this key to inform Grafana how to combine the partial responses
|
||||
* Multiple responses with same key are replaced (latest used)
|
||||
*/
|
||||
key?: string;
|
||||
|
||||
/**
|
||||
* Use this to control which state the response should have
|
||||
* Defaults to LoadingState.Done if state is not defined
|
||||
*/
|
||||
state?: LoadingState;
|
||||
}
|
||||
|
||||
export interface DataQuery {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentClass, ComponentType } from 'react';
|
||||
import { LoadingState, DataFrame, TimeRange, TimeZone, ScopedVars } from '@grafana/data';
|
||||
import { DataQueryRequest, DataQueryError, LegacyResponseData } from './datasource';
|
||||
import { DataQueryRequest, DataQueryError } from './datasource';
|
||||
import { PluginMeta, GrafanaPlugin } from './plugin';
|
||||
|
||||
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
|
||||
@@ -16,9 +16,6 @@ export interface PanelData {
|
||||
series: DataFrame[];
|
||||
request?: DataQueryRequest;
|
||||
error?: DataQueryError;
|
||||
|
||||
// Data format expected by Angular panels
|
||||
legacy?: LegacyResponseData[];
|
||||
}
|
||||
|
||||
export interface PanelProps<T = any> {
|
||||
|
||||
@@ -157,6 +157,7 @@ export class BackendSrv implements BackendService {
|
||||
// is canceled, canceling the previous datasource request if it is still
|
||||
// in-flight.
|
||||
const requestId = options.requestId;
|
||||
|
||||
if (requestId) {
|
||||
this.resolveCancelerIfExists(requestId);
|
||||
// create new canceler
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
updateHistory,
|
||||
clearHistory,
|
||||
hasNonEmptyQuery,
|
||||
instanceOfDataQueryError,
|
||||
getValueWithRefId,
|
||||
getFirstQueryErrorWithoutRefId,
|
||||
getRefIds,
|
||||
@@ -201,30 +200,6 @@ describe('hasNonEmptyQuery', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('instanceOfDataQueryError', () => {
|
||||
describe('when called with a DataQueryError', () => {
|
||||
it('then it should return true', () => {
|
||||
const error: DataQueryError = {
|
||||
message: 'A message',
|
||||
status: '200',
|
||||
statusText: 'Ok',
|
||||
};
|
||||
const result = instanceOfDataQueryError(error);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a non DataQueryError', () => {
|
||||
it('then it should return false', () => {
|
||||
const error = {};
|
||||
const result = instanceOfDataQueryError(error);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasRefId', () => {
|
||||
describe('when called with a null value', () => {
|
||||
it('then it should return null', () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
import { isLive } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
|
||||
// Services & Utils
|
||||
import {
|
||||
@@ -28,7 +29,6 @@ import {
|
||||
ExploreMode,
|
||||
} from 'app/types/explore';
|
||||
import { config } from '../config';
|
||||
import { PanelQueryState } from '../../features/dashboard/state/PanelQueryState';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
|
||||
export const DEFAULT_RANGE = {
|
||||
@@ -124,8 +124,7 @@ export function buildQueryTransaction(
|
||||
dashboardId: 0,
|
||||
// TODO probably should be taken from preferences but does not seem to be used anyway.
|
||||
timezone: DefaultTimeZone,
|
||||
// This is set to correct time later on before the query is actually run.
|
||||
startTime: 0,
|
||||
startTime: Date.now(),
|
||||
interval,
|
||||
intervalMs,
|
||||
// TODO: the query request expects number and we are using string here. Seems like it works so far but can create
|
||||
@@ -409,10 +408,6 @@ export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): Ti
|
||||
};
|
||||
};
|
||||
|
||||
export const instanceOfDataQueryError = (value: any): value is DataQueryError => {
|
||||
return value.message !== undefined && value.status !== undefined && value.statusText !== undefined;
|
||||
};
|
||||
|
||||
export const getValueWithRefId = (value: any): any | null => {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -518,9 +513,8 @@ export const convertToWebSocketUrl = (url: string) => {
|
||||
return `${backend}${url}`;
|
||||
};
|
||||
|
||||
export const stopQueryState = (queryState: PanelQueryState, reason: string) => {
|
||||
if (queryState && queryState.isStarted()) {
|
||||
queryState.cancel(reason);
|
||||
queryState.closeStreams(false);
|
||||
export const stopQueryState = (querySubscription: Unsubscribable) => {
|
||||
if (querySubscription) {
|
||||
querySubscription.unsubscribe();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ErrorBoundary } from '@grafana/ui';
|
||||
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
||||
import { applyPanelTimeOverrides, calculateInnerPanelHeight } from 'app/features/dashboard/utils/panel';
|
||||
import { profiler } from 'app/core/profiler';
|
||||
import { getProcessedDataFrames } from '../state/PanelQueryState';
|
||||
import { getProcessedDataFrames } from '../state/runRequest';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
import config from 'app/core/config';
|
||||
|
||||
@@ -82,6 +82,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.panel.events.off('refresh', this.onRefresh);
|
||||
|
||||
if (this.querySubscription) {
|
||||
this.querySubscription.unsubscribe();
|
||||
this.querySubscription = null;
|
||||
@@ -94,12 +95,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
// View state has changed
|
||||
if (isInView !== prevProps.isInView) {
|
||||
if (isInView) {
|
||||
// Subscribe will kick of a notice of the last known state
|
||||
if (!this.querySubscription && this.wantsQueryExecution) {
|
||||
const runner = this.props.panel.getQueryRunner();
|
||||
this.querySubscription = runner.subscribe(this.panelDataObserver);
|
||||
}
|
||||
|
||||
// Check if we need a delayed refresh
|
||||
if (this.state.refreshWhenInView) {
|
||||
this.onRefresh();
|
||||
@@ -170,8 +165,9 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
const queryRunner = panel.getQueryRunner();
|
||||
|
||||
if (!this.querySubscription) {
|
||||
this.querySubscription = queryRunner.subscribe(this.panelDataObserver);
|
||||
this.querySubscription = queryRunner.getData().subscribe(this.panelDataObserver);
|
||||
}
|
||||
|
||||
queryRunner.run({
|
||||
datasource: panel.datasource,
|
||||
queries: panel.targets,
|
||||
|
||||
@@ -22,10 +22,8 @@ import { DashboardModel } from '../state/DashboardModel';
|
||||
import { DataQuery, DataSourceSelectItem, PanelData, AlphaNotice, PluginState } from '@grafana/ui';
|
||||
import { LoadingState, DataTransformerConfig } from '@grafana/data';
|
||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||
import { PanelQueryRunnerFormat } from '../state/PanelQueryRunner';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard/SharedQueryRunner';
|
||||
import { DashboardQueryEditor } from 'app/plugins/datasource/dashboard/DashboardQueryEditor';
|
||||
import { isSharedDashboardQuery, DashboardQueryEditor } from 'app/plugins/datasource/dashboard';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -64,7 +62,9 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
const { panel } = this.props;
|
||||
const queryRunner = panel.getQueryRunner();
|
||||
|
||||
this.querySubscription = queryRunner.subscribe(this.panelDataObserver, PanelQueryRunnerFormat.both);
|
||||
this.querySubscription = queryRunner.getData(false).subscribe({
|
||||
next: (data: PanelData) => this.onPanelDataUpdate(data),
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -74,22 +74,9 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the response with information from the stream
|
||||
panelDataObserver = {
|
||||
next: (data: PanelData) => {
|
||||
try {
|
||||
const { panel } = this.props;
|
||||
if (data.state === LoadingState.Error) {
|
||||
panel.events.emit('data-error', data.error);
|
||||
} else if (data.state === LoadingState.Done) {
|
||||
panel.events.emit('data-received', data.legacy);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Panel.events handler error', err);
|
||||
}
|
||||
onPanelDataUpdate(data: PanelData) {
|
||||
this.setState({ data });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
findCurrentDataSource(): DataSourceSelectItem {
|
||||
const { panel } = this.props;
|
||||
@@ -226,11 +213,6 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
this.setState({ scrollTop: target.scrollTop });
|
||||
};
|
||||
|
||||
getCurrentData = (applyTransformations = true) => {
|
||||
const queryRunner = this.props.panel.getQueryRunner();
|
||||
return queryRunner.getCurrentData(applyTransformations).series;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { panel, dashboard } = this.props;
|
||||
const { currentDS, scrollTop, data } = this.state;
|
||||
@@ -301,7 +283,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
<TransformationsEditor
|
||||
transformations={this.props.panel.transformations || []}
|
||||
onChange={this.onTransformersChange}
|
||||
getCurrentData={this.getCurrentData}
|
||||
dataFrames={data.series}
|
||||
/>
|
||||
)}
|
||||
</PanelOptionsGroup>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
// Types
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
import { DataQuery, DataSourceApi, PanelData, DataQueryRequest, ErrorBoundaryAlert } from '@grafana/ui';
|
||||
import { TimeRange, LoadingState } from '@grafana/data';
|
||||
import { TimeRange, LoadingState, toLegacyResponseData } from '@grafana/data';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
|
||||
interface Props {
|
||||
@@ -89,7 +89,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { loadedDataSourceValue } = this.state;
|
||||
const { data, query } = this.props;
|
||||
const { data, query, panel } = this.props;
|
||||
|
||||
if (data !== prevProps.data) {
|
||||
this.setState({ queryResponse: filterPanelDataToQuery(data, query.refId) });
|
||||
@@ -99,9 +99,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
if (this.angularQueryEditor) {
|
||||
// Some query controllers listen to data error events and need a digest
|
||||
// for some reason this needs to be done in next tick
|
||||
setTimeout(this.angularQueryEditor.digest);
|
||||
notifyAngularQueryEditorsOfData(panel, data, this.angularQueryEditor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +263,25 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
// To avoid sending duplicate events for each row we have this global cached object here
|
||||
// So we can check if we already emitted this legacy data event
|
||||
let globalLastPanelDataCache: PanelData = null;
|
||||
|
||||
function notifyAngularQueryEditorsOfData(panel: PanelModel, data: PanelData, editor: AngularComponent) {
|
||||
if (data === globalLastPanelDataCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
globalLastPanelDataCache = data;
|
||||
|
||||
const legacy = data.series.map(v => toLegacyResponseData(v));
|
||||
panel.events.emit('data-received', legacy);
|
||||
|
||||
// Some query controllers listen to data error events and need a digest
|
||||
// for some reason this needs to be done in next tick
|
||||
setTimeout(editor.digest);
|
||||
}
|
||||
|
||||
export interface AngularQueryComponentScope {
|
||||
target: DataQuery;
|
||||
panel: PanelModel;
|
||||
|
||||
@@ -327,7 +327,8 @@ export class PanelModel {
|
||||
|
||||
getQueryRunner(): PanelQueryRunner {
|
||||
if (!this.queryRunner) {
|
||||
this.queryRunner = new PanelQueryRunner(this.id);
|
||||
this.queryRunner = new PanelQueryRunner();
|
||||
this.setTransformations(this.transformations);
|
||||
}
|
||||
return this.queryRunner;
|
||||
}
|
||||
@@ -336,6 +337,10 @@ export class PanelModel {
|
||||
return this.title && this.title.length > 0;
|
||||
}
|
||||
|
||||
isAngularPlugin(): boolean {
|
||||
return this.plugin && !!this.plugin.angularPanelCtrl;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.events.emit('panel-teardown');
|
||||
this.events.removeAllListeners();
|
||||
@@ -347,11 +352,8 @@ export class PanelModel {
|
||||
}
|
||||
|
||||
setTransformations(transformations: DataTransformerConfig[]) {
|
||||
// save for persistence
|
||||
this.transformations = transformations;
|
||||
|
||||
// update query runner transformers
|
||||
this.getQueryRunner().setTransform(transformations);
|
||||
this.getQueryRunner().setTransformations(transformations);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { PanelQueryRunner, QueryRunnerOptions } from './PanelQueryRunner';
|
||||
import { PanelData, DataQueryRequest, DataStreamObserver, DataStreamState } from '@grafana/ui';
|
||||
|
||||
import { LoadingState, MutableDataFrame, ScopedVars } from '@grafana/data';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { SHARED_DASHBODARD_QUERY } from 'app/plugins/datasource/dashboard/SharedQueryRunner';
|
||||
import { DashboardQuery } from 'app/plugins/datasource/dashboard/types';
|
||||
import { PanelQueryRunner } from './PanelQueryRunner';
|
||||
import { PanelData, DataQueryRequest } from '@grafana/ui';
|
||||
import { dateTime, ScopedVars } from '@grafana/data';
|
||||
import { PanelModel } from './PanelModel';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
jest.mock('app/core/services/backend_srv');
|
||||
|
||||
// Defined within setup functions
|
||||
const panelsForCurrentDashboardMock: { [key: number]: PanelModel } = {};
|
||||
|
||||
jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
|
||||
getDashboardSrv: () => {
|
||||
return {
|
||||
@@ -40,7 +36,6 @@ interface ScenarioContext {
|
||||
events?: PanelData[];
|
||||
res?: PanelData;
|
||||
queryCalledWith?: DataQueryRequest;
|
||||
observer: DataStreamObserver;
|
||||
runner: PanelQueryRunner;
|
||||
}
|
||||
|
||||
@@ -55,8 +50,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
|
||||
scopedVars: {
|
||||
server: { text: 'Server1', value: 'server-1' },
|
||||
},
|
||||
runner: new PanelQueryRunner(1),
|
||||
observer: (args: any) => {},
|
||||
runner: new PanelQueryRunner(),
|
||||
setup: (fn: () => void) => {
|
||||
setupFn = fn;
|
||||
},
|
||||
@@ -72,9 +66,8 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
|
||||
const datasource: any = {
|
||||
name: 'TestDB',
|
||||
interval: ctx.dsInterval,
|
||||
query: (options: DataQueryRequest, observer: DataStreamObserver) => {
|
||||
query: (options: DataQueryRequest) => {
|
||||
ctx.queryCalledWith = options;
|
||||
ctx.observer = observer;
|
||||
return Promise.resolve(response);
|
||||
},
|
||||
testDatasource: jest.fn(),
|
||||
@@ -95,9 +88,10 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
|
||||
queries: [{ refId: 'A', test: 1 }],
|
||||
};
|
||||
|
||||
ctx.runner = new PanelQueryRunner(1);
|
||||
ctx.runner.subscribe({
|
||||
ctx.runner = new PanelQueryRunner();
|
||||
ctx.runner.getData().subscribe({
|
||||
next: (data: PanelData) => {
|
||||
ctx.res = data;
|
||||
ctx.events.push(data);
|
||||
},
|
||||
});
|
||||
@@ -110,7 +104,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
|
||||
} as PanelModel;
|
||||
|
||||
ctx.events = [];
|
||||
ctx.res = await ctx.runner.run(args);
|
||||
ctx.runner.run(args);
|
||||
});
|
||||
|
||||
scenarioFn(ctx);
|
||||
@@ -190,102 +184,4 @@ describe('PanelQueryRunner', () => {
|
||||
expect(ctx.queryCalledWith.maxDataPoints).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describeQueryRunnerScenario('when datasource is streaming data', ctx => {
|
||||
let streamState: DataStreamState;
|
||||
let isUnsubbed = false;
|
||||
|
||||
beforeEach(() => {
|
||||
streamState = {
|
||||
state: LoadingState.Streaming,
|
||||
key: 'test-stream-1',
|
||||
data: [
|
||||
new MutableDataFrame({
|
||||
fields: [],
|
||||
name: 'I am a magic stream',
|
||||
}),
|
||||
],
|
||||
request: {
|
||||
requestId: ctx.queryCalledWith.requestId,
|
||||
} as any,
|
||||
unsubscribe: () => {
|
||||
isUnsubbed = true;
|
||||
},
|
||||
};
|
||||
ctx.observer(streamState);
|
||||
});
|
||||
|
||||
it('should push another update to subscriber', async () => {
|
||||
expect(ctx.events.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should set state to streaming', async () => {
|
||||
expect(ctx.events[1].state).toBe(LoadingState.Streaming);
|
||||
});
|
||||
|
||||
it('should not unsubscribe', async () => {
|
||||
expect(isUnsubbed).toBe(false);
|
||||
});
|
||||
|
||||
it('destroy should unsubscribe streams', async () => {
|
||||
ctx.runner.destroy();
|
||||
expect(isUnsubbed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describeQueryRunnerScenario('Shared query request', ctx => {
|
||||
ctx.setup(() => {});
|
||||
|
||||
it('should get the same results as the original', async () => {
|
||||
// Get the results from
|
||||
const q: DashboardQuery = { refId: 'Z', panelId: 1 };
|
||||
const myPanelId = 7;
|
||||
|
||||
const runnerWantingSharedResults = new PanelQueryRunner(myPanelId);
|
||||
panelsForCurrentDashboardMock[myPanelId] = {
|
||||
id: myPanelId,
|
||||
getQueryRunner: () => {
|
||||
return runnerWantingSharedResults;
|
||||
},
|
||||
} as PanelModel;
|
||||
|
||||
const res = await runnerWantingSharedResults.run({
|
||||
datasource: SHARED_DASHBODARD_QUERY,
|
||||
queries: [q],
|
||||
|
||||
// Same query setup
|
||||
scopedVars: ctx.scopedVars,
|
||||
minInterval: ctx.minInterval,
|
||||
widthPixels: ctx.widthPixels,
|
||||
maxDataPoints: ctx.maxDataPoints,
|
||||
timeRange: {
|
||||
from: dateTime().subtract(1, 'days'),
|
||||
to: dateTime(),
|
||||
raw: { from: '1h', to: 'now' },
|
||||
},
|
||||
panelId: myPanelId, // Not 1
|
||||
});
|
||||
|
||||
const req = res.request;
|
||||
expect(req.panelId).toBe(1); // The source panel
|
||||
expect(req.targets[0].datasource).toBe('TestDB');
|
||||
expect(res.series.length).toBe(1);
|
||||
expect(res.series[0].length).toBe(2);
|
||||
|
||||
// Get the private subject and check that someone is listening
|
||||
const subject = (ctx.runner as any).subject as Subject<PanelData>;
|
||||
expect(subject.observers.length).toBe(2);
|
||||
|
||||
// Now change the query and we should stop listening
|
||||
try {
|
||||
runnerWantingSharedResults.run({
|
||||
datasource: 'unknown-datasource',
|
||||
panelId: myPanelId, // Not 1
|
||||
} as QueryRunnerOptions);
|
||||
} catch {}
|
||||
// runnerWantingSharedResults subject is now unsubscribed
|
||||
// the test listener is still subscribed
|
||||
expect(subject.observers.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
// Libraries
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { Subject, Unsubscribable, PartialObserver } from 'rxjs';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { ReplaySubject, Unsubscribable, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
// Services & Utils
|
||||
import { config } from 'app/core/config';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
import { PanelQueryState } from './PanelQueryState';
|
||||
import { isSharedDashboardQuery, SharedQueryRunner } from 'app/plugins/datasource/dashboard/SharedQueryRunner';
|
||||
import { runRequest, preProcessPanelData } from './runRequest';
|
||||
import { runSharedRequest, isSharedDashboardQuery } from '../../../plugins/datasource/dashboard';
|
||||
|
||||
// Types
|
||||
import { PanelData, DataQuery, DataQueryRequest, DataSourceApi, DataSourceJsonData } from '@grafana/ui';
|
||||
|
||||
import { TimeRange, DataTransformerConfig, transformDataFrame, toLegacyResponseData, ScopedVars } from '@grafana/data';
|
||||
import config from 'app/core/config';
|
||||
import { TimeRange, DataTransformerConfig, transformDataFrame, ScopedVars } from '@grafana/data';
|
||||
|
||||
export interface QueryRunnerOptions<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
@@ -36,109 +35,45 @@ export interface QueryRunnerOptions<
|
||||
transformations?: DataTransformerConfig[];
|
||||
}
|
||||
|
||||
export enum PanelQueryRunnerFormat {
|
||||
frames = 'frames',
|
||||
legacy = 'legacy',
|
||||
both = 'both',
|
||||
}
|
||||
|
||||
let counter = 100;
|
||||
function getNextRequestId() {
|
||||
return 'Q' + counter++;
|
||||
}
|
||||
|
||||
export class PanelQueryRunner {
|
||||
private subject?: Subject<PanelData>;
|
||||
|
||||
private state = new PanelQueryState();
|
||||
private subject?: ReplaySubject<PanelData>;
|
||||
private subscription?: Unsubscribable;
|
||||
private transformations?: DataTransformerConfig[];
|
||||
|
||||
// Listen to another panel for changes
|
||||
private sharedQueryRunner: SharedQueryRunner;
|
||||
|
||||
constructor(private panelId: number) {
|
||||
this.state.onStreamingDataUpdated = this.onStreamingDataUpdated;
|
||||
this.subject = new Subject();
|
||||
}
|
||||
|
||||
getPanelId() {
|
||||
return this.panelId;
|
||||
constructor() {
|
||||
this.subject = new ReplaySubject(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last result -- optionally skip the transformation
|
||||
* Returns an observable that subscribes to the shared multi-cast subject (that reply last result).
|
||||
*/
|
||||
// TODO: add tests
|
||||
getCurrentData(transform = true): PanelData {
|
||||
const v = this.state.validateStreamsAndGetPanelData();
|
||||
const transformData = config.featureToggles.transformations && transform;
|
||||
const hasTransformations = this.transformations && this.transformations.length;
|
||||
|
||||
if (transformData && hasTransformations) {
|
||||
const processed = transformDataFrame(this.transformations, v.series);
|
||||
return {
|
||||
...v,
|
||||
series: processed,
|
||||
legacy: processed.map(p => toLegacyResponseData(p)),
|
||||
};
|
||||
getData(transform = true): Observable<PanelData> {
|
||||
if (transform) {
|
||||
return this.subject.pipe(
|
||||
map((data: PanelData) => {
|
||||
if (this.hasTransformations()) {
|
||||
const newSeries = transformDataFrame(this.transformations, data.series);
|
||||
return { ...data, series: newSeries };
|
||||
}
|
||||
return data;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return v;
|
||||
// Just pass it directly
|
||||
return this.subject.pipe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for updates to the PanelData. If a query has already run for this panel,
|
||||
* the results will be immediatly passed to the observer
|
||||
*/
|
||||
subscribe(observer: PartialObserver<PanelData>, format = PanelQueryRunnerFormat.frames): Unsubscribable {
|
||||
if (format === PanelQueryRunnerFormat.legacy) {
|
||||
this.state.sendLegacy = true;
|
||||
} else if (format === PanelQueryRunnerFormat.both) {
|
||||
this.state.sendFrames = true;
|
||||
this.state.sendLegacy = true;
|
||||
} else {
|
||||
this.state.sendFrames = true;
|
||||
hasTransformations() {
|
||||
return config.featureToggles.transformations && this.transformations && this.transformations.length > 0;
|
||||
}
|
||||
|
||||
// Send the last result
|
||||
if (this.state.isStarted()) {
|
||||
// Force check formats again?
|
||||
this.state.getDataAfterCheckingFormats();
|
||||
observer.next(this.getCurrentData()); // transformed
|
||||
}
|
||||
|
||||
return this.subject.subscribe(observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe one runner to another
|
||||
*/
|
||||
chain(runner: PanelQueryRunner): Unsubscribable {
|
||||
const { sendLegacy, sendFrames } = runner.state;
|
||||
let format = sendFrames ? PanelQueryRunnerFormat.frames : PanelQueryRunnerFormat.legacy;
|
||||
|
||||
if (sendLegacy) {
|
||||
format = PanelQueryRunnerFormat.both;
|
||||
}
|
||||
|
||||
return this.subscribe(runner.subject, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current transformation and notify all listeners
|
||||
* Should be used only by panel editor to update the transformers
|
||||
*/
|
||||
setTransform = (transformations?: DataTransformerConfig[]) => {
|
||||
this.transformations = transformations;
|
||||
|
||||
if (this.state.isStarted()) {
|
||||
this.onStreamingDataUpdated();
|
||||
}
|
||||
};
|
||||
|
||||
async run(options: QueryRunnerOptions): Promise<PanelData> {
|
||||
const { state } = this;
|
||||
|
||||
async run(options: QueryRunnerOptions) {
|
||||
const {
|
||||
queries,
|
||||
timezone,
|
||||
@@ -152,18 +87,12 @@ export class PanelQueryRunner {
|
||||
maxDataPoints,
|
||||
scopedVars,
|
||||
minInterval,
|
||||
delayStateNotification,
|
||||
// delayStateNotification,
|
||||
} = options;
|
||||
|
||||
// Support shared queries
|
||||
if (isSharedDashboardQuery(datasource)) {
|
||||
if (!this.sharedQueryRunner) {
|
||||
this.sharedQueryRunner = new SharedQueryRunner(this);
|
||||
}
|
||||
return this.sharedQueryRunner.process(options);
|
||||
} else if (this.sharedQueryRunner) {
|
||||
this.sharedQueryRunner.disconnect();
|
||||
this.sharedQueryRunner = null;
|
||||
this.pipeToSubject(runSharedRequest(options));
|
||||
return;
|
||||
}
|
||||
|
||||
const request: DataQueryRequest = {
|
||||
@@ -185,8 +114,6 @@ export class PanelQueryRunner {
|
||||
// Add deprecated property
|
||||
(request as any).rangeRaw = timeRange.raw;
|
||||
|
||||
let loadingStateTimeoutId = 0;
|
||||
|
||||
try {
|
||||
const ds = await getDataSource(datasource, request.scopedVars);
|
||||
|
||||
@@ -215,54 +142,30 @@ export class PanelQueryRunner {
|
||||
request.interval = norm.interval;
|
||||
request.intervalMs = norm.intervalMs;
|
||||
|
||||
// Check if we can reuse the already issued query
|
||||
const active = state.getActiveRunner();
|
||||
if (active) {
|
||||
if (state.isSameQuery(ds, request)) {
|
||||
// Maybe cancel if it has run too long?
|
||||
console.log('Trying to execute query while last one has yet to complete, returning same promise');
|
||||
return active;
|
||||
} else {
|
||||
state.cancel('Query Changed while running');
|
||||
}
|
||||
}
|
||||
|
||||
// Send a loading status event on slower queries
|
||||
loadingStateTimeoutId = window.setTimeout(() => {
|
||||
if (state.getActiveRunner()) {
|
||||
this.subject.next(this.state.validateStreamsAndGetPanelData());
|
||||
}
|
||||
}, delayStateNotification || 500);
|
||||
|
||||
this.transformations = options.transformations;
|
||||
|
||||
const data = await state.execute(ds, request);
|
||||
// Clear the delayed loading state timeout
|
||||
clearTimeout(loadingStateTimeoutId);
|
||||
|
||||
// Broadcast results
|
||||
this.subject.next(this.getCurrentData());
|
||||
return data;
|
||||
this.pipeToSubject(runRequest(ds, request));
|
||||
} catch (err) {
|
||||
clearTimeout(loadingStateTimeoutId);
|
||||
|
||||
const data = state.setError(err);
|
||||
this.subject.next(data);
|
||||
return data;
|
||||
console.log('PanelQueryRunner Error', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after every streaming event. This should be throttled so we
|
||||
* avoid accidentally overwhelming the browser
|
||||
*/
|
||||
onStreamingDataUpdated = throttle(
|
||||
() => {
|
||||
this.subject.next(this.getCurrentData());
|
||||
private pipeToSubject(observable: Observable<PanelData>) {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Makes sure everything is a proper DataFrame
|
||||
const prepare = preProcessPanelData();
|
||||
|
||||
this.subscription = observable.subscribe({
|
||||
next: (data: PanelData) => {
|
||||
this.subject.next(prepare(data));
|
||||
},
|
||||
50,
|
||||
{ trailing: true, leading: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setTransformations(transformations?: DataTransformerConfig[]) {
|
||||
this.transformations = transformations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the panel is closed
|
||||
@@ -273,17 +176,10 @@ export class PanelQueryRunner {
|
||||
this.subject.complete();
|
||||
}
|
||||
|
||||
// Will cancel and disconnect any open requets
|
||||
this.state.cancel('destroy');
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
setState = (state: PanelQueryState) => {
|
||||
this.state = state;
|
||||
};
|
||||
|
||||
getState = () => {
|
||||
return this.state;
|
||||
};
|
||||
}
|
||||
|
||||
async function getDataSource(
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
import { toDataQueryError, PanelQueryState, getProcessedDataFrames } from './PanelQueryState';
|
||||
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
|
||||
import { LoadingState, getDataFrameRow } from '@grafana/data';
|
||||
import { DataQueryResponse, DataQueryRequest, DataQuery } from '@grafana/ui';
|
||||
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
||||
|
||||
describe('PanelQueryState', () => {
|
||||
it('converts anythign to an error', () => {
|
||||
let err = toDataQueryError(undefined);
|
||||
expect(err.message).toEqual('Query error');
|
||||
|
||||
err = toDataQueryError('STRING ERRROR');
|
||||
expect(err.message).toEqual('STRING ERRROR');
|
||||
|
||||
err = toDataQueryError({ message: 'hello' });
|
||||
expect(err.message).toEqual('hello');
|
||||
});
|
||||
|
||||
it('keeps track of running queries', async () => {
|
||||
const state = new PanelQueryState();
|
||||
expect(state.getActiveRunner()).toBeFalsy();
|
||||
let hasRun = false;
|
||||
const dsRunner = new Promise<DataQueryResponse>((resolve, reject) => {
|
||||
// The status should be running when we get here
|
||||
expect(state.getActiveRunner()).toBeTruthy();
|
||||
resolve({ data: ['x', 'y'] });
|
||||
hasRun = true;
|
||||
});
|
||||
const ds = new MockDataSourceApi('test');
|
||||
ds.queryResolver = dsRunner;
|
||||
|
||||
// should not actually run for an empty query
|
||||
let empty = await state.execute(ds, getQueryOptions({}));
|
||||
expect(state.getActiveRunner()).toBeFalsy();
|
||||
expect(empty.series.length).toBe(0);
|
||||
expect(hasRun).toBeFalsy();
|
||||
|
||||
const query = getQueryOptions({
|
||||
targets: [{ hide: true, refId: 'X' }, { hide: true, refId: 'Y' }, { hide: true, refId: 'Z' }],
|
||||
});
|
||||
|
||||
empty = await state.execute(ds, query);
|
||||
// should not run any hidden queries'
|
||||
expect(state.getActiveRunner()).toBeFalsy();
|
||||
expect(empty.series.length).toBe(0);
|
||||
expect(hasRun).toBeFalsy();
|
||||
|
||||
// Check for the same query
|
||||
expect(state.isSameQuery(ds, query)).toBeTruthy();
|
||||
|
||||
// Check for differnet queries
|
||||
expect(state.isSameQuery(new MockDataSourceApi('test'), query)).toBeFalsy();
|
||||
expect(state.isSameQuery(ds, getQueryOptions({ targets: [{ refId: 'differnet' }] }))).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When cancelling request', () => {
|
||||
it('Should call rejector', () => {
|
||||
const state = new PanelQueryState();
|
||||
state.request = {} as DataQueryRequest<DataQuery>;
|
||||
(state as any).rejector = (obj: any) => {
|
||||
expect(obj.cancelled).toBe(true);
|
||||
expect(obj.message).toBe('OHH');
|
||||
};
|
||||
|
||||
state.cancel('OHH');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProcessedDataFrame', () => {
|
||||
it('converts timeseries to table skipping nulls', () => {
|
||||
const input1 = {
|
||||
target: 'Field Name',
|
||||
datapoints: [[100, 1], [200, 2]],
|
||||
};
|
||||
const input2 = {
|
||||
// without target
|
||||
target: '',
|
||||
datapoints: [[100, 1], [200, 2]],
|
||||
};
|
||||
const data = getProcessedDataFrames([null, input1, input2, null, null]);
|
||||
expect(data.length).toBe(2);
|
||||
expect(data[0].fields[0].name).toBe(input1.target);
|
||||
|
||||
const cmp = [getDataFrameRow(data[0], 0), getDataFrameRow(data[0], 1)];
|
||||
expect(cmp).toEqual(input1.datapoints);
|
||||
|
||||
// Default name
|
||||
expect(data[1].fields[0].name).toEqual('Value');
|
||||
|
||||
// Every colun should have a name and a type
|
||||
for (const table of data) {
|
||||
for (const field of table.fields) {
|
||||
expect(field.name).toBeDefined();
|
||||
expect(field.type).toBeDefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('supports null values from query OK', () => {
|
||||
expect(getProcessedDataFrames([null, null, null, null])).toEqual([]);
|
||||
expect(getProcessedDataFrames(undefined)).toEqual([]);
|
||||
expect(getProcessedDataFrames((null as unknown) as any[])).toEqual([]);
|
||||
expect(getProcessedDataFrames([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function makeSeriesStub(refId: string) {
|
||||
return {
|
||||
fields: [{ name: undefined }],
|
||||
refId,
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('stream handling', () => {
|
||||
const state = new PanelQueryState();
|
||||
state.onStreamingDataUpdated = () => {
|
||||
// nothing
|
||||
};
|
||||
state.request = {
|
||||
requestId: '123',
|
||||
range: {
|
||||
raw: {
|
||||
from: 123, // if string it gets revaluated
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
state.response = {
|
||||
state: LoadingState.Done,
|
||||
series: [makeSeriesStub('A'), makeSeriesStub('B')],
|
||||
};
|
||||
|
||||
it('gets the response', () => {
|
||||
const data = state.validateStreamsAndGetPanelData();
|
||||
expect(data.series.length).toBe(2);
|
||||
expect(data.state).toBe(LoadingState.Done);
|
||||
expect(data.series[0].refId).toBe('A');
|
||||
});
|
||||
|
||||
it('adds a stream event', () => {
|
||||
// Post a stream event
|
||||
state.dataStreamObserver({
|
||||
state: LoadingState.Loading,
|
||||
key: 'C',
|
||||
request: state.request, // From the same request
|
||||
data: [makeSeriesStub('C')],
|
||||
unsubscribe: () => {},
|
||||
});
|
||||
expect(state.streams.length).toBe(1);
|
||||
|
||||
const data = state.validateStreamsAndGetPanelData();
|
||||
expect(data.series.length).toBe(3);
|
||||
expect(data.state).toBe(LoadingState.Streaming);
|
||||
expect(data.series[2].refId).toBe('C');
|
||||
});
|
||||
|
||||
it('add another stream event (with a differnet key)', () => {
|
||||
// Post a stream event
|
||||
state.dataStreamObserver({
|
||||
state: LoadingState.Loading,
|
||||
key: 'D',
|
||||
request: state.request, // From the same request
|
||||
data: [makeSeriesStub('D')],
|
||||
unsubscribe: () => {},
|
||||
});
|
||||
expect(state.streams.length).toBe(2);
|
||||
|
||||
const data = state.validateStreamsAndGetPanelData();
|
||||
expect(data.series.length).toBe(4);
|
||||
expect(data.state).toBe(LoadingState.Streaming);
|
||||
expect(data.series[3].refId).toBe('D');
|
||||
});
|
||||
|
||||
it('replace the first stream value, but keep the order', () => {
|
||||
// Post a stream event
|
||||
state.dataStreamObserver({
|
||||
state: LoadingState.Loading,
|
||||
key: 'C', // The key to replace previous index 2
|
||||
request: state.request, // From the same request
|
||||
data: [makeSeriesStub('X')],
|
||||
unsubscribe: () => {},
|
||||
});
|
||||
expect(state.streams.length).toBe(2);
|
||||
|
||||
const data = state.validateStreamsAndGetPanelData();
|
||||
expect(data.series[2].refId).toBe('X');
|
||||
});
|
||||
|
||||
it('ignores streams from a differnet request', () => {
|
||||
// Post a stream event
|
||||
state.dataStreamObserver({
|
||||
state: LoadingState.Loading,
|
||||
key: 'Z', // Note with key 'A' it would still overwrite
|
||||
request: {
|
||||
...state.request,
|
||||
requestId: 'XXX', // Different request and id
|
||||
} as any,
|
||||
data: [makeSeriesStub('C')],
|
||||
unsubscribe: () => {},
|
||||
});
|
||||
|
||||
expect(state.streams.length).toBe(2); // no change
|
||||
const data = state.validateStreamsAndGetPanelData();
|
||||
expect(data.series.length).toBe(4);
|
||||
});
|
||||
|
||||
it('removes streams when the query changes', () => {
|
||||
state.request = {
|
||||
...state.request,
|
||||
requestId: 'somethine else',
|
||||
} as any;
|
||||
state.response = {
|
||||
state: LoadingState.Done,
|
||||
series: [makeSeriesStub('F')],
|
||||
};
|
||||
expect(state.streams.length).toBe(2); // unchanged
|
||||
|
||||
const data = state.validateStreamsAndGetPanelData();
|
||||
expect(data.series.length).toBe(1);
|
||||
expect(data.series[0].refId).toBe('F');
|
||||
expect(state.streams.length).toBe(0); // no streams
|
||||
});
|
||||
|
||||
it('should close streams on error', () => {
|
||||
// Post a stream event
|
||||
state.dataStreamObserver({
|
||||
state: LoadingState.Error,
|
||||
key: 'C',
|
||||
error: { message: 'EEEEE' },
|
||||
data: [],
|
||||
request: state.request,
|
||||
unsubscribe: () => {},
|
||||
});
|
||||
|
||||
expect(state.streams.length).toBe(0);
|
||||
expect(state.response.state).toBe(LoadingState.Error);
|
||||
});
|
||||
});
|
||||
@@ -1,377 +0,0 @@
|
||||
// Libraries
|
||||
import { isArray, isEqual, isString } from 'lodash';
|
||||
// Utils & Services
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import {
|
||||
dateMath,
|
||||
guessFieldTypes,
|
||||
LoadingState,
|
||||
toLegacyResponseData,
|
||||
DataFrame,
|
||||
toDataFrame,
|
||||
isDataFrame,
|
||||
} from '@grafana/data';
|
||||
// Types
|
||||
import {
|
||||
DataSourceApi,
|
||||
DataQueryRequest,
|
||||
PanelData,
|
||||
DataQueryError,
|
||||
DataStreamObserver,
|
||||
DataStreamState,
|
||||
DataQueryResponseData,
|
||||
} from '@grafana/ui';
|
||||
|
||||
export class PanelQueryState {
|
||||
// The current/last running request
|
||||
request = {
|
||||
startTime: 0,
|
||||
endTime: 1000, // Somethign not zero
|
||||
} as DataQueryRequest;
|
||||
|
||||
// The result back from the datasource query
|
||||
response = {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
} as PanelData;
|
||||
|
||||
// Active stream results
|
||||
streams: DataStreamState[] = [];
|
||||
|
||||
sendFrames = false;
|
||||
sendLegacy = false;
|
||||
|
||||
// A promise for the running query
|
||||
private executor?: Promise<PanelData> = null;
|
||||
private rejector = (reason?: any) => {};
|
||||
private datasource: DataSourceApi = {} as any;
|
||||
|
||||
isFinished(state: LoadingState) {
|
||||
return state === LoadingState.Done || state === LoadingState.Error;
|
||||
}
|
||||
|
||||
isStarted() {
|
||||
return this.response.state !== LoadingState.NotStarted;
|
||||
}
|
||||
|
||||
isSameQuery(ds: DataSourceApi, req: DataQueryRequest) {
|
||||
if (ds !== this.datasource) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For now just check that the targets look the same
|
||||
return isEqual(this.request.targets, req.targets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the currently running query
|
||||
*/
|
||||
getActiveRunner(): Promise<PanelData> | undefined {
|
||||
return this.executor;
|
||||
}
|
||||
|
||||
cancel(reason: string) {
|
||||
const { request } = this;
|
||||
this.executor = null;
|
||||
|
||||
try {
|
||||
// If no endTime the call to datasource.query did not complete
|
||||
// call rejector to reject the executor promise
|
||||
if (!request.endTime) {
|
||||
request.endTime = Date.now();
|
||||
this.rejector({ cancelled: true, message: reason });
|
||||
}
|
||||
|
||||
// Cancel any open HTTP request with the same ID
|
||||
if (request.requestId) {
|
||||
getBackendSrv().resolveCancelerIfExists(request.requestId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Error canceling request', err);
|
||||
}
|
||||
|
||||
// Close any open streams
|
||||
this.closeStreams(true);
|
||||
}
|
||||
|
||||
execute(ds: DataSourceApi, req: DataQueryRequest): Promise<PanelData> {
|
||||
this.request = {
|
||||
...req,
|
||||
startTime: Date.now(),
|
||||
};
|
||||
this.datasource = ds;
|
||||
|
||||
// Return early if there are no queries to run
|
||||
if (!req.targets.length) {
|
||||
console.log('No queries, so return early');
|
||||
this.request.endTime = Date.now();
|
||||
this.closeStreams();
|
||||
return Promise.resolve(
|
||||
(this.response = {
|
||||
state: LoadingState.Done,
|
||||
series: [], // Clear the data
|
||||
legacy: [],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Set the loading state immediately
|
||||
this.response.state = LoadingState.Loading;
|
||||
this.executor = new Promise<PanelData>((resolve, reject) => {
|
||||
this.rejector = reject;
|
||||
|
||||
return ds
|
||||
.query(this.request, this.dataStreamObserver)
|
||||
.then(resp => {
|
||||
if (!isArray(resp.data)) {
|
||||
throw new Error(`Expected response data to be array, got ${typeof resp.data}.`);
|
||||
}
|
||||
|
||||
this.request.endTime = Date.now();
|
||||
this.executor = null;
|
||||
|
||||
// Make sure we send something back -- called run() w/o subscribe!
|
||||
if (!(this.sendFrames || this.sendLegacy)) {
|
||||
this.sendFrames = true;
|
||||
}
|
||||
|
||||
// Save the result state
|
||||
this.response = {
|
||||
state: LoadingState.Done,
|
||||
request: this.request,
|
||||
series: this.sendFrames ? getProcessedDataFrames(resp.data) : [],
|
||||
legacy: this.sendLegacy ? translateToLegacyData(resp.data) : undefined,
|
||||
};
|
||||
resolve(this.validateStreamsAndGetPanelData());
|
||||
})
|
||||
.catch(err => {
|
||||
this.executor = null;
|
||||
resolve(this.setError(err));
|
||||
});
|
||||
});
|
||||
|
||||
return this.executor;
|
||||
}
|
||||
|
||||
// Send a notice when the stream has updated the current model
|
||||
onStreamingDataUpdated: () => void;
|
||||
|
||||
// This gets all stream events and keeps track of them
|
||||
// it will then delegate real changes to the PanelQueryRunner
|
||||
dataStreamObserver: DataStreamObserver = (stream: DataStreamState) => {
|
||||
// Streams only work with the 'series' format
|
||||
this.sendFrames = true;
|
||||
|
||||
if (stream.state === LoadingState.Error) {
|
||||
this.setError(stream.error);
|
||||
this.onStreamingDataUpdated();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the stream to our list
|
||||
let found = false;
|
||||
const active = this.streams.map(s => {
|
||||
if (s.key === stream.key) {
|
||||
found = true;
|
||||
return stream;
|
||||
}
|
||||
return s;
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
if (shouldDisconnect(this.request, stream)) {
|
||||
console.log('Got stream update from old stream, unsubscribing');
|
||||
stream.unsubscribe();
|
||||
return;
|
||||
}
|
||||
active.push(stream);
|
||||
}
|
||||
|
||||
this.streams = active;
|
||||
this.onStreamingDataUpdated();
|
||||
};
|
||||
|
||||
closeStreams(keepSeries = false) {
|
||||
if (!this.streams.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const series: DataFrame[] = [];
|
||||
|
||||
for (const stream of this.streams) {
|
||||
if (stream.data) {
|
||||
series.push.apply(series, stream.data);
|
||||
}
|
||||
|
||||
try {
|
||||
stream.unsubscribe();
|
||||
} catch {
|
||||
console.log('Failed to unsubscribe to stream');
|
||||
}
|
||||
}
|
||||
|
||||
this.streams = [];
|
||||
|
||||
// Move the series from streams to the response
|
||||
if (keepSeries) {
|
||||
const { response } = this;
|
||||
this.response = {
|
||||
...response,
|
||||
series: [
|
||||
...response.series,
|
||||
...series, // Append the streamed series
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called before broadcasting data to listeners. Given that
|
||||
* stream events can happen at any point, we need to make sure to
|
||||
* only return data from active streams.
|
||||
*/
|
||||
validateStreamsAndGetPanelData(): PanelData {
|
||||
const { response, streams, request } = this;
|
||||
|
||||
// When not streaming, return the response + request
|
||||
if (!streams.length) {
|
||||
return {
|
||||
...response,
|
||||
request: request,
|
||||
};
|
||||
}
|
||||
|
||||
let done = this.isFinished(response.state);
|
||||
const series = [...response.series];
|
||||
const active: DataStreamState[] = [];
|
||||
|
||||
for (const stream of this.streams) {
|
||||
if (shouldDisconnect(request, stream)) {
|
||||
console.log('getPanelData() - shouldDisconnect true, unsubscribing to steam');
|
||||
stream.unsubscribe();
|
||||
continue;
|
||||
}
|
||||
|
||||
active.push(stream);
|
||||
series.push.apply(series, stream.data);
|
||||
|
||||
if (!this.isFinished(stream.state)) {
|
||||
done = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.streams = active;
|
||||
|
||||
// Update the time range
|
||||
let timeRange = this.request.range;
|
||||
if (isString(timeRange.raw.from)) {
|
||||
timeRange = {
|
||||
from: dateMath.parse(timeRange.raw.from, false),
|
||||
to: dateMath.parse(timeRange.raw.to, true),
|
||||
raw: timeRange.raw,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state: done ? LoadingState.Done : LoadingState.Streaming,
|
||||
// This should not be needed but unfortunately Prometheus datasource sends non DataFrame here bypassing the
|
||||
// typings
|
||||
series: this.sendFrames ? getProcessedDataFrames(series) : [],
|
||||
legacy: this.sendLegacy ? translateToLegacyData(series) : undefined,
|
||||
request: {
|
||||
...this.request,
|
||||
range: timeRange, // update the time range
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure all requested formats exist on the data
|
||||
*/
|
||||
getDataAfterCheckingFormats(): PanelData {
|
||||
const { response, sendLegacy, sendFrames } = this;
|
||||
if (sendLegacy && (!response.legacy || !response.legacy.length)) {
|
||||
response.legacy = response.series.map(v => toLegacyResponseData(v));
|
||||
}
|
||||
if (sendFrames && !response.series.length && response.legacy) {
|
||||
response.series = response.legacy.map(v => toDataFrame(v));
|
||||
}
|
||||
return this.validateStreamsAndGetPanelData();
|
||||
}
|
||||
|
||||
setError(err: any): PanelData {
|
||||
if (!this.request.endTime) {
|
||||
this.request.endTime = Date.now();
|
||||
}
|
||||
this.closeStreams(true);
|
||||
this.response = {
|
||||
...this.response, // Keep any existing data
|
||||
state: LoadingState.Error,
|
||||
error: toDataQueryError(err),
|
||||
};
|
||||
return this.validateStreamsAndGetPanelData();
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldDisconnect(source: DataQueryRequest, state: DataStreamState) {
|
||||
// It came from the same the same request, so keep it
|
||||
if (source === state.request || state.request.requestId.startsWith(source.requestId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We should be able to check that it is the same query regardless of
|
||||
// if it came from the same request. This will be important for #16676
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function toDataQueryError(err: any): DataQueryError {
|
||||
const error = (err || {}) as DataQueryError;
|
||||
if (!error.message) {
|
||||
if (typeof err === 'string' || err instanceof String) {
|
||||
return { message: err } as DataQueryError;
|
||||
}
|
||||
|
||||
let message = 'Query error';
|
||||
if (error.message) {
|
||||
message = error.message;
|
||||
} else if (error.data && error.data.message) {
|
||||
message = error.data.message;
|
||||
} else if (error.data && error.data.error) {
|
||||
message = error.data.error;
|
||||
} else if (error.status) {
|
||||
message = `Query error: ${error.status} ${error.statusText}`;
|
||||
}
|
||||
error.message = message;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
function translateToLegacyData(data: DataQueryResponseData) {
|
||||
return data.map((v: any) => {
|
||||
if (isDataFrame(v)) {
|
||||
return toLegacyResponseData(v);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* All panels will be passed tables that have our best guess at colum type set
|
||||
*
|
||||
* This is also used by PanelChrome for snapshot support
|
||||
*/
|
||||
export function getProcessedDataFrames(results?: DataQueryResponseData[]): DataFrame[] {
|
||||
if (!isArray(results)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const series: DataFrame[] = [];
|
||||
for (const r of results) {
|
||||
if (r) {
|
||||
series.push(guessFieldTypes(toDataFrame(r)));
|
||||
}
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import { DataFrame, LoadingState, dateTime } from '@grafana/data';
|
||||
import { PanelData, DataSourceApi, DataQueryRequest, DataQueryResponse } from '@grafana/ui';
|
||||
import { Subscriber, Observable, Subscription } from 'rxjs';
|
||||
import { runRequest } from './runRequest';
|
||||
|
||||
jest.mock('app/core/services/backend_srv');
|
||||
|
||||
class ScenarioCtx {
|
||||
ds: DataSourceApi;
|
||||
request: DataQueryRequest;
|
||||
subscriber: Subscriber<DataQueryResponse>;
|
||||
isUnsubbed = false;
|
||||
setupFn: () => void = () => {};
|
||||
results: PanelData[];
|
||||
subscription: Subscription;
|
||||
wasStarted = false;
|
||||
error: Error = null;
|
||||
toStartTime = dateTime();
|
||||
fromStartTime = dateTime();
|
||||
|
||||
reset() {
|
||||
this.wasStarted = false;
|
||||
this.isUnsubbed = false;
|
||||
|
||||
this.results = [];
|
||||
this.request = {
|
||||
range: {
|
||||
from: this.toStartTime,
|
||||
to: this.fromStartTime,
|
||||
raw: { from: '1h', to: 'now' },
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
},
|
||||
],
|
||||
} as DataQueryRequest;
|
||||
|
||||
this.ds = {
|
||||
query: (request: DataQueryRequest) => {
|
||||
return new Observable<DataQueryResponse>(subscriber => {
|
||||
this.subscriber = subscriber;
|
||||
this.wasStarted = true;
|
||||
|
||||
if (this.error) {
|
||||
throw this.error;
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.isUnsubbed = true;
|
||||
};
|
||||
});
|
||||
},
|
||||
} as DataSourceApi;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.subscription = runRequest(this.ds, this.request).subscribe({
|
||||
next: (data: PanelData) => {
|
||||
this.results.push(data);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
emitPacket(packet: DataQueryResponse) {
|
||||
this.subscriber.next(packet);
|
||||
}
|
||||
|
||||
setup(fn: () => void) {
|
||||
this.setupFn = fn;
|
||||
}
|
||||
}
|
||||
|
||||
function runRequestScenario(desc: string, fn: (ctx: ScenarioCtx) => void) {
|
||||
describe(desc, () => {
|
||||
const ctx = new ScenarioCtx();
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.reset();
|
||||
return ctx.setupFn();
|
||||
});
|
||||
|
||||
fn(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
describe('runRequest', () => {
|
||||
runRequestScenario('with no queries', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.request.targets = [];
|
||||
ctx.start();
|
||||
});
|
||||
|
||||
it('should emit empty result with loading state done', () => {
|
||||
expect(ctx.wasStarted).toBe(false);
|
||||
expect(ctx.results[0].state).toBe(LoadingState.Done);
|
||||
});
|
||||
});
|
||||
|
||||
runRequestScenario('After first response', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.start();
|
||||
ctx.emitPacket({
|
||||
data: [{ name: 'Data' } as DataFrame],
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit single result with loading state done', () => {
|
||||
expect(ctx.wasStarted).toBe(true);
|
||||
expect(ctx.results.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
runRequestScenario('After tree responses, 2 with different keys', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.start();
|
||||
ctx.emitPacket({
|
||||
data: [{ name: 'DataA-1' } as DataFrame],
|
||||
key: 'A',
|
||||
});
|
||||
ctx.emitPacket({
|
||||
data: [{ name: 'DataA-2' } as DataFrame],
|
||||
key: 'A',
|
||||
});
|
||||
ctx.emitPacket({
|
||||
data: [{ name: 'DataB-1' } as DataFrame],
|
||||
key: 'B',
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit 3 seperate results', () => {
|
||||
expect(ctx.results.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should combine results and return latest data for key A', () => {
|
||||
expect(ctx.results[2].series).toEqual([{ name: 'DataA-2' }, { name: 'DataB-1' }]);
|
||||
});
|
||||
|
||||
it('should have loading state Done', () => {
|
||||
expect(ctx.results[2].state).toEqual(LoadingState.Done);
|
||||
});
|
||||
});
|
||||
|
||||
runRequestScenario('After response with state Streaming', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.start();
|
||||
ctx.emitPacket({
|
||||
data: [{ name: 'DataA-1' } as DataFrame],
|
||||
key: 'A',
|
||||
});
|
||||
ctx.emitPacket({
|
||||
data: [{ name: 'DataA-2' } as DataFrame],
|
||||
key: 'A',
|
||||
state: LoadingState.Streaming,
|
||||
});
|
||||
});
|
||||
|
||||
it('should have loading state Streaming', () => {
|
||||
expect(ctx.results[1].state).toEqual(LoadingState.Streaming);
|
||||
});
|
||||
});
|
||||
|
||||
runRequestScenario('If no response after 250ms', ctx => {
|
||||
ctx.setup(async () => {
|
||||
ctx.start();
|
||||
await sleep(250);
|
||||
});
|
||||
|
||||
it('should emit 1 result with loading state', () => {
|
||||
expect(ctx.results.length).toBe(1);
|
||||
expect(ctx.results[0].state).toBe(LoadingState.Loading);
|
||||
});
|
||||
});
|
||||
|
||||
runRequestScenario('on thrown error', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.error = new Error('Ohh no');
|
||||
ctx.start();
|
||||
});
|
||||
|
||||
it('should emit 1 error result', () => {
|
||||
expect(ctx.results[0].error.message).toBe('Ohh no');
|
||||
expect(ctx.results[0].state).toBe(LoadingState.Error);
|
||||
});
|
||||
});
|
||||
|
||||
runRequestScenario('If time range is relative', ctx => {
|
||||
ctx.setup(async () => {
|
||||
ctx.start();
|
||||
// wait a bit
|
||||
await sleep(20);
|
||||
|
||||
ctx.emitPacket({ data: [{ name: 'DataB-1' } as DataFrame] });
|
||||
});
|
||||
|
||||
it('should update returned request range', () => {
|
||||
expect(ctx.results[0].request.range.to.valueOf()).not.toBe(ctx.fromStartTime);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// Libraries
|
||||
import { Observable, of, timer, merge, from } from 'rxjs';
|
||||
import { flatten, map as lodashMap, isArray, isString } from 'lodash';
|
||||
import { map, catchError, takeUntil, mapTo, share, finalize } from 'rxjs/operators';
|
||||
// Utils & Services
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
// Types
|
||||
import {
|
||||
DataSourceApi,
|
||||
DataQueryRequest,
|
||||
PanelData,
|
||||
DataQueryResponse,
|
||||
DataQueryResponseData,
|
||||
DataQueryError,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { LoadingState, dateMath, toDataFrame, DataFrame, guessFieldTypes } from '@grafana/data';
|
||||
|
||||
type MapOfResponsePackets = { [str: string]: DataQueryResponse };
|
||||
|
||||
interface RunningQueryState {
|
||||
packets: { [key: string]: DataQueryResponse };
|
||||
panelData: PanelData;
|
||||
}
|
||||
|
||||
/*
|
||||
* This function should handle composing a PanelData from multiple responses
|
||||
*/
|
||||
export function processResponsePacket(packet: DataQueryResponse, state: RunningQueryState): RunningQueryState {
|
||||
const request = state.panelData.request;
|
||||
const packets: MapOfResponsePackets = {
|
||||
...state.packets,
|
||||
};
|
||||
|
||||
packets[packet.key || 'A'] = packet;
|
||||
|
||||
// Update the time range
|
||||
let timeRange = request.range;
|
||||
if (isString(timeRange.raw.from)) {
|
||||
timeRange = {
|
||||
from: dateMath.parse(timeRange.raw.from, false),
|
||||
to: dateMath.parse(timeRange.raw.to, true),
|
||||
raw: timeRange.raw,
|
||||
};
|
||||
}
|
||||
|
||||
const combinedData = flatten(
|
||||
lodashMap(packets, (packet: DataQueryResponse) => {
|
||||
return packet.data;
|
||||
})
|
||||
);
|
||||
|
||||
const panelData = {
|
||||
state: packet.state || LoadingState.Done,
|
||||
series: combinedData,
|
||||
request: {
|
||||
...request,
|
||||
range: timeRange,
|
||||
},
|
||||
};
|
||||
|
||||
return { packets, panelData };
|
||||
}
|
||||
|
||||
/**
|
||||
* This function handles the excecution of requests & and processes the single or multiple response packets into
|
||||
* a combined PanelData response.
|
||||
* It will
|
||||
* * Merge multiple responses into a single DataFrame array based on the packet key
|
||||
* * Will emit a loading state if no response after 50ms
|
||||
* * Cancel any still runnning network requests on unsubscribe (using request.requestId)
|
||||
*/
|
||||
export function runRequest(datasource: DataSourceApi, request: DataQueryRequest): Observable<PanelData> {
|
||||
let state: RunningQueryState = {
|
||||
panelData: {
|
||||
state: LoadingState.Loading,
|
||||
series: [],
|
||||
request: request,
|
||||
},
|
||||
packets: {},
|
||||
};
|
||||
|
||||
// Return early if there are no queries to run
|
||||
if (!request.targets.length) {
|
||||
request.endTime = Date.now();
|
||||
state.panelData.state = LoadingState.Done;
|
||||
return of(state.panelData);
|
||||
}
|
||||
|
||||
const dataObservable = callQueryMethod(datasource, request).pipe(
|
||||
// Transform response packets into PanelData with merged results
|
||||
map((packet: DataQueryResponse) => {
|
||||
if (!isArray(packet.data)) {
|
||||
throw new Error(`Expected response data to be array, got ${typeof packet.data}.`);
|
||||
}
|
||||
|
||||
request.endTime = Date.now();
|
||||
|
||||
state = processResponsePacket(packet, state);
|
||||
return state.panelData;
|
||||
}),
|
||||
// handle errors
|
||||
catchError(err =>
|
||||
of({
|
||||
...state.panelData,
|
||||
state: LoadingState.Error,
|
||||
error: processQueryError(err),
|
||||
})
|
||||
),
|
||||
// finalize is triggered when subscriber unsubscribes
|
||||
// This makes sure any still running network requests are cancelled
|
||||
finalize(cancelNetworkRequestsOnUnsubscribe(request)),
|
||||
// this makes it possible to share this observable in takeUntil
|
||||
share()
|
||||
);
|
||||
|
||||
// If 50ms without a response emit a loading state
|
||||
// mapTo will translate the timer event into state.panelData (which has state set to loading)
|
||||
// takeUntil will cancel the timer emit when first response packet is received on the dataObservable
|
||||
return merge(
|
||||
timer(200).pipe(
|
||||
mapTo(state.panelData),
|
||||
takeUntil(dataObservable)
|
||||
),
|
||||
dataObservable
|
||||
);
|
||||
}
|
||||
|
||||
function cancelNetworkRequestsOnUnsubscribe(req: DataQueryRequest) {
|
||||
return () => {
|
||||
getBackendSrv().resolveCancelerIfExists(req.requestId);
|
||||
};
|
||||
}
|
||||
|
||||
export function callQueryMethod(datasource: DataSourceApi, request: DataQueryRequest) {
|
||||
const returnVal = datasource.query(request);
|
||||
return from(returnVal);
|
||||
}
|
||||
|
||||
export function processQueryError(err: any): DataQueryError {
|
||||
const error = (err || {}) as DataQueryError;
|
||||
|
||||
if (!error.message) {
|
||||
if (typeof err === 'string' || err instanceof String) {
|
||||
return { message: err } as DataQueryError;
|
||||
}
|
||||
|
||||
let message = 'Query error';
|
||||
if (error.message) {
|
||||
message = error.message;
|
||||
} else if (error.data && error.data.message) {
|
||||
message = error.data.message;
|
||||
} else if (error.data && error.data.error) {
|
||||
message = error.data.error;
|
||||
} else if (error.status) {
|
||||
message = `Query error: ${error.status} ${error.statusText}`;
|
||||
}
|
||||
error.message = message;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* All panels will be passed tables that have our best guess at colum type set
|
||||
*
|
||||
* This is also used by PanelChrome for snapshot support
|
||||
*/
|
||||
export function getProcessedDataFrames(results?: DataQueryResponseData[]): DataFrame[] {
|
||||
if (!isArray(results)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dataFrames: DataFrame[] = [];
|
||||
|
||||
for (const result of results) {
|
||||
const dataFrame = guessFieldTypes(toDataFrame(result));
|
||||
|
||||
// clear out any cached calcs
|
||||
for (const field of dataFrame.fields) {
|
||||
field.calcs = null;
|
||||
}
|
||||
|
||||
dataFrames.push(dataFrame);
|
||||
}
|
||||
|
||||
return dataFrames;
|
||||
}
|
||||
|
||||
export function preProcessPanelData() {
|
||||
let lastResult: PanelData = null;
|
||||
|
||||
return function mapper(data: PanelData) {
|
||||
let { series } = data;
|
||||
|
||||
// for loading states with no data, use last result
|
||||
if (data.state === LoadingState.Loading && series.length === 0) {
|
||||
if (!lastResult) {
|
||||
lastResult = data;
|
||||
}
|
||||
|
||||
return { ...lastResult, state: LoadingState.Loading };
|
||||
}
|
||||
|
||||
// Makes sure the data is properly formatted
|
||||
series = getProcessedDataFrames(series);
|
||||
|
||||
lastResult = { ...data, series };
|
||||
return lastResult;
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// Types
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
import { Emitter } from 'app/core/core';
|
||||
import { DataQuery, DataSourceSelectItem, DataSourceApi, QueryFixAction, PanelData } from '@grafana/ui';
|
||||
|
||||
@@ -128,6 +129,11 @@ export interface QueryEndedPayload {
|
||||
response: PanelData;
|
||||
}
|
||||
|
||||
export interface QueryStoreSubscriptionPayload {
|
||||
exploreId: ExploreId;
|
||||
querySubscription: Unsubscribable;
|
||||
}
|
||||
|
||||
export interface HistoryUpdatedPayload {
|
||||
exploreId: ExploreId;
|
||||
history: HistoryItem[];
|
||||
@@ -307,6 +313,10 @@ export const queryStreamUpdatedAction = actionCreatorFactory<QueryEndedPayload>(
|
||||
'explore/QUERY_STREAM_UPDATED'
|
||||
).create();
|
||||
|
||||
export const queryStoreSubscriptionAction = actionCreatorFactory<QueryStoreSubscriptionPayload>(
|
||||
'explore/QUERY_STORE_SUBSCRIPTION'
|
||||
).create();
|
||||
|
||||
/**
|
||||
* Remove query row of the given index, as well as associated query results.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Libraries
|
||||
import { map } from 'rxjs/operators';
|
||||
// Services & Utils
|
||||
import store from 'app/core/store';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
@@ -63,21 +64,20 @@ import {
|
||||
loadExploreDatasources,
|
||||
changeModeAction,
|
||||
scanStopAction,
|
||||
queryStartAction,
|
||||
setUrlReplacedAction,
|
||||
changeRangeAction,
|
||||
historyUpdatedAction,
|
||||
queryEndedAction,
|
||||
queryStreamUpdatedAction,
|
||||
queryStoreSubscriptionAction,
|
||||
clearOriginAction,
|
||||
} from './actionTypes';
|
||||
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||
import { offOption } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
|
||||
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
|
||||
import _ from 'lodash';
|
||||
import { updateLocation } from '../../../core/actions';
|
||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { runRequest, preProcessPanelData } from '../../dashboard/state/runRequest';
|
||||
|
||||
/**
|
||||
* Updates UI state and save it to the URL
|
||||
@@ -436,12 +436,14 @@ export function runQueries(exploreId: ExploreId): ThunkResult<void> {
|
||||
datasourceError,
|
||||
containerWidth,
|
||||
isLive: live,
|
||||
queryState,
|
||||
queryIntervals,
|
||||
range,
|
||||
scanning,
|
||||
querySubscription,
|
||||
history,
|
||||
mode,
|
||||
showingGraph,
|
||||
showingTable,
|
||||
} = exploreItemState;
|
||||
|
||||
if (datasourceError) {
|
||||
@@ -459,10 +461,7 @@ export function runQueries(exploreId: ExploreId): ThunkResult<void> {
|
||||
// but we're using the datasource interval limit for now
|
||||
const interval = datasourceInstance.interval;
|
||||
|
||||
stopQueryState(queryState, 'New request issued');
|
||||
|
||||
queryState.sendFrames = true;
|
||||
queryState.sendLegacy = true;
|
||||
stopQueryState(querySubscription);
|
||||
|
||||
const queryOptions = {
|
||||
interval,
|
||||
@@ -470,32 +469,32 @@ export function runQueries(exploreId: ExploreId): ThunkResult<void> {
|
||||
// TODO: not sure if this makes sense for normal query when using both graph and table
|
||||
maxDataPoints: mode === ExploreMode.Logs ? 1000 : containerWidth,
|
||||
live,
|
||||
showingGraph,
|
||||
showingTable,
|
||||
};
|
||||
|
||||
const datasourceId = datasourceInstance.meta.id;
|
||||
const transaction = buildQueryTransaction(queries, queryOptions, range, queryIntervals, scanning);
|
||||
|
||||
queryState.onStreamingDataUpdated = () => {
|
||||
const response = queryState.validateStreamsAndGetPanelData();
|
||||
dispatch(queryStreamUpdatedAction({ exploreId, response }));
|
||||
};
|
||||
let firstResponse = true;
|
||||
|
||||
dispatch(queryStartAction({ exploreId }));
|
||||
|
||||
queryState
|
||||
.execute(datasourceInstance, transaction.request)
|
||||
.then((response: PanelData) => {
|
||||
if (!response.error) {
|
||||
const newQuerySub = runRequest(datasourceInstance, transaction.request)
|
||||
.pipe(map(preProcessPanelData()))
|
||||
.subscribe((data: PanelData) => {
|
||||
if (!data.error && firstResponse) {
|
||||
// Side-effect: Saving history in localstorage
|
||||
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||
dispatch(historyUpdatedAction({ exploreId, history: nextHistory }));
|
||||
dispatch(stateSave());
|
||||
}
|
||||
|
||||
dispatch(queryEndedAction({ exploreId, response }));
|
||||
dispatch(stateSave());
|
||||
firstResponse = false;
|
||||
|
||||
dispatch(queryStreamUpdatedAction({ exploreId, response: data }));
|
||||
|
||||
// Keep scanning for results if this was the last scanning transaction
|
||||
if (getState().explore[exploreId].scanning) {
|
||||
if (_.size(response.series) === 0) {
|
||||
if (data.state === LoadingState.Done && data.series.length === 0) {
|
||||
const range = getShiftedTimeRange(-1, getState().explore[exploreId].range);
|
||||
dispatch(updateTime({ exploreId, absoluteRange: range }));
|
||||
dispatch(runQueries(exploreId));
|
||||
@@ -504,15 +503,9 @@ export function runQueries(exploreId: ExploreId): ThunkResult<void> {
|
||||
dispatch(scanStopAction({ exploreId }));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(
|
||||
queryEndedAction({
|
||||
exploreId,
|
||||
response: { error, legacy: [], series: [], request: transaction.request, state: LoadingState.Error },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
dispatch(queryStoreSubscriptionAction({ exploreId, querySubscription: newQuerySub }));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
splitCloseAction,
|
||||
changeModeAction,
|
||||
scanStopAction,
|
||||
toggleGraphAction,
|
||||
toggleTableAction,
|
||||
} from './actionTypes';
|
||||
import { Reducer } from 'redux';
|
||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||
@@ -26,14 +28,12 @@ import { serializeStateToUrlParam } from 'app/core/utils/explore';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import { DataSourceApi, DataQuery } from '@grafana/ui';
|
||||
import { LogsModel, LogsDedupStrategy } from '@grafana/data';
|
||||
import { PanelQueryState } from '../../dashboard/state/PanelQueryState';
|
||||
|
||||
describe('Explore item reducer', () => {
|
||||
describe('scanning', () => {
|
||||
it('should start scanning', () => {
|
||||
const initalState = {
|
||||
...makeExploreItemState(),
|
||||
queryState: null as PanelQueryState,
|
||||
scanning: false,
|
||||
};
|
||||
|
||||
@@ -42,14 +42,12 @@ describe('Explore item reducer', () => {
|
||||
.whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left }))
|
||||
.thenStateShouldEqual({
|
||||
...makeExploreItemState(),
|
||||
queryState: null as PanelQueryState,
|
||||
scanning: true,
|
||||
});
|
||||
});
|
||||
it('should stop scanning', () => {
|
||||
const initalState = {
|
||||
...makeExploreItemState(),
|
||||
queryState: null as PanelQueryState,
|
||||
scanning: true,
|
||||
scanRange: {},
|
||||
};
|
||||
@@ -59,7 +57,6 @@ describe('Explore item reducer', () => {
|
||||
.whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
|
||||
.thenStateShouldEqual({
|
||||
...makeExploreItemState(),
|
||||
queryState: null as PanelQueryState,
|
||||
scanning: false,
|
||||
scanRange: undefined,
|
||||
});
|
||||
@@ -175,6 +172,30 @@ describe('Explore item reducer', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggling panels', () => {
|
||||
describe('when toggleGraphAction is dispatched', () => {
|
||||
it('then it should set correct state', () => {
|
||||
reducerTester()
|
||||
.givenReducer(itemReducer, { graphResult: [] })
|
||||
.whenActionIsDispatched(toggleGraphAction({ exploreId: ExploreId.left }))
|
||||
.thenStateShouldEqual({ showingGraph: true, graphResult: [] })
|
||||
.whenActionIsDispatched(toggleGraphAction({ exploreId: ExploreId.left }))
|
||||
.thenStateShouldEqual({ showingGraph: false, graphResult: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when toggleTableAction is dispatched', () => {
|
||||
it('then it should set correct state', () => {
|
||||
reducerTester()
|
||||
.givenReducer(itemReducer, { tableResult: {} })
|
||||
.whenActionIsDispatched(toggleTableAction({ exploreId: ExploreId.left }))
|
||||
.thenStateShouldEqual({ showingTable: true, tableResult: {} })
|
||||
.whenActionIsDispatched(toggleTableAction({ exploreId: ExploreId.left }))
|
||||
.thenStateShouldEqual({ showingTable: false, tableResult: new TableModel() });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export const setup = (urlStateOverrides?: any) => {
|
||||
|
||||
@@ -8,10 +8,9 @@ import {
|
||||
sortLogsResult,
|
||||
stopQueryState,
|
||||
refreshIntervalToSortOrder,
|
||||
instanceOfDataQueryError,
|
||||
} from 'app/core/utils/explore';
|
||||
import { ExploreItemState, ExploreState, ExploreId, ExploreUpdateState, ExploreMode } from 'app/types/explore';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
import { LoadingState, toLegacyResponseData } from '@grafana/data';
|
||||
import { DataQuery, DataSourceApi, PanelData } from '@grafana/ui';
|
||||
import {
|
||||
HigherOrderAction,
|
||||
@@ -29,9 +28,6 @@ import {
|
||||
queryStartAction,
|
||||
changeRangeAction,
|
||||
clearOriginAction,
|
||||
} from './actionTypes';
|
||||
|
||||
import {
|
||||
addQueryRowAction,
|
||||
changeQueryAction,
|
||||
changeSizeAction,
|
||||
@@ -53,17 +49,17 @@ import {
|
||||
toggleLogLevelAction,
|
||||
changeLoadingStateAction,
|
||||
resetExploreAction,
|
||||
queryEndedAction,
|
||||
queryStreamUpdatedAction,
|
||||
QueryEndedPayload,
|
||||
queryStoreSubscriptionAction,
|
||||
setPausedStateAction,
|
||||
toggleGraphAction,
|
||||
} from './actionTypes';
|
||||
import { reducerFactory, ActionOf } from 'app/core/redux';
|
||||
import { updateLocation } from 'app/core/actions/location';
|
||||
import { LocationUpdate } from '@grafana/runtime';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import { isLive } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
|
||||
import { PanelQueryState, toDataQueryError } from '../../dashboard/state/PanelQueryState';
|
||||
import { ResultProcessor } from '../utils/ResultProcessor';
|
||||
|
||||
export const DEFAULT_RANGE = {
|
||||
@@ -121,7 +117,6 @@ export const makeExploreItemState = (): ExploreItemState => ({
|
||||
isLive: false,
|
||||
isPaused: false,
|
||||
urlReplaced: false,
|
||||
queryState: new PanelQueryState(),
|
||||
queryResponse: createEmptyQueryResponse(),
|
||||
});
|
||||
|
||||
@@ -129,7 +124,6 @@ export const createEmptyQueryResponse = (): PanelData => ({
|
||||
state: LoadingState.NotStarted,
|
||||
request: null,
|
||||
series: [],
|
||||
legacy: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
@@ -203,8 +197,9 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
||||
const live = isLive(refreshInterval);
|
||||
const sortOrder = refreshIntervalToSortOrder(refreshInterval);
|
||||
const logsResult = sortLogsResult(state.logsResult, sortOrder);
|
||||
|
||||
if (isLive(state.refreshInterval) && !live) {
|
||||
stopQueryState(state.queryState, 'Live streaming stopped');
|
||||
stopQueryState(state.querySubscription);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -225,7 +220,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
||||
filter: clearQueriesAction,
|
||||
mapper: (state): ExploreItemState => {
|
||||
const queries = ensureQueries();
|
||||
stopQueryState(state.queryState, 'Queries cleared');
|
||||
stopQueryState(state.querySubscription);
|
||||
return {
|
||||
...state,
|
||||
queries: queries.slice(),
|
||||
@@ -284,7 +279,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
||||
|
||||
// Custom components
|
||||
const StartPage = datasourceInstance.components.ExploreStartPage;
|
||||
stopQueryState(state.queryState, 'Datasource changed');
|
||||
stopQueryState(state.querySubscription);
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -440,15 +435,26 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
||||
return { ...state, ...action.payload };
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: toggleGraphAction,
|
||||
mapper: (state): ExploreItemState => {
|
||||
const showingGraph = !state.showingGraph;
|
||||
if (showingGraph) {
|
||||
return { ...state, showingGraph };
|
||||
}
|
||||
|
||||
return { ...state, showingGraph, graphResult: null };
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: toggleTableAction,
|
||||
mapper: (state): ExploreItemState => {
|
||||
const showingTable = !state.showingTable;
|
||||
if (showingTable) {
|
||||
return { ...state };
|
||||
return { ...state, showingTable };
|
||||
}
|
||||
|
||||
return { ...state, tableResult: new TableModel() };
|
||||
return { ...state, showingTable, tableResult: new TableModel() };
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
@@ -566,10 +572,13 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
//queryStreamUpdatedAction
|
||||
filter: queryEndedAction,
|
||||
filter: queryStoreSubscriptionAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
return processQueryResponse(state, action);
|
||||
const { querySubscription } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
querySubscription,
|
||||
};
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
@@ -585,7 +594,7 @@ export const processQueryResponse = (
|
||||
action: ActionOf<QueryEndedPayload>
|
||||
): ExploreItemState => {
|
||||
const { response } = action.payload;
|
||||
const { request, state: loadingState, series, legacy, error } = response;
|
||||
const { request, state: loadingState, series, error } = response;
|
||||
|
||||
if (error) {
|
||||
if (error.cancelled) {
|
||||
@@ -595,12 +604,6 @@ export const processQueryResponse = (
|
||||
// For Angular editors
|
||||
state.eventBridge.emit('data-error', error);
|
||||
|
||||
console.error(error); // To help finding problems with query syntax
|
||||
|
||||
if (!instanceOfDataQueryError(error)) {
|
||||
response.error = toDataQueryError(error);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
@@ -613,19 +616,26 @@ export const processQueryResponse = (
|
||||
};
|
||||
}
|
||||
|
||||
const latency = request.endTime - request.startTime;
|
||||
const latency = request.endTime ? request.endTime - request.startTime : 0;
|
||||
const processor = new ResultProcessor(state, series);
|
||||
const graphResult = processor.getGraphResult() || state.graphResult; // don't replace results until we receive new results
|
||||
const tableResult = processor.getTableResult() || state.tableResult || new TableModel(); // don't replace results until we receive new results
|
||||
const logsResult = processor.getLogsResult();
|
||||
|
||||
// Send legacy data to Angular editors
|
||||
if (state.datasourceInstance.components.QueryCtrl) {
|
||||
const legacy = series.map(v => toLegacyResponseData(v));
|
||||
|
||||
// For Angular editors
|
||||
state.eventBridge.emit('data-received', legacy);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
latency,
|
||||
queryResponse: response,
|
||||
graphResult: processor.getGraphResult(),
|
||||
tableResult: processor.getTableResult(),
|
||||
logsResult: processor.getLogsResult(),
|
||||
graphResult,
|
||||
tableResult,
|
||||
logsResult,
|
||||
loading: loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming,
|
||||
showingStartPage: false,
|
||||
update: makeInitialUpdateState(),
|
||||
@@ -757,8 +767,8 @@ export const exploreReducer = (state = initialExploreState, action: HigherOrderA
|
||||
case resetExploreAction.type: {
|
||||
const leftState = state[ExploreId.left];
|
||||
const rightState = state[ExploreId.right];
|
||||
stopQueryState(leftState.queryState, 'Navigated away from Explore');
|
||||
stopQueryState(rightState.queryState, 'Navigated away from Explore');
|
||||
stopQueryState(leftState.querySubscription);
|
||||
stopQueryState(rightState.querySubscription);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -67,20 +67,20 @@ const testContext = (options: any = {}) => {
|
||||
describe('ResultProcessor', () => {
|
||||
describe('constructed without result', () => {
|
||||
describe('when calling getGraphResult', () => {
|
||||
it('then it should return an empty array', () => {
|
||||
it('then it should return null', () => {
|
||||
const { resultProcessor } = testContext({ dataFrames: [] });
|
||||
const theResult = resultProcessor.getGraphResult();
|
||||
|
||||
expect(theResult).toEqual([]);
|
||||
expect(theResult).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when calling getTableResult', () => {
|
||||
it('then it should return an empty TableModel', () => {
|
||||
it('then it should return null', () => {
|
||||
const { resultProcessor } = testContext({ dataFrames: [] });
|
||||
const theResult = resultProcessor.getTableResult();
|
||||
|
||||
expect(theResult).toEqual(new TableModel());
|
||||
expect(theResult).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,11 +11,15 @@ export class ResultProcessor {
|
||||
|
||||
getGraphResult(): GraphSeriesXY[] {
|
||||
if (this.state.mode !== ExploreMode.Metrics) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
|
||||
const onlyTimeSeries = this.dataFrames.filter(isTimeSeries);
|
||||
|
||||
if (onlyTimeSeries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getGraphSeriesModel(
|
||||
onlyTimeSeries,
|
||||
{},
|
||||
@@ -26,7 +30,7 @@ export class ResultProcessor {
|
||||
|
||||
getTableResult(): TableModel {
|
||||
if (this.state.mode !== ExploreMode.Metrics) {
|
||||
return new TableModel();
|
||||
return null;
|
||||
}
|
||||
|
||||
// For now ignore time series
|
||||
@@ -34,6 +38,10 @@ export class ResultProcessor {
|
||||
// Ignore time series only for prometheus
|
||||
const onlyTables = this.dataFrames.filter(frame => !isTimeSeries(frame));
|
||||
|
||||
if (onlyTables.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tables = onlyTables.map(frame => {
|
||||
const { fields } = frame;
|
||||
const fieldCount = fields.length;
|
||||
|
||||
@@ -6,12 +6,11 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl';
|
||||
import { getExploreUrl } from 'app/core/utils/explore';
|
||||
import { applyPanelTimeOverrides, getResolution } from 'app/features/dashboard/utils/panel';
|
||||
import { ContextSrv } from 'app/core/services/context_srv';
|
||||
import { toLegacyResponseData, isDataFrame, TimeRange, LoadingState, DataFrame, toDataFrameDTO } from '@grafana/data';
|
||||
import { toLegacyResponseData, TimeRange, LoadingState, DataFrame, toDataFrameDTO } from '@grafana/data';
|
||||
|
||||
import { LegacyResponseData, DataSourceApi, PanelData, DataQueryResponse } from '@grafana/ui';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { PanelQueryRunnerFormat } from '../dashboard/state/PanelQueryRunner';
|
||||
|
||||
class MetricsPanelCtrl extends PanelCtrl {
|
||||
scope: any;
|
||||
@@ -30,7 +29,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
skipDataOnInit: boolean;
|
||||
dataList: LegacyResponseData[];
|
||||
querySubscription?: Unsubscribable;
|
||||
dataFormat = PanelQueryRunnerFormat.legacy;
|
||||
useDataFrames = false;
|
||||
|
||||
constructor($scope: any, $injector: any) {
|
||||
super($scope, $injector);
|
||||
@@ -141,22 +140,12 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.dataFormat === PanelQueryRunnerFormat.legacy) {
|
||||
// The result should already be processed, but just in case
|
||||
if (!data.legacy) {
|
||||
data.legacy = data.series.map(v => {
|
||||
if (isDataFrame(v)) {
|
||||
return toLegacyResponseData(v);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
}
|
||||
|
||||
// Make the results look like they came directly from a <6.2 datasource request
|
||||
// NOTE: any object other than 'data' is no longer supported supported
|
||||
this.handleQueryResult({ data: data.legacy });
|
||||
} else {
|
||||
if (this.useDataFrames) {
|
||||
this.handleDataFrames(data.series);
|
||||
} else {
|
||||
// Make the results look as if they came directly from a <6.2 datasource request
|
||||
const legacy = data.series.map(v => toLegacyResponseData(v));
|
||||
this.handleQueryResult({ data: legacy });
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -197,7 +186,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
const queryRunner = panel.getQueryRunner();
|
||||
|
||||
if (!this.querySubscription) {
|
||||
this.querySubscription = queryRunner.subscribe(this.panelDataObserver, this.dataFormat);
|
||||
this.querySubscription = queryRunner.getData().subscribe(this.panelDataObserver);
|
||||
}
|
||||
|
||||
return queryRunner.run({
|
||||
|
||||
@@ -9,7 +9,7 @@ import config from 'app/core/config';
|
||||
import { css } from 'emotion';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { SHARED_DASHBODARD_QUERY } from './SharedQueryRunner';
|
||||
import { SHARED_DASHBODARD_QUERY } from './types';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { filterPanelDataToQuery } from 'app/features/dashboard/panel_editor/QueryEditorRow';
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import { DataSourceApi, DataQuery, PanelData } from '@grafana/ui';
|
||||
import { PanelQueryRunner, QueryRunnerOptions } from 'app/features/dashboard/state/PanelQueryRunner';
|
||||
import { toDataQueryError } from 'app/features/dashboard/state/PanelQueryState';
|
||||
import { DashboardQuery } from './types';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
|
||||
export const SHARED_DASHBODARD_QUERY = '-- Dashboard --';
|
||||
|
||||
export function isSharedDashboardQuery(datasource: string | DataSourceApi) {
|
||||
if (!datasource) {
|
||||
// default datasource
|
||||
return false;
|
||||
}
|
||||
if (datasource === SHARED_DASHBODARD_QUERY) {
|
||||
return true;
|
||||
}
|
||||
const ds = datasource as DataSourceApi;
|
||||
return ds.meta && ds.meta.name === SHARED_DASHBODARD_QUERY;
|
||||
}
|
||||
|
||||
export class SharedQueryRunner {
|
||||
private containerPanel: PanelModel;
|
||||
private listenToPanelId: number;
|
||||
private listenToPanel: PanelModel;
|
||||
private listenToRunner: PanelQueryRunner;
|
||||
private subscription: Unsubscribable;
|
||||
|
||||
constructor(private runner: PanelQueryRunner) {
|
||||
this.containerPanel = getDashboardSrv()
|
||||
.getCurrent()
|
||||
.getPanelById(runner.getPanelId());
|
||||
}
|
||||
|
||||
process(options: QueryRunnerOptions): Promise<PanelData> {
|
||||
const panelId = getPanelIdFromQuery(options.queries);
|
||||
|
||||
if (!panelId) {
|
||||
this.disconnect();
|
||||
return getQueryError('Missing panel reference ID');
|
||||
}
|
||||
|
||||
// The requested panel changed
|
||||
if (this.listenToPanelId !== panelId) {
|
||||
this.disconnect();
|
||||
|
||||
this.listenToPanel = getDashboardSrv()
|
||||
.getCurrent()
|
||||
.getPanelById(panelId);
|
||||
|
||||
if (!this.listenToPanel) {
|
||||
return getQueryError('Unknown Panel: ' + panelId);
|
||||
}
|
||||
|
||||
this.listenToPanelId = panelId;
|
||||
this.listenToRunner = this.listenToPanel.getQueryRunner();
|
||||
this.subscription = this.listenToRunner.chain(this.runner);
|
||||
this.runner.setState(this.listenToRunner.getState());
|
||||
console.log('Connecting panel: ', this.containerPanel.id, 'to:', this.listenToPanelId);
|
||||
}
|
||||
|
||||
// If the target has refreshed recently, use the exising data
|
||||
const data = this.listenToRunner.getCurrentData();
|
||||
if (data.request && data.request.startTime) {
|
||||
const elapsed = Date.now() - data.request.startTime;
|
||||
if (elapsed < 150) {
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
}
|
||||
|
||||
// When fullscreen run with the current panel settings
|
||||
if (this.containerPanel.fullscreen) {
|
||||
const { datasource, targets } = this.listenToPanel;
|
||||
const modified = {
|
||||
...options,
|
||||
panelId,
|
||||
datasource,
|
||||
queries: targets,
|
||||
};
|
||||
return this.listenToRunner.run(modified);
|
||||
} else {
|
||||
this.listenToPanel.refresh();
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
this.subscription = null;
|
||||
}
|
||||
if (this.listenToPanel) {
|
||||
this.listenToPanel = null;
|
||||
}
|
||||
this.listenToPanelId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getPanelIdFromQuery(queries: DataQuery[]): number | undefined {
|
||||
if (!queries || !queries.length) {
|
||||
return undefined;
|
||||
}
|
||||
return (queries[0] as DashboardQuery).panelId;
|
||||
}
|
||||
|
||||
function getQueryError(msg: string): Promise<PanelData> {
|
||||
return Promise.resolve({
|
||||
state: LoadingState.Error,
|
||||
series: [],
|
||||
legacy: [],
|
||||
error: toDataQueryError(msg),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { isSharedDashboardQuery, runSharedRequest } from './runSharedRequest';
|
||||
export { DashboardQueryEditor } from './DashboardQueryEditor';
|
||||
export { SHARED_DASHBODARD_QUERY } from './types';
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { isSharedDashboardQuery } from './SharedQueryRunner';
|
||||
import { isSharedDashboardQuery } from './runSharedRequest';
|
||||
import { DataSourceApi } from '@grafana/ui';
|
||||
|
||||
describe('SharedQueryRunner', () => {
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { DataQuery, PanelData, DataSourceApi } from '@grafana/ui';
|
||||
import { QueryRunnerOptions } from 'app/features/dashboard/state/PanelQueryRunner';
|
||||
import { DashboardQuery } from './types';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
import { SHARED_DASHBODARD_QUERY } from './types';
|
||||
|
||||
export function isSharedDashboardQuery(datasource: string | DataSourceApi) {
|
||||
if (!datasource) {
|
||||
// default datasource
|
||||
return false;
|
||||
}
|
||||
if (datasource === SHARED_DASHBODARD_QUERY) {
|
||||
return true;
|
||||
}
|
||||
const ds = datasource as DataSourceApi;
|
||||
return ds.meta && ds.meta.name === SHARED_DASHBODARD_QUERY;
|
||||
}
|
||||
|
||||
export function runSharedRequest(options: QueryRunnerOptions): Observable<PanelData> {
|
||||
return new Observable<PanelData>(subscriber => {
|
||||
const dashboard = getDashboardSrv().getCurrent();
|
||||
const listenToPanelId = getPanelIdFromQuery(options.queries);
|
||||
|
||||
if (!listenToPanelId) {
|
||||
subscriber.next(getQueryError('Missing panel reference ID'));
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentPanel = dashboard.getPanelById(options.panelId);
|
||||
const listenToPanel = dashboard.getPanelById(listenToPanelId);
|
||||
|
||||
if (!listenToPanel) {
|
||||
subscriber.next(getQueryError('Unknown Panel: ' + listenToPanelId));
|
||||
return null;
|
||||
}
|
||||
|
||||
const listenToRunner = listenToPanel.getQueryRunner();
|
||||
const subscription = listenToRunner.getData(false).subscribe({
|
||||
next: (data: PanelData) => {
|
||||
console.log('got data from other panel', data);
|
||||
subscriber.next(data);
|
||||
},
|
||||
});
|
||||
|
||||
// If we are in fullscreen the other panel will not execute any queries
|
||||
// So we have to trigger it from here
|
||||
if (currentPanel.fullscreen) {
|
||||
const { datasource, targets } = listenToPanel;
|
||||
const modified = {
|
||||
...options,
|
||||
datasource,
|
||||
panelId: listenToPanelId,
|
||||
queries: targets,
|
||||
};
|
||||
listenToRunner.run(modified);
|
||||
}
|
||||
|
||||
return () => {
|
||||
console.log('runSharedRequest unsubscribe');
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getPanelIdFromQuery(queries: DataQuery[]): number | undefined {
|
||||
if (!queries || !queries.length) {
|
||||
return undefined;
|
||||
}
|
||||
return (queries[0] as DashboardQuery).panelId;
|
||||
}
|
||||
|
||||
function getQueryError(msg: string): PanelData {
|
||||
return {
|
||||
state: LoadingState.Error,
|
||||
series: [],
|
||||
error: { message: msg },
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
|
||||
export const SHARED_DASHBODARD_QUERY = '-- Dashboard --';
|
||||
|
||||
export interface DashboardQuery extends DataQuery {
|
||||
panelId?: number;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('LokiDatasource', () => {
|
||||
targets: [{ expr: '{} foo', refId: 'B' }],
|
||||
});
|
||||
|
||||
const res = await ds.query(options);
|
||||
const res = await ds.query(options).toPromise();
|
||||
|
||||
const dataFrame = res.data[0] as DataFrame;
|
||||
expect(dataFrame.fields[1].values.get(0)).toBe('hello');
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
dateMath,
|
||||
DataFrame,
|
||||
LogRowModel,
|
||||
LoadingState,
|
||||
DateTime,
|
||||
AnnotationEvent,
|
||||
DataFrameView,
|
||||
LoadingState,
|
||||
} from '@grafana/data';
|
||||
import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
|
||||
import LanguageProvider from './language_provider';
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
DataSourceInstanceSettings,
|
||||
DataQueryError,
|
||||
DataQueryRequest,
|
||||
DataStreamObserver,
|
||||
DataQueryResponse,
|
||||
AnnotationQueryRequest,
|
||||
} from '@grafana/ui';
|
||||
@@ -31,6 +30,8 @@ import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { safeStringifyValue, convertToWebSocketUrl } from 'app/core/utils/explore';
|
||||
import { LiveTarget, LiveStreams } from './live_streams';
|
||||
import { Observable, from, merge } from 'rxjs';
|
||||
import { map, filter } from 'rxjs/operators';
|
||||
|
||||
export const DEFAULT_MAX_LINES = 1000;
|
||||
|
||||
@@ -167,79 +168,51 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
||||
return series;
|
||||
};
|
||||
|
||||
runLiveQueries = (options: DataQueryRequest<LokiQuery>, observer?: DataStreamObserver) => {
|
||||
const liveTargets = options.targets
|
||||
.filter(target => target.expr && !target.hide && target.live)
|
||||
.map(target => this.prepareLiveTarget(target, options));
|
||||
|
||||
for (const liveTarget of liveTargets) {
|
||||
// Reuse an existing stream if one is already running
|
||||
runLiveQuery = (options: DataQueryRequest<LokiQuery>, target: LokiQuery): Observable<DataQueryResponse> => {
|
||||
const liveTarget = this.prepareLiveTarget(target, options);
|
||||
const stream = this.streams.getStream(liveTarget);
|
||||
const subscription = stream.subscribe({
|
||||
next: (data: DataFrame[]) => {
|
||||
observer({
|
||||
key: `loki-${liveTarget.refId}`,
|
||||
request: options,
|
||||
state: LoadingState.Streaming,
|
||||
return stream.pipe(
|
||||
map(data => {
|
||||
return {
|
||||
data,
|
||||
unsubscribe: () => {
|
||||
subscription.unsubscribe();
|
||||
},
|
||||
});
|
||||
},
|
||||
error: (err: any) => {
|
||||
observer({
|
||||
key: `loki-${liveTarget.refId}`,
|
||||
request: options,
|
||||
state: LoadingState.Error,
|
||||
error: this.processError(err, liveTarget),
|
||||
unsubscribe: () => {
|
||||
subscription.unsubscribe();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
state: LoadingState.Streaming,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
runQueries = async (options: DataQueryRequest<LokiQuery>): Promise<{ data: DataFrame[] }> => {
|
||||
const queryTargets = options.targets
|
||||
.filter(target => target.expr && !target.hide && !target.live)
|
||||
.map(target => this.prepareQueryTarget(target, options));
|
||||
|
||||
if (queryTargets.length === 0) {
|
||||
return Promise.resolve({ data: [] });
|
||||
}
|
||||
|
||||
const queries = queryTargets.map(target =>
|
||||
this._request('/api/prom/query', target).catch((err: any) => {
|
||||
runQuery = (options: DataQueryRequest<LokiQuery>, target: LokiQuery): Observable<DataQueryResponse> => {
|
||||
const query = this.prepareQueryTarget(target, options);
|
||||
return from(
|
||||
this._request('/api/prom/query', query).catch((err: any) => {
|
||||
if (err.cancelled) {
|
||||
return err;
|
||||
}
|
||||
|
||||
const error: DataQueryError = this.processError(err, target);
|
||||
const error: DataQueryError = this.processError(err, query);
|
||||
throw error;
|
||||
})
|
||||
).pipe(
|
||||
filter((response: any) => (response.cancelled ? false : true)),
|
||||
map((response: any) => {
|
||||
const data = this.processResult(response.data, query);
|
||||
return { data, key: query.refId };
|
||||
})
|
||||
);
|
||||
|
||||
return Promise.all(queries).then((results: any[]) => {
|
||||
let series: DataFrame[] = [];
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
if (result.data) {
|
||||
series = series.concat(this.processResult(result.data, queryTargets[i]));
|
||||
}
|
||||
}
|
||||
|
||||
return { data: series };
|
||||
});
|
||||
};
|
||||
|
||||
async query(options: DataQueryRequest<LokiQuery>, observer?: DataStreamObserver) {
|
||||
this.runLiveQueries(options, observer);
|
||||
query(options: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> {
|
||||
const subQueries = options.targets
|
||||
.filter(target => target.expr && !target.hide)
|
||||
.map(target => {
|
||||
if (target.live) {
|
||||
return this.runLiveQuery(options, target);
|
||||
}
|
||||
return this.runQuery(options, target);
|
||||
});
|
||||
|
||||
return this.runQueries(options);
|
||||
return merge(...subQueries);
|
||||
}
|
||||
|
||||
async importQueries(queries: LokiQuery[], originMeta: PluginMeta): Promise<LokiQuery[]> {
|
||||
@@ -383,12 +356,14 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
||||
return [];
|
||||
}
|
||||
|
||||
const query = queryRequestFromAnnotationOptions(options);
|
||||
const { data } = await this.runQueries(query);
|
||||
const request = queryRequestFromAnnotationOptions(options);
|
||||
const { data } = await this.runQuery(request, request.targets[0]).toPromise();
|
||||
const annotations: AnnotationEvent[] = [];
|
||||
|
||||
for (const frame of data) {
|
||||
const tags = Object.values(frame.labels);
|
||||
const tags = Object.values(frame.labels) as string[];
|
||||
const view = new DataFrameView<{ ts: string; line: string }>(frame);
|
||||
|
||||
view.forEachRow(row => {
|
||||
annotations.push({
|
||||
time: new Date(row.ts).valueOf(),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
||||
import { DataSourceInstanceSettings } from '@grafana/ui';
|
||||
import { MixedDatasource } from './module';
|
||||
import { from } from 'rxjs';
|
||||
|
||||
const defaultDS = new MockDataSourceApi('DefaultDS', { data: ['DDD'] });
|
||||
const datasourceSrv = new DatasourceSrvMock(defaultDS, {
|
||||
@@ -26,10 +27,19 @@ describe('MixedDatasource', () => {
|
||||
{ refId: 'QC', datasource: 'C' }, // 3
|
||||
],
|
||||
});
|
||||
const results: any[] = [];
|
||||
|
||||
beforeEach(async () => {
|
||||
const ds = await getDataSourceSrv().get('-- Mixed --');
|
||||
from(ds.query(requestMixed)).subscribe(result => {
|
||||
results.push(result);
|
||||
});
|
||||
});
|
||||
|
||||
it('direct query should return results', async () => {
|
||||
const ds = await getDataSourceSrv().get('-- Mixed --');
|
||||
const res = await ds.query(requestMixed);
|
||||
expect(res.data).toEqual(['AAAA', 'BBBB', 'CCCC']);
|
||||
expect(results.length).toBe(3);
|
||||
expect(results[0].data).toEqual(['AAAA']);
|
||||
expect(results[1].data).toEqual(['BBBB']);
|
||||
expect(results[2].data).toEqual(['CCCC']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import map from 'lodash/map';
|
||||
import flatten from 'lodash/flatten';
|
||||
import filter from 'lodash/filter';
|
||||
|
||||
import {
|
||||
DataSourceApi,
|
||||
DataQuery,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataStreamObserver,
|
||||
DataSourceInstanceSettings,
|
||||
} from '@grafana/ui';
|
||||
import { from, of, Observable, merge } from 'rxjs';
|
||||
|
||||
import { DataSourceApi, DataQuery, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings } from '@grafana/ui';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { mergeMap, map, filter } from 'rxjs/operators';
|
||||
|
||||
export const MIXED_DATASOURCE_NAME = '-- Mixed --';
|
||||
|
||||
@@ -22,43 +13,68 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
|
||||
super(instanceSettings);
|
||||
}
|
||||
|
||||
async query(request: DataQueryRequest<DataQuery>, observer: DataStreamObserver): Promise<DataQueryResponse> {
|
||||
query(request: DataQueryRequest<DataQuery>): Observable<DataQueryResponse> {
|
||||
// Remove any invalid queries
|
||||
const queries = request.targets.filter(t => {
|
||||
return t.datasource !== MIXED_DATASOURCE_NAME;
|
||||
});
|
||||
|
||||
if (!queries.length) {
|
||||
return Promise.resolve({ data: [] }); // nothing
|
||||
return of({ data: [] } as DataQueryResponse); // nothing
|
||||
}
|
||||
|
||||
const sets = groupBy(queries, 'datasource');
|
||||
const sets: { [key: string]: DataQuery[] } = groupBy(queries, 'datasource');
|
||||
const observables: Array<Observable<DataQueryResponse>> = [];
|
||||
|
||||
const promises = map(sets, (targets: DataQuery[]) => {
|
||||
for (const key in sets) {
|
||||
const targets = sets[key];
|
||||
const dsName = targets[0].datasource;
|
||||
return getDataSourceSrv()
|
||||
.get(dsName)
|
||||
.then((ds: DataSourceApi) => {
|
||||
const opt = cloneDeep(request);
|
||||
|
||||
const observable = from(getDataSourceSrv().get(dsName)).pipe(
|
||||
map((dataSourceApi: DataSourceApi) => {
|
||||
const datasourceRequest = cloneDeep(request);
|
||||
|
||||
// Remove any unused hidden queries
|
||||
if (!ds.meta.hiddenQueries) {
|
||||
targets = filter(targets, (t: DataQuery) => {
|
||||
return !t.hide;
|
||||
});
|
||||
if (targets.length === 0) {
|
||||
return { data: [] };
|
||||
}
|
||||
let newTargets = targets.slice();
|
||||
if (!dataSourceApi.meta.hiddenQueries) {
|
||||
newTargets = newTargets.filter((t: DataQuery) => !t.hide);
|
||||
}
|
||||
|
||||
opt.targets = targets;
|
||||
return ds.query(opt);
|
||||
});
|
||||
});
|
||||
datasourceRequest.targets = newTargets;
|
||||
datasourceRequest.requestId = `${dsName}${datasourceRequest.requestId || ''}`;
|
||||
return {
|
||||
dataSourceApi,
|
||||
datasourceRequest,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return Promise.all(promises).then(results => {
|
||||
return { data: flatten(map(results, 'data')) };
|
||||
});
|
||||
const noTargets = observable.pipe(
|
||||
filter(({ datasourceRequest }) => datasourceRequest.targets.length === 0),
|
||||
mergeMap(() => {
|
||||
return of({ data: [] } as DataQueryResponse);
|
||||
})
|
||||
);
|
||||
|
||||
const hasTargets = observable.pipe(
|
||||
filter(({ datasourceRequest }) => datasourceRequest.targets.length > 0),
|
||||
mergeMap(({ dataSourceApi, datasourceRequest }) => {
|
||||
return from(dataSourceApi.query(datasourceRequest)).pipe(
|
||||
map((response: DataQueryResponse) => {
|
||||
return {
|
||||
...response,
|
||||
data: response.data || [],
|
||||
key: `${dsName}${response.key || ''}`,
|
||||
} as DataQueryResponse;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
observables.push(merge(noTargets, hasTargets));
|
||||
}
|
||||
|
||||
return merge(...observables);
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
|
||||
@@ -293,6 +293,7 @@ export class PostgresQueryCtrl extends QueryCtrl {
|
||||
onDataReceived(dataList: any) {
|
||||
this.lastQueryMeta = null;
|
||||
this.lastQueryError = null;
|
||||
console.log('postgres query data received', dataList);
|
||||
|
||||
const anySeriesFromQuery: any = _.find(dataList, { refId: this.target.refId });
|
||||
if (anySeriesFromQuery) {
|
||||
|
||||
@@ -3,9 +3,9 @@ import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
// Services & Utils
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { dateMath, TimeRange, DateTime, AnnotationEvent, LoadingState } from '@grafana/data';
|
||||
import { Observable, from, of } from 'rxjs';
|
||||
import { single, filter, mergeMap, catchError } from 'rxjs/operators';
|
||||
import { dateMath, TimeRange, DateTime, AnnotationEvent } from '@grafana/data';
|
||||
import { Observable, from, of, merge } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
|
||||
import PrometheusMetricFindQuery from './metric_find_query';
|
||||
import { ResultTransformer } from './result_transformer';
|
||||
@@ -21,14 +21,14 @@ import {
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataQueryError,
|
||||
DataStreamObserver,
|
||||
DataQueryResponseData,
|
||||
DataStreamState,
|
||||
DataQueryResponse,
|
||||
} from '@grafana/ui';
|
||||
import { safeStringifyValue } from 'app/core/utils/explore';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { ExploreUrlState } from 'app/types';
|
||||
import { LoadingState } from '@grafana/data/src/types/data';
|
||||
|
||||
export interface PromDataQueryResponse {
|
||||
data: {
|
||||
@@ -174,61 +174,6 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
||||
return series;
|
||||
};
|
||||
|
||||
runObserverQueries = (
|
||||
options: DataQueryRequest<PromQuery>,
|
||||
observer: DataStreamObserver,
|
||||
queries: PromQueryRequest[],
|
||||
activeTargets: PromQuery[],
|
||||
end: number
|
||||
) => {
|
||||
for (let index = 0; index < queries.length; index++) {
|
||||
const query = queries[index];
|
||||
const target = activeTargets[index];
|
||||
let observable: Observable<any> = null;
|
||||
|
||||
if (query.instant) {
|
||||
observable = from(this.performInstantQuery(query, end));
|
||||
} else {
|
||||
observable = from(this.performTimeSeriesQuery(query, query.start, query.end));
|
||||
}
|
||||
|
||||
observable
|
||||
.pipe(
|
||||
single(), // unsubscribes automatically after first result
|
||||
filter((response: any) => (response.cancelled ? false : true)),
|
||||
mergeMap((response: any) => {
|
||||
const data = this.processResult(response, query, target, queries.length);
|
||||
const state: DataStreamState = {
|
||||
key: `prometheus-${target.refId}`,
|
||||
state: LoadingState.Done,
|
||||
request: options,
|
||||
// TODO this is obviously wrong as data is not a DataFrame and needs to be dealt with later on
|
||||
// in PanelQueryState
|
||||
data: data as any,
|
||||
unsubscribe: () => undefined,
|
||||
};
|
||||
|
||||
return [state];
|
||||
}),
|
||||
catchError(err => {
|
||||
const error = this.handleErrors(err, target);
|
||||
const state: DataStreamState = {
|
||||
key: `prometheus-${target.refId}`,
|
||||
request: options,
|
||||
state: LoadingState.Error,
|
||||
error,
|
||||
unsubscribe: () => undefined,
|
||||
};
|
||||
|
||||
return of(state);
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: state => observer(state),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
prepareTargets = (options: DataQueryRequest<PromQuery>, start: number, end: number) => {
|
||||
const queries: PromQueryRequest[] = [];
|
||||
const activeTargets: PromQuery[] = [];
|
||||
@@ -238,23 +183,36 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.context === PromContext.Explore) {
|
||||
target.format = 'time_series';
|
||||
target.instant = false;
|
||||
target.requestId = options.panelId + target.refId;
|
||||
|
||||
if (target.context !== PromContext.Explore) {
|
||||
activeTargets.push(target);
|
||||
queries.push(this.createQuery(target, options, start, end));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.showingTable) {
|
||||
// create instant target only if Table is showed in Explore
|
||||
const instantTarget: any = _.cloneDeep(target);
|
||||
instantTarget.format = 'table';
|
||||
instantTarget.instant = true;
|
||||
instantTarget.valueWithRefId = true;
|
||||
delete instantTarget.maxDataPoints;
|
||||
instantTarget.requestId += '_instant';
|
||||
instantTarget.refId += '_instant';
|
||||
|
||||
activeTargets.push(instantTarget);
|
||||
queries.push(this.createQuery(instantTarget, options, start, end));
|
||||
}
|
||||
|
||||
if (target.showingGraph) {
|
||||
// create time series target only if Graph is showed in Explore
|
||||
target.format = 'time_series';
|
||||
target.instant = false;
|
||||
|
||||
activeTargets.push(target);
|
||||
queries.push(this.createQuery(target, options, start, end));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
queries,
|
||||
@@ -262,54 +220,44 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
||||
};
|
||||
};
|
||||
|
||||
query(options: DataQueryRequest<PromQuery>, observer?: DataStreamObserver): Promise<{ data: any }> {
|
||||
query(options: DataQueryRequest<PromQuery>): Observable<DataQueryResponse> {
|
||||
const start = this.getPrometheusTime(options.range.from, false);
|
||||
const end = this.getPrometheusTime(options.range.to, true);
|
||||
|
||||
options = _.clone(options);
|
||||
const { queries, activeTargets } = this.prepareTargets(options, start, end);
|
||||
|
||||
// No valid targets, return the empty result to save a round trip.
|
||||
if (_.isEmpty(queries)) {
|
||||
return this.$q.when({ data: [] }) as Promise<{ data: any }>;
|
||||
}
|
||||
|
||||
if (
|
||||
observer &&
|
||||
options.targets.filter(target => target.context === PromContext.Explore).length === options.targets.length
|
||||
) {
|
||||
// using observer to make the instant query return immediately
|
||||
this.runObserverQueries(options, observer, queries, activeTargets, end);
|
||||
return this.$q.when({ data: [] }) as Promise<{ data: any }>;
|
||||
}
|
||||
|
||||
const allQueryPromise = _.map(queries, query => {
|
||||
if (query.instant) {
|
||||
return this.performInstantQuery(query, end);
|
||||
} else {
|
||||
return this.performTimeSeriesQuery(query, query.start, query.end);
|
||||
}
|
||||
});
|
||||
|
||||
const allPromise = this.$q.all(allQueryPromise).then((responseList: any) => {
|
||||
let result: any[] = [];
|
||||
|
||||
_.each(responseList, (response, index: number) => {
|
||||
if (response.cancelled) {
|
||||
return;
|
||||
return of({ data: [] });
|
||||
}
|
||||
|
||||
const allInstant = queries.filter(query => query.instant).length === queries.length;
|
||||
const allTimeSeries = queries.filter(query => !query.instant).length === queries.length;
|
||||
const subQueries = queries.map((query, index) => {
|
||||
const target = activeTargets[index];
|
||||
const query = queries[index];
|
||||
const series = this.processResult(response, query, target, queries.length);
|
||||
let observable: Observable<any> = null;
|
||||
const state: LoadingState =
|
||||
allInstant || allTimeSeries ? LoadingState.Done : query.instant ? LoadingState.Loading : LoadingState.Done;
|
||||
|
||||
result = [...result, ...series];
|
||||
if (query.instant) {
|
||||
observable = from(this.performInstantQuery(query, end));
|
||||
} else {
|
||||
observable = from(this.performTimeSeriesQuery(query, query.start, query.end));
|
||||
}
|
||||
|
||||
return observable.pipe(
|
||||
filter((response: any) => (response.cancelled ? false : true)),
|
||||
map((response: any) => {
|
||||
const data = this.processResult(response, query, target, queries.length);
|
||||
return {
|
||||
data,
|
||||
key: query.requestId,
|
||||
state,
|
||||
} as DataQueryResponse;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return { data: result };
|
||||
});
|
||||
|
||||
return allPromise as Promise<{ data: any }>;
|
||||
return merge(...subQueries);
|
||||
}
|
||||
|
||||
createQuery(target: PromQuery, options: DataQueryRequest<PromQuery>, start: number, end: number) {
|
||||
@@ -318,8 +266,8 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
||||
instant: target.instant,
|
||||
step: 0,
|
||||
expr: '',
|
||||
requestId: '',
|
||||
refId: '',
|
||||
requestId: target.requestId,
|
||||
refId: target.refId,
|
||||
start: 0,
|
||||
end: 0,
|
||||
};
|
||||
@@ -361,8 +309,6 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
||||
|
||||
// Only replace vars in expression after having (possibly) updated interval vars
|
||||
query.expr = this.templateSrv.replace(expr, scopedVars, this.interpolateQueryExpr);
|
||||
query.requestId = options.panelId + target.refId;
|
||||
query.refId = target.refId;
|
||||
|
||||
// Align query interval with step to allow query caching and to ensure
|
||||
// that about-same-time query results look the same.
|
||||
@@ -520,9 +466,17 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
||||
...options,
|
||||
interval: step,
|
||||
};
|
||||
|
||||
// Unsetting min interval for accurate event resolution
|
||||
const minStep = '1s';
|
||||
const query = this.createQuery({ expr, interval: minStep, refId: 'X' }, queryOptions, start, end);
|
||||
const queryModel = {
|
||||
expr,
|
||||
interval: minStep,
|
||||
refId: 'X',
|
||||
requestId: `prom-query-${annotation.name}`,
|
||||
};
|
||||
|
||||
const query = this.createQuery(queryModel, queryOptions, start, end);
|
||||
|
||||
const self = this;
|
||||
return this.performTimeSeriesQuery(query, query.start, query.end).then((results: PromDataQueryResponse) => {
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
prometheusSpecialRegexEscape,
|
||||
} from '../datasource';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { DataSourceInstanceSettings, DataQueryResponseData } from '@grafana/ui';
|
||||
import { PromOptions } from '../types';
|
||||
import { DataSourceInstanceSettings, DataQueryResponseData, DataQueryRequest } from '@grafana/ui';
|
||||
import { PromOptions, PromQuery, PromContext } from '../types';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { CustomVariable } from 'app/features/templating/custom_variable';
|
||||
@@ -179,8 +179,8 @@ describe('PrometheusDatasource', () => {
|
||||
},
|
||||
];
|
||||
|
||||
ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock);
|
||||
return ctx.ds.query(ctx.query).then((result: any) => {
|
||||
ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue([responseMock]);
|
||||
ctx.ds.query(ctx.query).subscribe((result: any) => {
|
||||
const results = result.data;
|
||||
return expect(results).toMatchObject(expected);
|
||||
});
|
||||
@@ -209,8 +209,8 @@ describe('PrometheusDatasource', () => {
|
||||
|
||||
const expected = ['1', '2', '4', '+Inf'];
|
||||
|
||||
ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock);
|
||||
return ctx.ds.query(ctx.query).then((result: any) => {
|
||||
ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue([responseMock]);
|
||||
ctx.ds.query(ctx.query).subscribe((result: any) => {
|
||||
const seriesLabels = _.map(result.data, 'target');
|
||||
return expect(seriesLabels).toEqual(expected);
|
||||
});
|
||||
@@ -469,7 +469,7 @@ describe('PrometheusDatasource', () => {
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
|
||||
await ctx.ds.query(query).then((data: any) => {
|
||||
ctx.ds.query(query).subscribe((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
@@ -503,20 +503,16 @@ describe('PrometheusDatasource', () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
it('should generate an error', () => {
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.reject(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
|
||||
await ctx.ds.query(query).catch((e: any) => {
|
||||
ctx.ds.query(query).subscribe((e: any) => {
|
||||
results = e.message;
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate an error', () => {
|
||||
expect(results).toBe(`"${errMessage}"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When querying prometheus with one target which returns multiple series', () => {
|
||||
let results: any;
|
||||
@@ -553,7 +549,7 @@ describe('PrometheusDatasource', () => {
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
|
||||
await ctx.ds.query(query).then((data: any) => {
|
||||
ctx.ds.query(query).subscribe((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
@@ -614,8 +610,7 @@ describe('PrometheusDatasource', () => {
|
||||
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
|
||||
await ctx.ds.query(query).then((data: any) => {
|
||||
ctx.ds.query(query).subscribe((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
@@ -816,7 +811,7 @@ describe('PrometheusDatasource', () => {
|
||||
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query).then((data: any) => {
|
||||
ctx.ds.query(query).subscribe((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
@@ -853,7 +848,7 @@ describe('PrometheusDatasource', () => {
|
||||
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -869,7 +864,7 @@ describe('PrometheusDatasource', () => {
|
||||
const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1';
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -890,7 +885,7 @@ describe('PrometheusDatasource', () => {
|
||||
const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -907,7 +902,7 @@ describe('PrometheusDatasource', () => {
|
||||
const urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2';
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -929,7 +924,7 @@ describe('PrometheusDatasource', () => {
|
||||
const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=400&step=50';
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -950,7 +945,7 @@ describe('PrometheusDatasource', () => {
|
||||
const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15';
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -972,7 +967,7 @@ describe('PrometheusDatasource', () => {
|
||||
const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=400&step=100';
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -994,7 +989,7 @@ describe('PrometheusDatasource', () => {
|
||||
const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100';
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -1016,7 +1011,7 @@ describe('PrometheusDatasource', () => {
|
||||
const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60';
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -1059,7 +1054,7 @@ describe('PrometheusDatasource', () => {
|
||||
templateSrv.replace = jest.fn(str => str);
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -1099,7 +1094,7 @@ describe('PrometheusDatasource', () => {
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
templateSrv.replace = jest.fn(str => str);
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -1140,7 +1135,7 @@ describe('PrometheusDatasource', () => {
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
templateSrv.replace = jest.fn(str => str);
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -1187,7 +1182,7 @@ describe('PrometheusDatasource', () => {
|
||||
templateSrv.replace = jest.fn(str => str);
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -1228,7 +1223,7 @@ describe('PrometheusDatasource', () => {
|
||||
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -1274,7 +1269,7 @@ describe('PrometheusDatasource', () => {
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
templateSrv.replace = jest.fn(str => str);
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.method).toBe('GET');
|
||||
expect(res.url).toBe(urlExpected);
|
||||
@@ -1326,7 +1321,7 @@ describe('PrometheusDatasource', () => {
|
||||
templateSrv.replace = jest.fn(str => str);
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query);
|
||||
ctx.ds.query(query);
|
||||
const res = backendSrv.datasourceRequest.mock.calls[0][0];
|
||||
expect(res.url).toBe(urlExpected);
|
||||
|
||||
@@ -1391,7 +1386,7 @@ describe('PrometheusDatasource for POST', () => {
|
||||
};
|
||||
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
|
||||
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
await ctx.ds.query(query).then((data: any) => {
|
||||
ctx.ds.query(query).subscribe((data: any) => {
|
||||
results = data;
|
||||
});
|
||||
});
|
||||
@@ -1432,3 +1427,211 @@ describe('PrometheusDatasource for POST', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const getPrepareTargetsContext = (target: PromQuery) => {
|
||||
const instanceSettings = ({
|
||||
url: 'proxied',
|
||||
directUrl: 'direct',
|
||||
user: 'test',
|
||||
password: 'mupp',
|
||||
jsonData: { httpMethod: 'POST' },
|
||||
} as unknown) as DataSourceInstanceSettings<PromOptions>;
|
||||
const start = 0;
|
||||
const end = 1;
|
||||
const panelId = '2';
|
||||
const options = ({ targets: [target], interval: '1s', panelId } as any) as DataQueryRequest<PromQuery>;
|
||||
|
||||
const ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
|
||||
const { queries, activeTargets } = ds.prepareTargets(options, start, end);
|
||||
|
||||
return {
|
||||
queries,
|
||||
activeTargets,
|
||||
start,
|
||||
end,
|
||||
panelId,
|
||||
};
|
||||
};
|
||||
|
||||
describe('prepareTargets', () => {
|
||||
describe('when run from a Panel', () => {
|
||||
it('then it should just add targets', () => {
|
||||
const target: PromQuery = {
|
||||
refId: 'A',
|
||||
expr: 'up',
|
||||
context: PromContext.Panel,
|
||||
};
|
||||
|
||||
const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target);
|
||||
|
||||
expect(queries.length).toBe(1);
|
||||
expect(activeTargets.length).toBe(1);
|
||||
expect(queries[0]).toEqual({
|
||||
end,
|
||||
expr: 'up',
|
||||
headers: {
|
||||
'X-Dashboard-Id': undefined,
|
||||
'X-Panel-Id': panelId,
|
||||
},
|
||||
hinting: undefined,
|
||||
instant: undefined,
|
||||
refId: target.refId,
|
||||
requestId: panelId + target.refId,
|
||||
start,
|
||||
step: 1,
|
||||
});
|
||||
expect(activeTargets[0]).toEqual(target);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when run from Explore', () => {
|
||||
describe('and both Graph and Table are shown', () => {
|
||||
it('then it should return both instant and time series related objects', () => {
|
||||
const target: PromQuery = {
|
||||
refId: 'A',
|
||||
expr: 'up',
|
||||
context: PromContext.Explore,
|
||||
showingGraph: true,
|
||||
showingTable: true,
|
||||
};
|
||||
|
||||
const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target);
|
||||
|
||||
expect(queries.length).toBe(2);
|
||||
expect(activeTargets.length).toBe(2);
|
||||
expect(queries[0]).toEqual({
|
||||
end,
|
||||
expr: 'up',
|
||||
headers: {
|
||||
'X-Dashboard-Id': undefined,
|
||||
'X-Panel-Id': panelId,
|
||||
},
|
||||
hinting: undefined,
|
||||
instant: true,
|
||||
refId: target.refId,
|
||||
requestId: panelId + target.refId + '_instant',
|
||||
start,
|
||||
step: 1,
|
||||
});
|
||||
expect(activeTargets[0]).toEqual({
|
||||
...target,
|
||||
format: 'table',
|
||||
instant: true,
|
||||
requestId: panelId + target.refId + '_instant',
|
||||
valueWithRefId: true,
|
||||
});
|
||||
expect(queries[1]).toEqual({
|
||||
end,
|
||||
expr: 'up',
|
||||
headers: {
|
||||
'X-Dashboard-Id': undefined,
|
||||
'X-Panel-Id': panelId,
|
||||
},
|
||||
hinting: undefined,
|
||||
instant: false,
|
||||
refId: target.refId,
|
||||
requestId: panelId + target.refId,
|
||||
start,
|
||||
step: 1,
|
||||
});
|
||||
expect(activeTargets[1]).toEqual({
|
||||
...target,
|
||||
format: 'time_series',
|
||||
instant: false,
|
||||
requestId: panelId + target.refId,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('and both Graph and Table are hidden', () => {
|
||||
it('then it should return empty arrays', () => {
|
||||
const target: PromQuery = {
|
||||
refId: 'A',
|
||||
expr: 'up',
|
||||
context: PromContext.Explore,
|
||||
showingGraph: false,
|
||||
showingTable: false,
|
||||
};
|
||||
|
||||
const { queries, activeTargets } = getPrepareTargetsContext(target);
|
||||
|
||||
expect(queries.length).toBe(0);
|
||||
expect(activeTargets.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and Graph is hidden', () => {
|
||||
it('then it should return only intant related objects', () => {
|
||||
const target: PromQuery = {
|
||||
refId: 'A',
|
||||
expr: 'up',
|
||||
context: PromContext.Explore,
|
||||
showingGraph: false,
|
||||
showingTable: true,
|
||||
};
|
||||
|
||||
const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target);
|
||||
|
||||
expect(queries.length).toBe(1);
|
||||
expect(activeTargets.length).toBe(1);
|
||||
expect(queries[0]).toEqual({
|
||||
end,
|
||||
expr: 'up',
|
||||
headers: {
|
||||
'X-Dashboard-Id': undefined,
|
||||
'X-Panel-Id': panelId,
|
||||
},
|
||||
hinting: undefined,
|
||||
instant: true,
|
||||
refId: target.refId,
|
||||
requestId: panelId + target.refId + '_instant',
|
||||
start,
|
||||
step: 1,
|
||||
});
|
||||
expect(activeTargets[0]).toEqual({
|
||||
...target,
|
||||
format: 'table',
|
||||
instant: true,
|
||||
requestId: panelId + target.refId + '_instant',
|
||||
valueWithRefId: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and Table is hidden', () => {
|
||||
it('then it should return only time series related objects', () => {
|
||||
const target: PromQuery = {
|
||||
refId: 'A',
|
||||
expr: 'up',
|
||||
context: PromContext.Explore,
|
||||
showingGraph: true,
|
||||
showingTable: false,
|
||||
};
|
||||
|
||||
const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target);
|
||||
|
||||
expect(queries.length).toBe(1);
|
||||
expect(activeTargets.length).toBe(1);
|
||||
expect(queries[0]).toEqual({
|
||||
end,
|
||||
expr: 'up',
|
||||
headers: {
|
||||
'X-Dashboard-Id': undefined,
|
||||
'X-Panel-Id': panelId,
|
||||
},
|
||||
hinting: undefined,
|
||||
instant: false,
|
||||
refId: target.refId,
|
||||
requestId: panelId + target.refId,
|
||||
start,
|
||||
step: 1,
|
||||
});
|
||||
expect(activeTargets[0]).toEqual({
|
||||
...target,
|
||||
format: 'time_series',
|
||||
instant: false,
|
||||
requestId: panelId + target.refId,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,9 @@ export interface PromQuery extends DataQuery {
|
||||
intervalFactor?: number;
|
||||
legendFormat?: string;
|
||||
valueWithRefId?: boolean;
|
||||
requestId?: string;
|
||||
showingGraph?: boolean;
|
||||
showingTable?: boolean;
|
||||
}
|
||||
|
||||
export interface PromOptions extends DataSourceJsonData {
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { LogLevel } from '@grafana/data';
|
||||
|
||||
let index = 0;
|
||||
|
||||
export function getRandomLogLevel(): LogLevel {
|
||||
const v = Math.random();
|
||||
if (v > 0.9) {
|
||||
return LogLevel.critical;
|
||||
}
|
||||
if (v > 0.8) {
|
||||
return LogLevel.error;
|
||||
}
|
||||
if (v > 0.7) {
|
||||
return LogLevel.warning;
|
||||
}
|
||||
if (v > 0.4) {
|
||||
return LogLevel.info;
|
||||
}
|
||||
if (v > 0.3) {
|
||||
return LogLevel.debug;
|
||||
}
|
||||
if (v > 0.1) {
|
||||
return LogLevel.trace;
|
||||
}
|
||||
return LogLevel.unknown;
|
||||
}
|
||||
|
||||
export function getNextWord() {
|
||||
index = (index + Math.floor(Math.random() * 5)) % words.length;
|
||||
return words[index];
|
||||
}
|
||||
|
||||
export function getRandomLine(length = 60) {
|
||||
let line = getNextWord();
|
||||
while (line.length < length) {
|
||||
line += ' ' + getNextWord();
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
const words = [
|
||||
'At',
|
||||
'vero',
|
||||
'eos',
|
||||
'et',
|
||||
'accusamus',
|
||||
'et',
|
||||
'iusto',
|
||||
'odio',
|
||||
'dignissimos',
|
||||
'ducimus',
|
||||
'qui',
|
||||
'blanditiis',
|
||||
'praesentium',
|
||||
'voluptatum',
|
||||
'deleniti',
|
||||
'atque',
|
||||
'corrupti',
|
||||
'quos',
|
||||
'dolores',
|
||||
'et',
|
||||
'quas',
|
||||
'molestias',
|
||||
'excepturi',
|
||||
'sint',
|
||||
'occaecati',
|
||||
'cupiditate',
|
||||
'non',
|
||||
'provident',
|
||||
'similique',
|
||||
'sunt',
|
||||
'in',
|
||||
'culpa',
|
||||
'qui',
|
||||
'officia',
|
||||
'deserunt',
|
||||
'mollitia',
|
||||
'animi',
|
||||
'id',
|
||||
'est',
|
||||
'laborum',
|
||||
'et',
|
||||
'dolorum',
|
||||
'fuga',
|
||||
'Et',
|
||||
'harum',
|
||||
'quidem',
|
||||
'rerum',
|
||||
'facilis',
|
||||
'est',
|
||||
'et',
|
||||
'expedita',
|
||||
'distinctio',
|
||||
'Nam',
|
||||
'libero',
|
||||
'tempore',
|
||||
'cum',
|
||||
'soluta',
|
||||
'nobis',
|
||||
'est',
|
||||
'eligendi',
|
||||
'optio',
|
||||
'cumque',
|
||||
'nihil',
|
||||
'impedit',
|
||||
'quo',
|
||||
'minus',
|
||||
'id',
|
||||
'quod',
|
||||
'maxime',
|
||||
'placeat',
|
||||
'facere',
|
||||
'possimus',
|
||||
'omnis',
|
||||
'voluptas',
|
||||
'assumenda',
|
||||
'est',
|
||||
'omnis',
|
||||
'dolor',
|
||||
'repellendus',
|
||||
'Temporibus',
|
||||
'autem',
|
||||
'quibusdam',
|
||||
'et',
|
||||
'aut',
|
||||
'officiis',
|
||||
'debitis',
|
||||
'aut',
|
||||
'rerum',
|
||||
'necessitatibus',
|
||||
'saepe',
|
||||
'eveniet',
|
||||
'ut',
|
||||
'et',
|
||||
'voluptates',
|
||||
'repudiandae',
|
||||
'sint',
|
||||
'et',
|
||||
'molestiae',
|
||||
'non',
|
||||
'recusandae',
|
||||
'Itaque',
|
||||
'earum',
|
||||
'rerum',
|
||||
'hic',
|
||||
'tenetur',
|
||||
'a',
|
||||
'sapiente',
|
||||
'delectus',
|
||||
'ut',
|
||||
'aut',
|
||||
'reiciendis',
|
||||
'voluptatibus',
|
||||
'maiores',
|
||||
'alias',
|
||||
'consequatur',
|
||||
'aut',
|
||||
'perferendis',
|
||||
'doloribus',
|
||||
'asperiores',
|
||||
'repellat',
|
||||
];
|
||||
+29
-41
@@ -3,52 +3,45 @@ import {
|
||||
DataSourceApi,
|
||||
DataQueryRequest,
|
||||
DataSourceInstanceSettings,
|
||||
DataStreamObserver,
|
||||
DataQueryResponse,
|
||||
MetricFindValue,
|
||||
} from '@grafana/ui';
|
||||
import { TableData, TimeSeries } from '@grafana/data';
|
||||
import { TestDataQuery, Scenario } from './types';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { StreamHandler } from './StreamHandler';
|
||||
import { queryMetricTree } from './metricTree';
|
||||
import { Observable, from, merge } from 'rxjs';
|
||||
import { runStream } from './runStreams';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
|
||||
type TestData = TimeSeries | TableData;
|
||||
|
||||
export interface TestDataRegistry {
|
||||
[key: string]: TestData[];
|
||||
}
|
||||
|
||||
export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
|
||||
streams = new StreamHandler();
|
||||
|
||||
/** @ngInject */
|
||||
constructor(instanceSettings: DataSourceInstanceSettings) {
|
||||
super(instanceSettings);
|
||||
}
|
||||
|
||||
query(options: DataQueryRequest<TestDataQuery>, observer: DataStreamObserver) {
|
||||
const queries = options.targets.map(item => {
|
||||
return {
|
||||
...item,
|
||||
query(options: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
|
||||
const queries: any[] = [];
|
||||
const streams: Array<Observable<DataQueryResponse>> = [];
|
||||
|
||||
// Start streams and prepare queries
|
||||
for (const target of options.targets) {
|
||||
if (target.scenarioId === 'streaming_client') {
|
||||
streams.push(runStream(target, options));
|
||||
} else {
|
||||
queries.push({
|
||||
...target,
|
||||
intervalMs: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
datasourceId: this.id,
|
||||
alias: templateSrv.replace(item.alias || ''),
|
||||
};
|
||||
alias: templateSrv.replace(target.alias || ''),
|
||||
});
|
||||
|
||||
if (queries.length === 0) {
|
||||
return Promise.resolve({ data: [] });
|
||||
}
|
||||
}
|
||||
|
||||
// Currently we do not support mixed with client only streaming
|
||||
const resp = this.streams.process(options, observer);
|
||||
if (resp) {
|
||||
return Promise.resolve(resp);
|
||||
}
|
||||
|
||||
return getBackendSrv()
|
||||
if (queries.length) {
|
||||
const req: Promise<DataQueryResponse> = getBackendSrv()
|
||||
.datasourceRequest({
|
||||
method: 'POST',
|
||||
url: '/api/tsdb/query',
|
||||
@@ -57,21 +50,22 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
|
||||
to: options.range.to.valueOf().toString(),
|
||||
queries: queries,
|
||||
},
|
||||
|
||||
// This sets up a cancel token
|
||||
requestId: options.requestId,
|
||||
})
|
||||
.then((res: any) => {
|
||||
.then((res: any) => this.processQueryResult(queries, res));
|
||||
|
||||
streams.push(from(req));
|
||||
}
|
||||
|
||||
return merge(...streams);
|
||||
}
|
||||
|
||||
processQueryResult(queries: any, res: any): DataQueryResponse {
|
||||
const data: TestData[] = [];
|
||||
|
||||
// Returns data in the order it was asked for.
|
||||
// if the response has data with different refId, it is ignored
|
||||
for (const query of queries) {
|
||||
const results = res.data.results[query.refId];
|
||||
if (!results) {
|
||||
console.warn('No Results for:', query);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const t of results.tables || []) {
|
||||
const table = t as TableData;
|
||||
@@ -81,17 +75,11 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
|
||||
}
|
||||
|
||||
for (const series of results.series || []) {
|
||||
data.push({
|
||||
target: series.name,
|
||||
datapoints: series.points,
|
||||
refId: query.refId,
|
||||
tags: series.tags,
|
||||
});
|
||||
data.push({ target: series.name, datapoints: series.points, refId: query.refId, tags: series.tags });
|
||||
}
|
||||
}
|
||||
|
||||
return { data: data };
|
||||
});
|
||||
return { data };
|
||||
}
|
||||
|
||||
annotationQuery(options: any) {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
import { defaultQuery } from './StreamHandler';
|
||||
import { defaultQuery } from './runStreams';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { dateTime } from '@grafana/data';
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import { defaults } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { DataQueryRequest, DataQueryResponse } from '@grafana/ui';
|
||||
|
||||
import { FieldType, CircularDataFrame, CSVReader, Field, LoadingState } from '@grafana/data';
|
||||
|
||||
import { TestDataQuery, StreamingQuery } from './types';
|
||||
import { getRandomLine } from './LogIpsum';
|
||||
|
||||
export const defaultQuery: StreamingQuery = {
|
||||
type: 'signal',
|
||||
speed: 250, // ms
|
||||
spread: 3.5,
|
||||
noise: 2.2,
|
||||
bands: 1,
|
||||
};
|
||||
|
||||
export function runStream(target: TestDataQuery, req: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
|
||||
const query = defaults(target.stream, defaultQuery);
|
||||
if ('signal' === query.type) {
|
||||
return runSignalStream(target, query, req);
|
||||
}
|
||||
if ('logs' === query.type) {
|
||||
return runLogsStream(target, query, req);
|
||||
}
|
||||
if ('fetch' === query.type) {
|
||||
return runFetchStream(target, query, req);
|
||||
}
|
||||
throw new Error(`Unknown Stream Type: ${query.type}`);
|
||||
}
|
||||
|
||||
export function runSignalStream(
|
||||
target: TestDataQuery,
|
||||
query: StreamingQuery,
|
||||
req: DataQueryRequest<TestDataQuery>
|
||||
): Observable<DataQueryResponse> {
|
||||
return new Observable<DataQueryResponse>(subscriber => {
|
||||
const streamId = `signal-${req.panelId}-${target.refId}`;
|
||||
const maxDataPoints = req.maxDataPoints || 1000;
|
||||
|
||||
const data = new CircularDataFrame({
|
||||
append: 'tail',
|
||||
capacity: maxDataPoints,
|
||||
});
|
||||
data.refId = target.refId;
|
||||
data.name = target.alias || 'Signal ' + target.refId;
|
||||
data.addField({ name: 'time', type: FieldType.time });
|
||||
data.addField({ name: 'value', type: FieldType.number });
|
||||
|
||||
const { spread, speed, bands, noise } = query;
|
||||
|
||||
for (let i = 0; i < bands; i++) {
|
||||
const suffix = bands > 1 ? ` ${i + 1}` : '';
|
||||
data.addField({ name: 'Min' + suffix, type: FieldType.number });
|
||||
data.addField({ name: 'Max' + suffix, type: FieldType.number });
|
||||
}
|
||||
|
||||
let value = Math.random() * 100;
|
||||
let timeoutId: any = null;
|
||||
|
||||
const addNextRow = (time: number) => {
|
||||
value += (Math.random() - 0.5) * spread;
|
||||
|
||||
let idx = 0;
|
||||
data.fields[idx++].values.add(time);
|
||||
data.fields[idx++].values.add(value);
|
||||
|
||||
let min = value;
|
||||
let max = value;
|
||||
|
||||
for (let i = 0; i < bands; i++) {
|
||||
min = min - Math.random() * noise;
|
||||
max = max + Math.random() * noise;
|
||||
|
||||
data.fields[idx++].values.add(min);
|
||||
data.fields[idx++].values.add(max);
|
||||
}
|
||||
};
|
||||
|
||||
// Fill the buffer on init
|
||||
if (true) {
|
||||
let time = Date.now() - maxDataPoints * speed;
|
||||
for (let i = 0; i < maxDataPoints; i++) {
|
||||
addNextRow(time);
|
||||
time += speed;
|
||||
}
|
||||
}
|
||||
|
||||
const pushNextEvent = () => {
|
||||
addNextRow(Date.now());
|
||||
subscriber.next({
|
||||
data: [data],
|
||||
key: streamId,
|
||||
});
|
||||
|
||||
timeoutId = setTimeout(pushNextEvent, speed);
|
||||
};
|
||||
|
||||
// Send first event in 5ms
|
||||
setTimeout(pushNextEvent, 5);
|
||||
|
||||
return () => {
|
||||
console.log('unsubscribing to stream ' + streamId);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function runLogsStream(
|
||||
target: TestDataQuery,
|
||||
query: StreamingQuery,
|
||||
req: DataQueryRequest<TestDataQuery>
|
||||
): Observable<DataQueryResponse> {
|
||||
return new Observable<DataQueryResponse>(subscriber => {
|
||||
const streamId = `logs-${req.panelId}-${target.refId}`;
|
||||
const maxDataPoints = req.maxDataPoints || 1000;
|
||||
|
||||
const data = new CircularDataFrame({
|
||||
append: 'tail',
|
||||
capacity: maxDataPoints,
|
||||
});
|
||||
data.refId = target.refId;
|
||||
data.name = target.alias || 'Logs ' + target.refId;
|
||||
data.addField({ name: 'time', type: FieldType.time });
|
||||
data.addField({ name: 'line', type: FieldType.string });
|
||||
|
||||
const { speed } = query;
|
||||
|
||||
let timeoutId: any = null;
|
||||
|
||||
const pushNextEvent = () => {
|
||||
data.values.time.add(Date.now());
|
||||
data.values.line.add(getRandomLine());
|
||||
|
||||
subscriber.next({
|
||||
data: [data],
|
||||
key: streamId,
|
||||
});
|
||||
|
||||
timeoutId = setTimeout(pushNextEvent, speed);
|
||||
};
|
||||
|
||||
// Send first event in 5ms
|
||||
setTimeout(pushNextEvent, 5);
|
||||
|
||||
return () => {
|
||||
console.log('unsubscribing to stream ' + streamId);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function runFetchStream(
|
||||
target: TestDataQuery,
|
||||
query: StreamingQuery,
|
||||
req: DataQueryRequest<TestDataQuery>
|
||||
): Observable<DataQueryResponse> {
|
||||
return new Observable<DataQueryResponse>(subscriber => {
|
||||
const streamId = `fetch-${req.panelId}-${target.refId}`;
|
||||
const maxDataPoints = req.maxDataPoints || 1000;
|
||||
|
||||
let data = new CircularDataFrame({
|
||||
append: 'tail',
|
||||
capacity: maxDataPoints,
|
||||
});
|
||||
data.refId = target.refId;
|
||||
data.name = target.alias || 'Fetch ' + target.refId;
|
||||
|
||||
let reader: ReadableStreamReader<Uint8Array>;
|
||||
const csv = new CSVReader({
|
||||
callback: {
|
||||
onHeader: (fields: Field[]) => {
|
||||
// Clear any existing fields
|
||||
if (data.fields.length) {
|
||||
data = new CircularDataFrame({
|
||||
append: 'tail',
|
||||
capacity: maxDataPoints,
|
||||
});
|
||||
data.refId = target.refId;
|
||||
data.name = 'Fetch ' + target.refId;
|
||||
}
|
||||
for (const field of fields) {
|
||||
data.addField(field);
|
||||
}
|
||||
},
|
||||
onRow: (row: any[]) => {
|
||||
data.add(row);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const processChunk = (value: ReadableStreamReadResult<Uint8Array>): any => {
|
||||
if (value.value) {
|
||||
const text = new TextDecoder().decode(value.value);
|
||||
csv.readCSV(text);
|
||||
}
|
||||
|
||||
subscriber.next({
|
||||
data: [data],
|
||||
key: streamId,
|
||||
state: value.done ? LoadingState.Done : LoadingState.Streaming,
|
||||
});
|
||||
|
||||
if (value.done) {
|
||||
console.log('Finished stream');
|
||||
subscriber.complete(); // necessary?
|
||||
return;
|
||||
}
|
||||
|
||||
return reader.read().then(processChunk);
|
||||
};
|
||||
|
||||
fetch(new Request(query.url)).then(response => {
|
||||
reader = response.body.getReader();
|
||||
reader.read().then(processChunk);
|
||||
});
|
||||
|
||||
return () => {
|
||||
// Cancel fetch?
|
||||
console.log('unsubscribing to stream ' + streamId);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -13,8 +13,7 @@ import config from 'app/core/config';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import { DataFrame, DataLink, DateTimeInput } from '@grafana/data';
|
||||
import { getColorFromHexRgbOrName, LegacyResponseData, VariableSuggestion } from '@grafana/ui';
|
||||
import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState';
|
||||
import { PanelQueryRunnerFormat } from 'app/features/dashboard/state/PanelQueryRunner';
|
||||
import { getProcessedDataFrames } from 'app/features/dashboard/state/runRequest';
|
||||
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
|
||||
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||
|
||||
@@ -143,7 +142,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
_.defaults(this.panel.xaxis, this.panelDefaults.xaxis);
|
||||
_.defaults(this.panel.options, this.panelDefaults.options);
|
||||
|
||||
this.dataFormat = PanelQueryRunnerFormat.frames;
|
||||
this.useDataFrames = true;
|
||||
this.processor = new DataProcessor(this.panel);
|
||||
this.contextMenuCtrl = new GraphContextMenuCtrl($scope);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DataProcessor } from '../data_processor';
|
||||
import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState';
|
||||
import { getProcessedDataFrames } from 'app/features/dashboard/state/runRequest';
|
||||
|
||||
describe('Graph DataProcessor', () => {
|
||||
const panel: any = {
|
||||
|
||||
@@ -28,8 +28,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { auto } from 'angular';
|
||||
import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
|
||||
import { PanelQueryRunnerFormat } from 'app/features/dashboard/state/PanelQueryRunner';
|
||||
import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState';
|
||||
import { getProcessedDataFrames } from 'app/features/dashboard/state/runRequest';
|
||||
|
||||
const BASE_FONT_SIZE = 38;
|
||||
|
||||
@@ -124,7 +123,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
this.events.on('data-snapshot-load', this.onDataReceived.bind(this));
|
||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
||||
|
||||
this.dataFormat = PanelQueryRunnerFormat.frames;
|
||||
this.useDataFrames = true;
|
||||
|
||||
this.onSparklineColorChange = this.onSparklineColorChange.bind(this);
|
||||
this.onSparklineFillChange = this.onSparklineFillChange.bind(this);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
import { ComponentClass } from 'react';
|
||||
import {
|
||||
DataQuery,
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
|
||||
import { Emitter } from 'app/core/core';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import { PanelQueryState } from '../features/dashboard/state/PanelQueryState';
|
||||
|
||||
export enum ExploreMode {
|
||||
Metrics = 'Metrics',
|
||||
@@ -263,7 +263,7 @@ export interface ExploreItemState {
|
||||
isPaused: boolean;
|
||||
urlReplaced: boolean;
|
||||
|
||||
queryState: PanelQueryState;
|
||||
querySubscription?: Unsubscribable;
|
||||
|
||||
queryResponse: PanelData;
|
||||
originPanelId?: number;
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface When<State> {
|
||||
}
|
||||
|
||||
export interface Then<State> {
|
||||
thenStateShouldEqual: (state: State) => Then<State>;
|
||||
thenStateShouldEqual: (state: State) => When<State>;
|
||||
}
|
||||
|
||||
interface ObjectType extends Object {
|
||||
@@ -62,12 +62,12 @@ export const reducerTester = <State>(): Given<State> => {
|
||||
};
|
||||
|
||||
const whenActionIsDispatched = (action: ActionOf<any>): Then<State> => {
|
||||
resultingState = reducerUnderTest(initialState, action);
|
||||
resultingState = reducerUnderTest(resultingState || initialState, action);
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
const thenStateShouldEqual = (state: State): Then<State> => {
|
||||
const thenStateShouldEqual = (state: State): When<State> => {
|
||||
expect(state).toEqual(resultingState);
|
||||
|
||||
return instance;
|
||||
|
||||
Reference in New Issue
Block a user