mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* 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
212 lines
5.8 KiB
TypeScript
212 lines
5.8 KiB
TypeScript
// 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;
|
|
};
|
|
}
|