From bf977ac2456cd8cdd136a992489260c861d8fe03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Tue, 29 Mar 2022 14:10:40 +0100 Subject: [PATCH] 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 --- .betterer.results | 2 +- .../src/services/templateSrv.ts | 7 ++- .../features/explore/spec/helper/setup.tsx | 2 +- .../explore/spec/interpolation.test.tsx | 46 +++++++++++++++ .../explore/state/explorePane.test.ts | 41 ++++--------- public/app/features/explore/state/helpers.ts | 46 +++++++++++++++ .../app/features/explore/state/query.test.ts | 57 ++++++------------- .../app/features/explore/state/time.test.ts | 37 +++++++++++- public/app/features/explore/state/time.ts | 5 ++ .../app/features/explore/utils/links.test.ts | 1 + .../features/templating/template_srv.mock.ts | 4 +- .../loki/configuration/DebugSection.test.tsx | 3 +- 12 files changed, 174 insertions(+), 77 deletions(-) create mode 100644 public/app/features/explore/spec/interpolation.test.tsx create mode 100644 public/app/features/explore/state/helpers.ts diff --git a/.betterer.results b/.betterer.results index abe8fcf708c..6b3ee027eb3 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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": [ diff --git a/packages/grafana-runtime/src/services/templateSrv.ts b/packages/grafana-runtime/src/services/templateSrv.ts index 50b40325c5c..b0ac664e15e 100644 --- a/packages/grafana-runtime/src/services/templateSrv.ts +++ b/packages/grafana-runtime/src/services/templateSrv.ts @@ -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; diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index 1bddee54436..81e292cceac 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -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 = () => { diff --git a/public/app/features/explore/spec/interpolation.test.tsx b/public/app/features/explore/spec/interpolation.test.tsx new file mode 100644 index 00000000000..cfe11ccbc0f --- /dev/null +++ b/public/app/features/explore/spec/interpolation.test.tsx @@ -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
{props.children({ width: 1000 })}
; + }, + }; +}); + +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) => { + 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" }'); + }); +}); diff --git a/public/app/features/explore/state/explorePane.test.ts b/public/app/features/explore/state/explorePane.test.ts index 81f1ef87e6a..e429616a7d6 100644 --- a/public/app/features/explore/state/explorePane.test.ts +++ b/public/app/features/explore/state/explorePane.test.ts @@ -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({ diff --git a/public/app/features/explore/state/helpers.ts b/public/app/features/explore/state/helpers.ts new file mode 100644 index 00000000000..c4c0fc84224 --- /dev/null +++ b/public/app/features/explore/state/helpers.ts @@ -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 }; +}; diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 9aa8bcad2ac..c0d235cfd0d 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -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( diff --git a/public/app/features/explore/state/time.test.ts b/public/app/features/explore/state/time.test.ts index 99b0b73a3e2..513887c8e47 100644 --- a/public/app/features/explore/state/time.test.ts +++ b/public/app/features/explore/state/time.test.ts @@ -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(); diff --git a/public/app/features/explore/state/time.ts b/public/app/features/explore/state/time.ts index 5ab326f4646..32ccf4c3255 100644 --- a/public/app/features/explore/state/time.ts +++ b/public/app/features/explore/state/time.ts @@ -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 })); }; diff --git a/public/app/features/explore/utils/links.test.ts b/public/app/features/explore/utils/links.test.ts index 1f58b54bcbd..c1e071b5c40 100644 --- a/public/app/features/explore/utils/links.test.ts +++ b/public/app/features/explore/utils/links.test.ts @@ -25,6 +25,7 @@ describe('getFieldLinksForExplore', () => { containsTemplate() { return false; }, + updateTimeRange(timeRange: TimeRange) {}, }); }); diff --git a/public/app/features/templating/template_srv.mock.ts b/public/app/features/templating/template_srv.mock.ts index 4ce57cdc914..4d4b4961de0 100644 --- a/public/app/features/templating/template_srv.mock.ts +++ b/public/app/features/templating/template_srv.mock.ts @@ -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) {} } diff --git a/public/app/plugins/datasource/loki/configuration/DebugSection.test.tsx b/public/app/plugins/datasource/loki/configuration/DebugSection.test.tsx index 5005a4f185a..d1aaac6c77a 100644 --- a/public/app/plugins/datasource/loki/configuration/DebugSection.test.tsx +++ b/public/app/plugins/datasource/loki/configuration/DebugSection.test.tsx @@ -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) {}, }); });