Commit 0ec83038 by Torkel Ödegaard Committed by Hugo Häggmark

Panels: Skip re-rendering panel/visualisation in loading state (#19518)

* Loading states and partial rendering, set loading state in mixed data source, and do not render loading states for react panels

* Updated mixed data source tests
parent f2ef49f9
......@@ -115,27 +115,34 @@ export class PanelChrome extends PureComponent<Props, State> {
return;
}
let { errorMessage, isFirstLoad } = this.state;
if (data.state === LoadingState.Error) {
const { error } = data;
if (error) {
if (errorMessage !== error.message) {
errorMessage = error.message;
let { isFirstLoad } = this.state;
let errorMessage: string | null = null;
switch (data.state) {
case LoadingState.Loading:
// Skip updating state data if it is already in loading state
// This is to avoid rendering partial loading responses
if (this.state.data.state === LoadingState.Loading) {
return;
}
}
} else {
errorMessage = null;
}
if (data.state === LoadingState.Done) {
// If we are doing a snapshot save data in panel model
if (this.props.dashboard.snapshot) {
this.props.panel.snapshotData = data.series.map(frame => toDataFrameDTO(frame));
}
if (isFirstLoad) {
isFirstLoad = false;
}
break;
case LoadingState.Error:
const { error } = data;
if (error) {
if (errorMessage !== error.message) {
errorMessage = error.message;
}
}
break;
case LoadingState.Done:
// If we are doing a snapshot save data in panel model
if (this.props.dashboard.snapshot) {
this.props.panel.snapshotData = data.series.map(frame => toDataFrameDTO(frame));
}
if (isFirstLoad) {
isFirstLoad = false;
}
break;
}
this.setState({ isFirstLoad, errorMessage, data });
......
......@@ -2,6 +2,7 @@ import { DatasourceSrvMock, MockDataSourceApi } from 'test/mocks/datasource_srv'
import { getDataSourceSrv } from '@grafana/runtime';
import { getQueryOptions } from 'test/helpers/getQueryOptions';
import { DataSourceInstanceSettings } from '@grafana/ui';
import { LoadingState } from '@grafana/data';
import { MixedDatasource } from './module';
import { from } from 'rxjs';
......@@ -29,17 +30,23 @@ describe('MixedDatasource', () => {
});
const results: any[] = [];
beforeEach(async () => {
beforeEach(async done => {
const ds = await getDataSourceSrv().get('-- Mixed --');
from(ds.query(requestMixed)).subscribe(result => {
results.push(result);
if (result.state === LoadingState.Done) {
done();
}
});
});
it('direct query should return results', async () => {
expect(results.length).toBe(3);
expect(results[0].data).toEqual(['AAAA']);
expect(results[0].state).toEqual(LoadingState.Loading);
expect(results[1].data).toEqual(['BBBB']);
expect(results[2].data).toEqual(['CCCC']);
expect(results[2].state).toEqual(LoadingState.Done);
});
});
import cloneDeep from 'lodash/cloneDeep';
import groupBy from 'lodash/groupBy';
import { from, of, Observable, merge } from 'rxjs';
import { tap } from 'rxjs/operators';
import { LoadingState } from '@grafana/data';
import { DataSourceApi, DataQuery, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings } from '@grafana/ui';
import { getDataSourceSrv } from '@grafana/runtime';
import { mergeMap, map, filter } from 'rxjs/operators';
import { mergeMap, map } from 'rxjs/operators';
export const MIXED_DATASOURCE_NAME = '-- Mixed --';
......@@ -25,13 +27,14 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
const sets: { [key: string]: DataQuery[] } = groupBy(queries, 'datasource');
const observables: Array<Observable<DataQueryResponse>> = [];
let runningSubRequests = 0;
for (const key in sets) {
const targets = sets[key];
const dsName = targets[0].datasource;
const observable = from(getDataSourceSrv().get(dsName)).pipe(
map((dataSourceApi: DataSourceApi) => {
mergeMap((dataSourceApi: DataSourceApi) => {
const datasourceRequest = cloneDeep(request);
// Remove any unused hidden queries
......@@ -42,28 +45,41 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
datasourceRequest.targets = newTargets;
datasourceRequest.requestId = `${dsName}${datasourceRequest.requestId || ''}`;
return {
dataSourceApi,
datasourceRequest,
};
})
);
const noTargets = observable.pipe(
filter(({ datasourceRequest }) => datasourceRequest.targets.length === 0),
mergeMap(() => {
return of({ data: [] } as DataQueryResponse);
})
);
// all queries hidden return empty result for for this requestId
if (datasourceRequest.targets.length === 0) {
return of({ data: [], key: datasourceRequest.requestId });
}
runningSubRequests++;
let hasCountedAsDone = false;
const hasTargets = observable.pipe(
filter(({ datasourceRequest }) => datasourceRequest.targets.length > 0),
mergeMap(({ dataSourceApi, datasourceRequest }) => {
return from(dataSourceApi.query(datasourceRequest)).pipe(
tap(
(response: DataQueryResponse) => {
if (
hasCountedAsDone ||
response.state === LoadingState.Streaming ||
response.state === LoadingState.Loading
) {
return;
}
runningSubRequests--;
hasCountedAsDone = true;
},
() => {
if (hasCountedAsDone) {
return;
}
hasCountedAsDone = true;
runningSubRequests--;
}
),
map((response: DataQueryResponse) => {
return {
...response,
data: response.data || [],
state: runningSubRequests === 0 ? LoadingState.Done : LoadingState.Loading,
key: `${dsName}${response.key || ''}`,
} as DataQueryResponse;
})
......@@ -71,7 +87,7 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
})
);
observables.push(merge(noTargets, hasTargets));
observables.push(observable);
}
return merge(...observables);
......
......@@ -40,7 +40,11 @@ export class MockDataSourceApi extends DataSourceApi {
if (this.queryResolver) {
return this.queryResolver;
}
return Promise.resolve(this.result);
return new Promise(resolver => {
setTimeout(() => {
resolver(this.result);
});
});
}
testDatasource() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment