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) {},
});
});