|
|
@@ -1,16 +1,25 @@
|
|
|
+// Libraries
|
|
|
+import isString from 'lodash/isString';
|
|
|
+import isEqual from 'lodash/isEqual';
|
|
|
+
|
|
|
+// Utils & Services
|
|
|
+import { getBackendSrv } from 'app/core/services/backend_srv';
|
|
|
+import * as dateMath from 'app/core/utils/datemath';
|
|
|
+import { guessFieldTypes, toSeriesData, isSeriesData } from '@grafana/ui/src/utils';
|
|
|
+
|
|
|
+// Types
|
|
|
import {
|
|
|
DataSourceApi,
|
|
|
DataQueryRequest,
|
|
|
PanelData,
|
|
|
LoadingState,
|
|
|
toLegacyResponseData,
|
|
|
- isSeriesData,
|
|
|
- toSeriesData,
|
|
|
DataQueryError,
|
|
|
+ DataStreamObserver,
|
|
|
+ DataStreamState,
|
|
|
+ SeriesData,
|
|
|
+ DataQueryResponseData,
|
|
|
} from '@grafana/ui';
|
|
|
-import { getProcessedSeriesData } from './PanelQueryRunner';
|
|
|
-import { getBackendSrv } from 'app/core/services/backend_srv';
|
|
|
-import isEqual from 'lodash/isEqual';
|
|
|
|
|
|
export class PanelQueryState {
|
|
|
// The current/last running request
|
|
|
@@ -19,26 +28,33 @@ export class PanelQueryState {
|
|
|
endTime: 1000, // Somethign not zero
|
|
|
} as DataQueryRequest;
|
|
|
|
|
|
- // The best known state of data
|
|
|
- data = {
|
|
|
+ // The result back from the datasource query
|
|
|
+ response = {
|
|
|
state: LoadingState.NotStarted,
|
|
|
series: [],
|
|
|
} as PanelData;
|
|
|
|
|
|
+ // Active stream results
|
|
|
+ streams: DataStreamState[] = [];
|
|
|
+
|
|
|
sendSeries = false;
|
|
|
sendLegacy = false;
|
|
|
|
|
|
// A promise for the running query
|
|
|
- private executor: Promise<PanelData> = {} as any;
|
|
|
+ private executor?: Promise<PanelData>;
|
|
|
private rejector = (reason?: any) => {};
|
|
|
private datasource: DataSourceApi = {} as any;
|
|
|
|
|
|
- isRunning() {
|
|
|
- return this.data.state === LoadingState.Loading; //
|
|
|
+ isFinished(state: LoadingState) {
|
|
|
+ return state === LoadingState.Done || state === LoadingState.Error;
|
|
|
+ }
|
|
|
+
|
|
|
+ isStarted() {
|
|
|
+ return this.response.state !== LoadingState.NotStarted;
|
|
|
}
|
|
|
|
|
|
isSameQuery(ds: DataSourceApi, req: DataQueryRequest) {
|
|
|
- if (this.datasource !== this.datasource) {
|
|
|
+ if (ds !== this.datasource) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
@@ -46,16 +62,22 @@ export class PanelQueryState {
|
|
|
return isEqual(this.request.targets, req.targets);
|
|
|
}
|
|
|
|
|
|
- getCurrentExecutor() {
|
|
|
+ /**
|
|
|
+ * 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('Canceled:' + reason);
|
|
|
}
|
|
|
|
|
|
@@ -64,8 +86,11 @@ export class PanelQueryState {
|
|
|
getBackendSrv().resolveCancelerIfExists(request.requestId);
|
|
|
}
|
|
|
} catch (err) {
|
|
|
- console.log('Error canceling request');
|
|
|
+ console.log('Error canceling request', err);
|
|
|
}
|
|
|
+
|
|
|
+ // Close any open streams
|
|
|
+ this.closeStreams(true);
|
|
|
}
|
|
|
|
|
|
execute(ds: DataSourceApi, req: DataQueryRequest): Promise<PanelData> {
|
|
|
@@ -75,85 +100,214 @@ export class PanelQueryState {
|
|
|
if (!req.targets.length) {
|
|
|
console.log('No queries, so return early');
|
|
|
this.request.endTime = Date.now();
|
|
|
+ this.closeStreams();
|
|
|
return Promise.resolve(
|
|
|
- (this.data = {
|
|
|
+ (this.response = {
|
|
|
state: LoadingState.Done,
|
|
|
series: [], // Clear the data
|
|
|
legacy: [],
|
|
|
- request: req,
|
|
|
})
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// Set the loading state immediatly
|
|
|
- this.data.state = LoadingState.Loading;
|
|
|
- return (this.executor = new Promise<PanelData>((resolve, reject) => {
|
|
|
+ this.response.state = LoadingState.Loading;
|
|
|
+ this.executor = new Promise<PanelData>((resolve, reject) => {
|
|
|
this.rejector = reject;
|
|
|
|
|
|
return ds
|
|
|
- .query(this.request)
|
|
|
+ .query(this.request, this.dataStreamObserver)
|
|
|
.then(resp => {
|
|
|
this.request.endTime = Date.now();
|
|
|
+ this.executor = null;
|
|
|
|
|
|
// Make sure we send something back -- called run() w/o subscribe!
|
|
|
if (!(this.sendSeries || this.sendLegacy)) {
|
|
|
this.sendSeries = true;
|
|
|
}
|
|
|
|
|
|
- // Make sure the response is in a supported format
|
|
|
- const series = this.sendSeries ? getProcessedSeriesData(resp.data) : [];
|
|
|
- const legacy = this.sendLegacy
|
|
|
- ? resp.data.map(v => {
|
|
|
- if (isSeriesData(v)) {
|
|
|
- return toLegacyResponseData(v);
|
|
|
- }
|
|
|
- return v;
|
|
|
- })
|
|
|
- : undefined;
|
|
|
-
|
|
|
- resolve(
|
|
|
- (this.data = {
|
|
|
- state: LoadingState.Done,
|
|
|
- request: this.request,
|
|
|
- series,
|
|
|
- legacy,
|
|
|
- })
|
|
|
- );
|
|
|
+ // Save the result state
|
|
|
+ this.response = {
|
|
|
+ state: LoadingState.Done,
|
|
|
+ request: this.request,
|
|
|
+ series: this.sendSeries ? getProcessedSeriesData(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.sendSeries = true;
|
|
|
+
|
|
|
+ // 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: SeriesData[] = [];
|
|
|
+
|
|
|
+ for (const stream of this.streams) {
|
|
|
+ if (stream.series) {
|
|
|
+ series.push.apply(series, stream.series);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ stream.unsubscribe();
|
|
|
+ } catch {
|
|
|
+ console.log('Failed to unsubscribe to stream');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.streams = [];
|
|
|
+
|
|
|
+ // Move the series from streams to the resposne
|
|
|
+ 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.series);
|
|
|
+
|
|
|
+ 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,
|
|
|
+ series, // Union of series from response and all streams
|
|
|
+ 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 { data, sendLegacy, sendSeries } = this;
|
|
|
- if (sendLegacy && (!data.legacy || !data.legacy.length)) {
|
|
|
- data.legacy = data.series.map(v => toLegacyResponseData(v));
|
|
|
+ const { response, sendLegacy, sendSeries } = this;
|
|
|
+ if (sendLegacy && (!response.legacy || !response.legacy.length)) {
|
|
|
+ response.legacy = response.series.map(v => toLegacyResponseData(v));
|
|
|
}
|
|
|
- if (sendSeries && !data.series.length && data.legacy) {
|
|
|
- data.series = data.legacy.map(v => toSeriesData(v));
|
|
|
+ if (sendSeries && !response.series.length && response.legacy) {
|
|
|
+ response.series = response.legacy.map(v => toSeriesData(v));
|
|
|
}
|
|
|
- return this.data;
|
|
|
+ return this.validateStreamsAndGetPanelData();
|
|
|
}
|
|
|
|
|
|
setError(err: any): PanelData {
|
|
|
if (!this.request.endTime) {
|
|
|
this.request.endTime = Date.now();
|
|
|
}
|
|
|
-
|
|
|
- return (this.data = {
|
|
|
- ...this.data, // Keep any existing data
|
|
|
+ this.closeStreams(true);
|
|
|
+ this.response = {
|
|
|
+ ...this.response, // Keep any existing data
|
|
|
state: LoadingState.Error,
|
|
|
error: toDataQueryError(err),
|
|
|
- request: this.request,
|
|
|
- });
|
|
|
+ };
|
|
|
+ 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) {
|
|
|
@@ -175,3 +329,32 @@ export function toDataQueryError(err: any): DataQueryError {
|
|
|
}
|
|
|
return error;
|
|
|
}
|
|
|
+
|
|
|
+function translateToLegacyData(data: DataQueryResponseData) {
|
|
|
+ return data.map(v => {
|
|
|
+ if (isSeriesData(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 getProcessedSeriesData(results?: any[]): SeriesData[] {
|
|
|
+ if (!results) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ const series: SeriesData[] = [];
|
|
|
+ for (const r of results) {
|
|
|
+ if (r) {
|
|
|
+ series.push(guessFieldTypes(toSeriesData(r)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return series;
|
|
|
+}
|