Explore: Fix time interpolation (#46737)

* Ensure TemplateService is updated with new time range on each time range change.

* Fix linting errors

* Fix explorePane.test.ts

* Reuse createDefaultInitialState

* Remove unused imports

* Add a test for left/right split

* Silence console.error in tests

* Silence console.error in tests
This commit is contained in:
Piotr Jamróz 2022-03-29 14:10:40 +01:00 committed by GitHub
parent 562397ff8b
commit bf977ac245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 174 additions and 77 deletions

View File

@ -329,7 +329,7 @@ exports[`no enzyme tests`] = {
"public/app/plugins/datasource/loki/configuration/ConfigEditor.test.tsx:1661240493": [
[1, 17, 13, "RegExp match", "2409514259"]
],
"public/app/plugins/datasource/loki/configuration/DebugSection.test.tsx:3844880066": [
"public/app/plugins/datasource/loki/configuration/DebugSection.test.tsx:2909181412": [
[1, 17, 13, "RegExp match", "2409514259"]
],
"public/app/plugins/datasource/loki/configuration/DerivedField.test.tsx:1527527456": [

View File

@ -1,4 +1,4 @@
import { VariableModel, ScopedVars } from '@grafana/data';
import { VariableModel, ScopedVars, TimeRange } from '@grafana/data';
/**
* Via the TemplateSrv consumers get access to all the available template variables
@ -22,6 +22,11 @@ export interface TemplateSrv {
* Checks if a target contains template variables.
*/
containsTemplate(target?: string): boolean;
/**
* Update the current time range to be used when interpolating __from / __to variables.
*/
updateTimeRange(timeRange: TimeRange): void;
}
let singletonInstance: TemplateSrv;

View File

@ -144,7 +144,7 @@ function makeDatasourceSetup({ name = 'loki', id = 1 }: { name?: string; id?: nu
}
export const waitForExplore = async () => {
await screen.findByText(/Editor/i);
await screen.findAllByText(/Editor/i);
};
export const tearDown = () => {

View File

@ -0,0 +1,46 @@
import React from 'react';
import { setupExplore, waitForExplore } from './helper/setup';
import { makeLogsQueryResponse } from './helper/query';
import { DataQueryRequest, serializeStateToUrlParam } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { LokiQuery } from '../../../plugins/datasource/loki/types';
jest.mock('react-virtualized-auto-sizer', () => {
return {
__esModule: true,
default(props: any) {
return <div>{props.children({ width: 1000 })}</div>;
},
};
});
describe('Explore: interpolation', () => {
// support-escalations/issues/1459
it('Time is interpolated when explore is opened with a URL', async () => {
const urlParams = {
left: serializeStateToUrlParam({
datasource: 'loki',
queries: [{ refId: 'A', expr: '{ job="a", from="${__from}", to="${__to}" }' }],
range: { from: '1600000000000', to: '1700000000000' },
}),
right: serializeStateToUrlParam({
datasource: 'loki',
queries: [{ refId: 'b', expr: '{ job="b", from="${__from}", to="${__to}" }' }],
range: { from: '1800000000000', to: '1900000000000' },
}),
};
const { datasources } = setupExplore({ urlParams });
const fakeFetch = jest.fn();
(datasources.loki.query as jest.Mock).mockImplementation((request: DataQueryRequest<LokiQuery>) => {
fakeFetch(getTemplateSrv().replace(request.targets[0]!.expr));
return makeLogsQueryResponse();
});
await waitForExplore();
expect(fakeFetch).toBeCalledTimes(2);
expect(fakeFetch).toHaveBeenCalledWith('{ job="a", from="1600000000000", to="1700000000000" }');
expect(fakeFetch).toHaveBeenCalledWith('{ job="b", from="1800000000000", to="1900000000000" }');
});
});

View File

@ -1,47 +1,26 @@
import { DataQuery, DefaultTimeZone, EventBusExtended, serializeStateToUrlParam, toUtc } from '@grafana/data';
import { serializeStateToUrlParam } from '@grafana/data';
import { ExploreId, StoreState, ThunkDispatch } from 'app/types';
import { refreshExplore } from './explorePane';
import { setDataSourceSrv } from '@grafana/runtime';
import { configureStore } from '../../../store/configureStore';
import { of } from 'rxjs';
import { createDefaultInitialState } from './helpers';
jest.mock('../../dashboard/services/TimeSrv', () => ({
getTimeSrv: jest.fn().mockReturnValue({
init: jest.fn(),
timeRange: jest.fn().mockReturnValue({}),
}),
}));
const t = toUtc();
const testRange = {
from: t,
to: t,
raw: {
from: t,
to: t,
},
};
const { testRange, defaultInitialState } = createDefaultInitialState();
const defaultInitialState = {
user: {
orgId: '1',
timeZone: DefaultTimeZone,
},
explore: {
[ExploreId.left]: {
initialized: true,
containerWidth: 1920,
eventBridge: {} as EventBusExtended,
queries: [] as DataQuery[],
range: testRange,
history: [],
refreshInterval: {
label: 'Off',
value: 0,
},
cache: [],
},
},
};
jest.mock('@grafana/runtime', () => ({
...(jest.requireActual('@grafana/runtime') as unknown as object),
getTemplateSrv: () => ({
updateTimeRange: jest.fn(),
}),
}));
function setupStore(state?: any) {
return configureStore({

View File

@ -0,0 +1,46 @@
import { DefaultTimeZone, toUtc } from '@grafana/data';
import { ExploreId } from '../../../types';
export const createDefaultInitialState = () => {
const t = toUtc();
const testRange = {
from: t,
to: t,
raw: {
from: t,
to: t,
},
};
const defaultInitialState = {
user: {
orgId: '1',
timeZone: DefaultTimeZone,
},
explore: {
[ExploreId.left]: {
datasourceInstance: {
query: jest.fn(),
getRef: jest.fn(),
getLogsVolumeDataProvider: jest.fn(),
meta: {
id: 'something',
},
},
initialized: true,
containerWidth: 1920,
eventBridge: { emit: () => {} } as any,
queries: [{ expr: 'test' }] as any[],
range: testRange,
history: [],
refreshInterval: {
label: 'Off',
value: 0,
},
cache: [],
},
},
};
return { testRange, defaultInitialState };
};

View File

@ -22,12 +22,10 @@ import {
DataSourceApi,
DataSourceJsonData,
DataSourceWithLogsVolumeSupport,
DefaultTimeZone,
LoadingState,
MutableDataFrame,
PanelData,
RawTimeRange,
toUtc,
} from '@grafana/data';
import { thunkTester } from 'test/core/thunk/thunkTester';
import { makeExplorePaneState } from './utils';
@ -35,45 +33,24 @@ import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { configureStore } from '../../../store/configureStore';
import { setTimeSrv } from '../../dashboard/services/TimeSrv';
import Mock = jest.Mock;
import { createDefaultInitialState } from './helpers';
const t = toUtc();
const testRange = {
from: t,
to: t,
raw: {
from: t,
to: t,
},
};
const defaultInitialState = {
user: {
orgId: '1',
timeZone: DefaultTimeZone,
},
explore: {
[ExploreId.left]: {
datasourceInstance: {
query: jest.fn(),
getRef: jest.fn(),
getLogsVolumeDataProvider: jest.fn(),
meta: {
id: 'something',
},
},
initialized: true,
containerWidth: 1920,
eventBridge: { emit: () => {} } as any,
queries: [{ expr: 'test' }] as any[],
range: testRange,
history: [],
refreshInterval: {
label: 'Off',
value: 0,
},
cache: [],
},
},
};
const { testRange, defaultInitialState } = createDefaultInitialState();
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
...jest.requireActual('app/features/dashboard/services/TimeSrv'),
getTimeSrv: () => ({
init: jest.fn(),
timeRange: jest.fn().mockReturnValue({}),
}),
}));
jest.mock('@grafana/runtime', () => ({
...(jest.requireActual('@grafana/runtime') as unknown as object),
getTemplateSrv: () => ({
updateTimeRange: jest.fn(),
}),
}));
function setupQueryResponse(state: StoreState) {
(state.explore[ExploreId.left].datasourceInstance?.query as Mock).mockReturnValueOnce(

View File

@ -3,9 +3,44 @@ import { dateTime, LoadingState } from '@grafana/data';
import { makeExplorePaneState } from './utils';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { reducerTester } from 'test/core/redux/reducerTester';
import { changeRangeAction, changeRefreshIntervalAction, timeReducer } from './time';
import { changeRangeAction, changeRefreshIntervalAction, timeReducer, updateTime } from './time';
import { createDefaultInitialState } from './helpers';
import { configureStore } from 'app/store/configureStore';
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
const MOCK_TIME_RANGE = {};
const mockTimeSrv = {
init: jest.fn(),
timeRange: jest.fn().mockReturnValue(MOCK_TIME_RANGE),
};
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
...jest.requireActual('app/features/dashboard/services/TimeSrv'),
getTimeSrv: () => mockTimeSrv,
}));
const mockTemplateSrv = {
updateTimeRange: jest.fn(),
};
jest.mock('@grafana/runtime', () => ({
...(jest.requireActual('@grafana/runtime') as unknown as object),
getTemplateSrv: () => mockTemplateSrv,
}));
describe('Explore item reducer', () => {
silenceConsoleOutput();
describe('When time is updated', () => {
it('Time service is re-initialized and template service is updated with the new time range', async () => {
const { dispatch } = configureStore({
...(createDefaultInitialState() as any),
});
await dispatch(updateTime({ exploreId: ExploreId.left }));
expect(mockTimeSrv.init).toBeCalled();
expect(mockTemplateSrv.updateTimeRange).toBeCalledWith(MOCK_TIME_RANGE);
});
});
describe('changing refresh intervals', () => {
it("should result in 'streaming' state, when live-tailing is active", () => {
const initialState = makeExplorePaneState();

View File

@ -8,6 +8,7 @@ import {
TimeRange,
} from '@grafana/data';
import { RefreshPicker } from '@grafana/ui';
import { getTemplateSrv } from '@grafana/runtime';
import { getTimeRange, refreshIntervalToSortOrder, stopQueryState } from 'app/core/utils/explore';
import { ExploreItemState, ThunkResult } from 'app/types';
@ -105,7 +106,11 @@ export const updateTime = (config: {
},
};
// We need to re-initialize TimeSrv because it might have been triggered by the other Explore pane (when split)
getTimeSrv().init(timeModel);
// After re-initializing TimeSrv we need to update the time range in Template service for interpolation
// of __from and __to variables
getTemplateSrv().updateTimeRange(getTimeSrv().timeRange());
dispatch(changeRangeAction({ exploreId, range, absoluteRange }));
};

View File

@ -25,6 +25,7 @@ describe('getFieldLinksForExplore', () => {
containsTemplate() {
return false;
},
updateTimeRange(timeRange: TimeRange) {},
});
});

View File

@ -1,4 +1,4 @@
import { ScopedVars, VariableModel } from '@grafana/data';
import { ScopedVars, TimeRange, VariableModel } from '@grafana/data';
import { variableRegex } from '../variables/utils';
import { TemplateSrv } from '@grafana/runtime';
@ -55,4 +55,6 @@ export class TemplateSrvMock implements TemplateSrv {
const match = this.regex.exec(target);
return match !== null;
}
updateTimeRange(timeRange: TimeRange) {}
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
import { dateTime } from '@grafana/data';
import { dateTime, TimeRange } from '@grafana/data';
import { setTemplateSrv } from '@grafana/runtime';
import { DebugSection } from './DebugSection';
@ -38,6 +38,7 @@ describe('DebugSection', () => {
containsTemplate() {
return false;
},
updateTimeRange(timeRange: TimeRange) {},
});
});