mirror of https://github.com/grafana/grafana
Refactor: split PanelQueryRunner into runner and state (#16685)
* check for running * split out panel state * adding test file * remove bad testpull/16688/head
parent
178ce8eec8
commit
e7f56a74fc
@ -0,0 +1,46 @@ |
|||||||
|
import { toDataQueryError, PanelQueryState } from './PanelQueryState'; |
||||||
|
import { MockDataSourceApi } from 'test/mocks/datasource_srv'; |
||||||
|
import { DataQueryResponse } 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.isRunning()).toBeFalsy(); |
||||||
|
let hasRun = false; |
||||||
|
const dsRunner = new Promise<DataQueryResponse>((resolve, reject) => { |
||||||
|
// The status should be running when we get here
|
||||||
|
expect(state.isRunning()).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.isRunning()).toBeFalsy(); |
||||||
|
expect(empty.series.length).toBe(0); |
||||||
|
expect(hasRun).toBeFalsy(); |
||||||
|
|
||||||
|
empty = await state.execute( |
||||||
|
ds, |
||||||
|
getQueryOptions({ targets: [{ hide: true, refId: 'X' }, { hide: true, refId: 'Y' }, { hide: true, refId: 'Z' }] }) |
||||||
|
); |
||||||
|
// should not run any hidden queries'
|
||||||
|
expect(state.isRunning()).toBeFalsy(); |
||||||
|
expect(empty.series.length).toBe(0); |
||||||
|
expect(hasRun).toBeFalsy(); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -0,0 +1,179 @@ |
|||||||
|
import { |
||||||
|
DataSourceApi, |
||||||
|
DataQueryRequest, |
||||||
|
PanelData, |
||||||
|
LoadingState, |
||||||
|
toLegacyResponseData, |
||||||
|
isSeriesData, |
||||||
|
toSeriesData, |
||||||
|
DataQueryError, |
||||||
|
} 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
|
||||||
|
request = { |
||||||
|
startTime: 0, |
||||||
|
endTime: 1000, // Somethign not zero
|
||||||
|
} as DataQueryRequest; |
||||||
|
|
||||||
|
// The best known state of data
|
||||||
|
data = { |
||||||
|
state: LoadingState.NotStarted, |
||||||
|
series: [], |
||||||
|
} as PanelData; |
||||||
|
|
||||||
|
sendSeries = false; |
||||||
|
sendLegacy = false; |
||||||
|
|
||||||
|
// A promise for the running query
|
||||||
|
private executor: Promise<PanelData> = {} as any; |
||||||
|
private rejector = (reason?: any) => {}; |
||||||
|
private datasource: DataSourceApi = {} as any; |
||||||
|
|
||||||
|
isRunning() { |
||||||
|
return this.data.state === LoadingState.Loading; //
|
||||||
|
} |
||||||
|
|
||||||
|
isSameQuery(ds: DataSourceApi, req: DataQueryRequest) { |
||||||
|
if (this.datasource !== this.datasource) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// For now just check that the targets look the same
|
||||||
|
return isEqual(this.request.targets, req.targets); |
||||||
|
} |
||||||
|
|
||||||
|
getCurrentExecutor() { |
||||||
|
return this.executor; |
||||||
|
} |
||||||
|
|
||||||
|
cancel(reason: string) { |
||||||
|
const { request } = this; |
||||||
|
try { |
||||||
|
if (!request.endTime) { |
||||||
|
request.endTime = Date.now(); |
||||||
|
|
||||||
|
this.rejector('Canceled:' + reason); |
||||||
|
} |
||||||
|
|
||||||
|
// Cancel any open HTTP request with the same ID
|
||||||
|
if (request.requestId) { |
||||||
|
getBackendSrv().resolveCancelerIfExists(request.requestId); |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
console.log('Error canceling request'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
execute(ds: DataSourceApi, req: DataQueryRequest): Promise<PanelData> { |
||||||
|
this.request = req; |
||||||
|
|
||||||
|
console.log('EXXXX', req); |
||||||
|
|
||||||
|
// 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(); |
||||||
|
return Promise.resolve( |
||||||
|
(this.data = { |
||||||
|
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.rejector = reject; |
||||||
|
|
||||||
|
return ds |
||||||
|
.query(this.request) |
||||||
|
.then(resp => { |
||||||
|
this.request.endTime = Date.now(); |
||||||
|
|
||||||
|
// 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, |
||||||
|
}) |
||||||
|
); |
||||||
|
}) |
||||||
|
.catch(err => { |
||||||
|
resolve(this.setError(err)); |
||||||
|
}); |
||||||
|
})); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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)); |
||||||
|
} |
||||||
|
if (sendSeries && !data.series.length && data.legacy) { |
||||||
|
data.series = data.legacy.map(v => toSeriesData(v)); |
||||||
|
} |
||||||
|
return this.data; |
||||||
|
} |
||||||
|
|
||||||
|
setError(err: any): PanelData { |
||||||
|
if (!this.request.endTime) { |
||||||
|
this.request.endTime = Date.now(); |
||||||
|
} |
||||||
|
|
||||||
|
return (this.data = { |
||||||
|
...this.data, // Keep any existing data
|
||||||
|
state: LoadingState.Error, |
||||||
|
error: toDataQueryError(err), |
||||||
|
request: this.request, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
import { DataSourceApi, DataQueryRequest, DataQueryResponse } from '@grafana/ui'; |
||||||
|
|
||||||
|
export class DatasourceSrvMock { |
||||||
|
constructor(private defaultDS: DataSourceApi, private datasources: { [name: string]: DataSourceApi }) { |
||||||
|
//
|
||||||
|
} |
||||||
|
|
||||||
|
get(name?: string): Promise<DataSourceApi> { |
||||||
|
if (!name) { |
||||||
|
return Promise.resolve(this.defaultDS); |
||||||
|
} |
||||||
|
const ds = this.datasources[name]; |
||||||
|
if (ds) { |
||||||
|
return Promise.resolve(ds); |
||||||
|
} |
||||||
|
return Promise.reject('Unknown Datasource: ' + name); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class MockDataSourceApi implements DataSourceApi { |
||||||
|
name: string; |
||||||
|
|
||||||
|
result: DataQueryResponse = { data: [] }; |
||||||
|
queryResolver: Promise<DataQueryResponse>; |
||||||
|
|
||||||
|
constructor(DataQueryResponse, name?: string) { |
||||||
|
this.name = name ? name : 'MockDataSourceApi'; |
||||||
|
} |
||||||
|
|
||||||
|
query(request: DataQueryRequest): Promise<DataQueryResponse> { |
||||||
|
if (this.queryResolver) { |
||||||
|
return this.queryResolver; |
||||||
|
} |
||||||
|
return Promise.resolve(this.result); |
||||||
|
} |
||||||
|
|
||||||
|
testDatasource() { |
||||||
|
return Promise.resolve(); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue