diff --git a/.betterer.results b/.betterer.results index 5b05a2b46c1..35f20e39fac 100644 --- a/.betterer.results +++ b/.betterer.results @@ -173,9 +173,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"] + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], "packages/grafana-data/src/field/overrides/processors.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index 86acc680108..fe241167923 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -847,8 +847,8 @@ describe('getLinksSupplier', () => { const links = supplier({ valueRowIndex: 0 }); const rangeStr = JSON.stringify({ - from: range.from.toISOString(), - to: range.to.toISOString(), + from: range.from.valueOf().toString(), + to: range.to.valueOf().toString(), }); const encodeURIParams = `{"range":${rangeStr},"datasource":"${datasourceUid}","queries":["12345"]}`; expect(links.length).toBe(1); diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index bc2ceece08a..d653001541a 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -400,7 +400,7 @@ const defaultInternalLinkPostProcessor: DataLinkPostProcessor = (options) => { internalLink: link.internal, scopedVars: dataLinkScopedVars, field, - range: link.internal.range ?? ({} as any), + range: link.internal.range, replaceVariables, }); } else { diff --git a/packages/grafana-data/src/types/explore.ts b/packages/grafana-data/src/types/explore.ts index d5a837e38ed..eecf02813e6 100644 --- a/packages/grafana-data/src/types/explore.ts +++ b/packages/grafana-data/src/types/explore.ts @@ -1,15 +1,28 @@ import { DataQuery } from '@grafana/schema'; import { PreferredVisualisationType } from './data'; -import { RawTimeRange, TimeRange } from './time'; +import { TimeRange } from './time'; type AnyQuery = DataQuery & Record; +// enforce type-incompatibility with RawTimeRange to ensure it's parsed and converted. +// URLRangeValue may be a string representing UTC time in ms, which is not a compatible +// value for RawTimeRange when used as a string (it could only be an ISO formatted date) +export type URLRangeValue = string | { __brand: 'URL Range Value' }; + +/** + * @internal + */ +export type URLRange = { + from: URLRangeValue; + to: URLRangeValue; +}; + /** @internal */ export interface ExploreUrlState { datasource: string | null; queries: T[]; - range: RawTimeRange; + range: URLRange; panelsState?: ExplorePanelsState; } diff --git a/packages/grafana-data/src/utils/dataLinks.test.ts b/packages/grafana-data/src/utils/dataLinks.test.ts index b2b0baca976..31f6e4162b8 100644 --- a/packages/grafana-data/src/utils/dataLinks.test.ts +++ b/packages/grafana-data/src/utils/dataLinks.test.ts @@ -1,7 +1,21 @@ +import { DateTime, toUtc } from '../datetime'; import { DataLink, FieldType, TimeRange } from '../types'; import { mapInternalLinkToExplore } from './dataLinks'; +const createTimeRange = (from: DateTime, to: DateTime): TimeRange => ({ + from, + to, + raw: { + from, + to, + }, +}); + +const DATE_AS_DATE_TIME = toUtc([2000, 1, 1]); +const DATE_AS_MS = '949363200000'; +const TIME_RANGE = createTimeRange(DATE_AS_DATE_TIME, DATE_AS_DATE_TIME); + describe('mapInternalLinkToExplore', () => { it('creates internal link', () => { const dataLink = { @@ -18,7 +32,6 @@ describe('mapInternalLinkToExplore', () => { link: dataLink, internalLink: dataLink.internal, scopedVars: {}, - range: {} as unknown as TimeRange, field: { name: 'test', type: FieldType.number, @@ -59,7 +72,6 @@ describe('mapInternalLinkToExplore', () => { link: dataLink, internalLink: dataLink.internal!, scopedVars: {}, - range: {} as unknown as TimeRange, field: { name: 'test', type: FieldType.number, @@ -106,7 +118,7 @@ describe('mapInternalLinkToExplore', () => { scopedVars: { var1: { text: '', value: 'val1' }, }, - range: {} as unknown as TimeRange, + range: TIME_RANGE, field: { name: 'test', type: FieldType.number, @@ -118,6 +130,10 @@ describe('mapInternalLinkToExplore', () => { expect(decodeURIComponent(link.href)).toEqual( `/explore?left=${JSON.stringify({ + range: { + from: DATE_AS_MS, + to: DATE_AS_MS, + }, datasource: 'uid', queries: [ { diff --git a/packages/grafana-data/src/utils/dataLinks.ts b/packages/grafana-data/src/utils/dataLinks.ts index e483ab427d8..130c8682055 100644 --- a/packages/grafana-data/src/utils/dataLinks.ts +++ b/packages/grafana-data/src/utils/dataLinks.ts @@ -12,7 +12,7 @@ import { } from '../types'; import { locationUtil } from './location'; -import { serializeStateToUrlParam } from './url'; +import { serializeStateToUrlParam, toURLRange } from './url'; export const DataLinkBuiltInVars = { keepTime: '__url_time_range', @@ -33,7 +33,7 @@ export const DataLinkBuiltInVars = { export type LinkToExploreOptions = { link: DataLink; scopedVars: ScopedVars; - range: TimeRange; + range?: TimeRange; field: Field; internalLink: InternalDataLink; onClickFn?: SplitOpen; @@ -77,13 +77,16 @@ export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkMod function generateInternalHref( datasourceUid: string, query: T, - range: TimeRange, + range?: TimeRange, panelsState?: ExplorePanelsState ): string { return locationUtil.assureBaseUrl( `/explore?left=${encodeURIComponent( serializeStateToUrlParam({ - range: range.raw, + // @deprecated mapInternalLinkToExplore required passing range. Some consumers to generate the URL + // with defaults pass range as `{} as any`. This is why we need to check for `range?.raw` not just + // `range ? ...` here. This behavior will be marked as deprecated in #72498 + ...(range?.raw ? { range: toURLRange(range.raw) } : {}), datasource: datasourceUid, queries: [query], panelsState: panelsState, diff --git a/packages/grafana-data/src/utils/index.ts b/packages/grafana-data/src/utils/index.ts index 45926b72558..81c3ae68186 100644 --- a/packages/grafana-data/src/utils/index.ts +++ b/packages/grafana-data/src/utils/index.ts @@ -16,7 +16,7 @@ export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUI export { arrayUtils }; export { getFlotPairs, getFlotPairsConstant } from './flotPairs'; export { locationUtil } from './location'; -export { urlUtil, type UrlQueryMap, type UrlQueryValue, serializeStateToUrlParam } from './url'; +export { urlUtil, type UrlQueryMap, type UrlQueryValue, serializeStateToUrlParam, toURLRange } from './url'; export { DataLinkBuiltInVars, mapInternalLinkToExplore } from './dataLinks'; export { DocsId } from './docs'; export { makeClassES5Compatible } from './makeClassES5Compatible'; diff --git a/packages/grafana-data/src/utils/url.ts b/packages/grafana-data/src/utils/url.ts index 71f80b8c4d2..f3f6449e0d4 100644 --- a/packages/grafana-data/src/utils/url.ts +++ b/packages/grafana-data/src/utils/url.ts @@ -2,6 +2,8 @@ * @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT */ +import { isDateTime } from '../datetime'; +import { URLRange, RawTimeRange } from '../types'; import { ExploreUrlState } from '../types/explore'; /** @@ -200,14 +202,37 @@ export const urlUtil = { /** * Create an string that is used in URL to represent the Explore state. This is basically just a stringified json - * that is that used as a state of a single Explore pane so it does not represent full Explore URL. + * that is used as a state of a single Explore pane so it does not represent full Explore URL so some properties + * may be omitted (they will be filled in with default values). * * @param urlState * @param compact this parameter is deprecated and will be removed in a future release. */ -export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { +export function serializeStateToUrlParam(urlState: Partial, compact?: boolean): string { if (compact !== undefined) { console.warn('`compact` parameter is deprecated and will be removed in a future release'); } return JSON.stringify(urlState); } + +/** + * Converts RawTimeRange to a string that is stored in the URL + * - relative - stays as it is (e.g. "now") + * - absolute - converted to ms + */ +export const toURLRange = (range: RawTimeRange): URLRange => { + let from = range.from; + if (isDateTime(from)) { + from = from.valueOf().toString(); + } + + let to = range.to; + if (isDateTime(to)) { + to = to.valueOf().toString(); + } + + return { + from, + to, + }; +}; diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 9e2156bb41b..ccd337b8a27 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -87,7 +87,7 @@ describe('getExploreUrl', () => { getDataSourceById: jest.fn(), }, timeSrv: { - timeRangeForUrl: () => '1', + timeRange: () => ({ raw: { from: 'now-1h', to: 'now' } }), }, } as unknown as GetExploreUrlArguments; diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index df89672f9ca..438ab5a8298 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -19,6 +19,7 @@ import { RawTimeRange, TimeRange, TimeZone, + toURLRange, urlUtil, } from '@grafana/data'; import { DataSourceSrv, getDataSourceSrv } from '@grafana/runtime'; @@ -76,8 +77,8 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise = { range }; + const range = timeSrv.timeRange().raw; + let state: Partial = { range: toURLRange(range) }; if (exploreDatasource.interpolateVariablesInQueries) { const scopedVars = panel.scopedVars || {}; state = { diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index 6e88a1e978e..e4517360b16 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -61,7 +61,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\", service_namespace=\\"namespace1\\"}","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"loki1_uid","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\", service_namespace=\\"namespace1\\"}","refId":""}]}' )}` ); }); @@ -87,7 +87,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{ip=\\"192.168.0.1\\"}","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"loki1_uid","queries":[{"expr":"{ip=\\"192.168.0.1\\"}","refId":""}]}' )}` ); }); @@ -113,34 +113,35 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{ip=\\"192.168.0.1\\", host=\\"host\\"}","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"loki1_uid","queries":[{"expr":"{ip=\\"192.168.0.1\\", host=\\"host\\"}","refId":""}]}' )}` ); }); it('with adjusted start and end time', () => { const createLink = setupSpanLinkFactory({ - spanStartTimeShift: '1m', + spanStartTimeShift: '-1m', spanEndTimeShift: '1m', }); expect(createLink).toBeDefined(); - const links = createLink!( - createTraceSpan({ - process: { - serviceName: 'service', - tags: [ - { key: 'hostname', value: 'hostname1' }, - { key: 'ip', value: '192.168.0.1' }, - ], - }, - }) - ); + const span = createTraceSpan({ + process: { + serviceName: 'service', + tags: [ + { key: 'hostname', value: 'hostname1' }, + { key: 'ip', value: '192.168.0.1' }, + ], + }, + }); + const links = createLink!(span); const linkDef = links?.[0]; expect(linkDef).toBeDefined(); expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:01:00.000Z","to":"2020-10-14T01:01:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{hostname=\\"hostname1\\"}","refId":""}]}' + `{"range":{"from":"${span.startTime / 1000 - 60000}","to":"${ + span.startTime / 1000 + span.duration / 1000 + 60000 + }"},"datasource":"loki1_uid","queries":[{"expr":"{hostname=\\"hostname1\\"}","refId":""}]}` )}` ); }); @@ -159,7 +160,7 @@ describe('createSpanLinkFactory', () => { expect(decodeURIComponent(linkDef!.href)).toBe( '/explore?left=' + JSON.stringify({ - range: { from: '2020-10-14T01:00:00.000Z', to: '2020-10-14T01:00:01.000Z' }, + range: { from: '1602637200000', to: '1602637201000' }, datasource: 'loki1_uid', queries: [ { @@ -221,7 +222,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{service=\\"serviceName\\", pod=\\"podName\\"}","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"loki1_uid","queries":[{"expr":"{service=\\"serviceName\\", pod=\\"podName\\"}","refId":""}]}' )}` ); }); @@ -251,7 +252,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{service.name=\\"serviceName\\", pod=\\"podName\\"}","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"loki1_uid","queries":[{"expr":"{service.name=\\"serviceName\\", pod=\\"podName\\"}","refId":""}]}' )}` ); }); @@ -326,10 +327,10 @@ describe('createSpanLinkFactory', () => { expect(linkDef).toBeDefined(); expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toContain( - `${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}` + `${encodeURIComponent('{"range":{"from":"1602637200000","to":"1602637201000"}')}` ); expect(linkDef!.href).not.toContain( - `${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:00.000Z"}')}` + `${encodeURIComponent('{"range":{"from":"1602637200000","to":"1602637200000"}')}` ); }); @@ -348,7 +349,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" service_namespace=\\"namespace1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"splunkUID","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" service_namespace=\\"namespace1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}]}' )}` ); }); @@ -372,7 +373,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"ip=\\"192.168.0.1\\"","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"splunkUID","queries":[{"query":"ip=\\"192.168.0.1\\"","refId":""}]}' )}` ); }); @@ -399,7 +400,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"hostname=\\"hostname1\\" ip=\\"192.168.0.1\\"","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"splunkUID","queries":[{"query":"hostname=\\"hostname1\\" ip=\\"192.168.0.1\\"","refId":""}]}' )}` ); }); @@ -429,7 +430,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"service=\\"serviceName\\" pod=\\"podName\\"","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"splunkUID","queries":[{"query":"service=\\"serviceName\\" pod=\\"podName\\"","refId":""}]}' )}` ); }); @@ -466,7 +467,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Metrics); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T00:58:00.000Z","to":"2020-10-14T01:02:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}]}' + '{"range":{"from":"1602637080000","to":"1602637321000"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}]}' )}` ); }); @@ -515,7 +516,7 @@ describe('createSpanLinkFactory', () => { expect(namedLink!.title).toBe('Named Query'); expect(namedLink!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T00:58:00.000Z","to":"2020-10-14T01:02:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}]}' + '{"range":{"from":"1602637080000","to":"1602637321000"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}]}' )}` ); @@ -525,7 +526,7 @@ describe('createSpanLinkFactory', () => { expect(defaultLink!.title).toBe('defaultQuery'); expect(defaultLink!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T00:58:00.000Z","to":"2020-10-14T01:02:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"histogram_quantile(0.5, sum(rate(traces_spanmetrics_latency_bucket{service=\\"test service\\"}[5m])) by (le))","refId":"A"}]}' + '{"range":{"from":"1602637080000","to":"1602637321000"},"datasource":"prom1Uid","queries":[{"expr":"histogram_quantile(0.5, sum(rate(traces_spanmetrics_latency_bucket{service=\\"test service\\"}[5m])) by (le))","refId":"A"}]}' )}` ); @@ -535,7 +536,7 @@ describe('createSpanLinkFactory', () => { expect(unnamedQuery!.title).toBeUndefined(); expect(unnamedQuery!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T00:58:00.000Z","to":"2020-10-14T01:02:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"no_name_here","refId":"A"}]}' + '{"range":{"from":"1602637080000","to":"1602637321000"},"datasource":"prom1Uid","queries":[{"expr":"no_name_here","refId":"A"}]}' )}` ); }); @@ -561,7 +562,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Metrics); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T00:00:00.000Z","to":"2020-10-14T02:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}]}' + '{"range":{"from":"1602633600000","to":"1602640801000"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}]}' )}` ); }); @@ -599,7 +600,7 @@ describe('createSpanLinkFactory', () => { expect(links![0].type).toBe(SpanLinkType.Metrics); expect(links![0].href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T00:58:00.000Z","to":"2020-10-14T01:02:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"metric{job=\\"tns/app\\", pod=\\"sample-pod\\", job=\\"tns/app\\", pod=\\"sample-pod\\"}[5m]","refId":"A"}]}' + '{"range":{"from":"1602637080000","to":"1602637321000"},"datasource":"prom1Uid","queries":[{"expr":"metric{job=\\"tns/app\\", pod=\\"sample-pod\\", job=\\"tns/app\\", pod=\\"sample-pod\\"}[5m]","refId":"A"}]}' )}` ); }); @@ -703,10 +704,10 @@ describe('createSpanLinkFactory', () => { expect(linkDef).toBeDefined(); expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toContain( - `${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}` + `${encodeURIComponent('{"range":{"from":"1602637200000","to":"1602637201000"}')}` ); expect(linkDef!.href).not.toContain( - `${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:00.000Z"}')}` + `${encodeURIComponent('{"range":{"from":"1602637200000","to":"1602637200000"}')}` ); }); @@ -728,7 +729,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster:\\"cluster1\\" AND hostname:\\"hostname1\\" AND service_namespace:\\"namespace1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` + `{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster:\\"cluster1\\" AND hostname:\\"hostname1\\" AND service_namespace:\\"namespace1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` )}` ); }); @@ -756,7 +757,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef).toBeDefined(); expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toBe( - `/explore?left={"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"searchUID","queries":[{"query":"\\"7946b05c2e2e4e5a\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` + `/explore?left={"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"searchUID","queries":[{"query":"\\"7946b05c2e2e4e5a\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` ); }); @@ -782,7 +783,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"ip:\\"192.168.0.1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` + `{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"${searchUID}","queries":[{"query":"ip:\\"192.168.0.1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` )}` ); }); @@ -812,7 +813,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"hostname:\\"hostname1\\" AND ip:\\"192.168.0.1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` + `{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"${searchUID}","queries":[{"query":"hostname:\\"hostname1\\" AND ip:\\"192.168.0.1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` )}` ); }); @@ -845,7 +846,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"service:\\"serviceName\\" AND pod:\\"podName\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` + `{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"${searchUID}","queries":[{"query":"service:\\"serviceName\\" AND pod:\\"podName\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` )}` ); }); @@ -893,10 +894,10 @@ describe('createSpanLinkFactory', () => { expect(linkDef).toBeDefined(); expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toContain( - `${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}` + `${encodeURIComponent('{"range":{"from":"1602637200000","to":"1602637201000"}')}` ); expect(linkDef!.href).not.toContain( - `${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:00.000Z"}')}` + `${encodeURIComponent('{"range":{"from":"1602637200000","to":"1602637200000"}')}` ); }); @@ -918,7 +919,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster=\\"cluster1\\" AND hostname=\\"hostname1\\" AND service_namespace=\\"namespace1\\"","refId":""}]}` + `{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster=\\"cluster1\\" AND hostname=\\"hostname1\\" AND service_namespace=\\"namespace1\\"","refId":""}]}` )}` ); }); @@ -946,7 +947,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef).toBeDefined(); expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toBe( - `/explore?left={"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"searchUID","queries":[{"query":"\\"7946b05c2e2e4e5a\\"","refId":""}]}` + `/explore?left={"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"searchUID","queries":[{"query":"\\"7946b05c2e2e4e5a\\"","refId":""}]}` ); }); @@ -972,7 +973,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"ip=\\"192.168.0.1\\"","refId":""}]}` + `{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"${searchUID}","queries":[{"query":"ip=\\"192.168.0.1\\"","refId":""}]}` )}` ); }); @@ -1002,7 +1003,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"hostname=\\"hostname1\\" AND ip=\\"192.168.0.1\\"","refId":""}]}` + `{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"${searchUID}","queries":[{"query":"hostname=\\"hostname1\\" AND ip=\\"192.168.0.1\\"","refId":""}]}` )}` ); }); @@ -1035,7 +1036,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"service=\\"serviceName\\" AND pod=\\"podName\\"","refId":""}]}` + `{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"${searchUID}","queries":[{"query":"service=\\"serviceName\\" AND pod=\\"podName\\"","refId":""}]}` )}` ); }); @@ -1142,7 +1143,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"cluster=\\"cluster1\\" OR hostname=\\"hostname1\\" OR service_namespace=\\"namespace1\\" or \\"7946b05c2e2e4e5a\\" or \\"6605c7b08e715d6c\\"","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"falconLogScaleUID","queries":[{"lsql":"cluster=\\"cluster1\\" OR hostname=\\"hostname1\\" OR service_namespace=\\"namespace1\\" or \\"7946b05c2e2e4e5a\\" or \\"6605c7b08e715d6c\\"","refId":""}]}' )}` ); }); @@ -1166,7 +1167,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"ip=\\"192.168.0.1\\"","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"falconLogScaleUID","queries":[{"lsql":"ip=\\"192.168.0.1\\"","refId":""}]}' )}` ); }); @@ -1193,7 +1194,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"hostname=\\"hostname1\\" OR ip=\\"192.168.0.1\\"","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"falconLogScaleUID","queries":[{"lsql":"hostname=\\"hostname1\\" OR ip=\\"192.168.0.1\\"","refId":""}]}' )}` ); }); @@ -1223,7 +1224,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"service=\\"serviceName\\" OR pod=\\"podName\\"","refId":""}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"falconLogScaleUID","queries":[{"lsql":"service=\\"serviceName\\" OR pod=\\"podName\\"","refId":""}]}' )}` ); }); @@ -1258,13 +1259,13 @@ describe('dataFrame links', () => { expect(links![0].type).toBe(SpanLinkType.Unknown); expect(links![1].href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"message":"SELECT * FROM superhero WHERE name=host"}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"loki1_uid","queries":[{"message":"SELECT * FROM superhero WHERE name=host"}]}' )}` ); expect(links![1].type).toBe(SpanLinkType.Unknown); expect(links![2].href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"go_memstats_heap_inuse_bytes{job=\'host\'}"}]}' + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"loki1_uid","queries":[{"expr":"go_memstats_heap_inuse_bytes{job=\'host\'}"}]}' )}` ); expect(links![2].type).toBe(SpanLinkType.Unknown); diff --git a/public/app/features/explore/hooks/useStateSync/index.ts b/public/app/features/explore/hooks/useStateSync/index.ts index b06fbea83a1..78575430e39 100644 --- a/public/app/features/explore/hooks/useStateSync/index.ts +++ b/public/app/features/explore/hooks/useStateSync/index.ts @@ -1,7 +1,7 @@ import { identity, isEmpty, isEqual, isObject, mapValues, omitBy } from 'lodash'; import { useEffect, useRef } from 'react'; -import { CoreApp, ExploreUrlState, RawTimeRange, DataSourceApi } from '@grafana/data'; +import { CoreApp, ExploreUrlState, DataSourceApi, toURLRange } from '@grafana/data'; import { DataQuery, DataSourceRef } from '@grafana/schema'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { clearQueryKeys, getLastUsedDatasourceUID } from 'app/core/utils/explore'; @@ -15,7 +15,7 @@ import { clearPanes, splitClose, splitOpen, syncTimesAction } from '../../state/ import { runQueries, setQueriesAction } from '../../state/query'; import { selectPanes } from '../../state/selectors'; import { changeRangeAction, updateTime } from '../../state/time'; -import { DEFAULT_RANGE, fromURLRange, toURLTimeRange } from '../../state/utils'; +import { DEFAULT_RANGE, fromURLRange } from '../../state/utils'; import { withUniqueRefIds } from '../../utils/queries'; import { isFulfilled } from '../utils'; @@ -139,7 +139,7 @@ export function useStateSync(params: ExploreQueryParams) { exploreId, datasource: datasource || '', queries: withUniqueRefIds(queries), - range, + range: fromURLRange(range), panelsState, position: i, }) @@ -207,7 +207,7 @@ export function useStateSync(params: ExploreQueryParams) { exploreId, datasource, queries, - range, + range: fromURLRange(range), panelsState, }) ).unwrap(); @@ -358,7 +358,7 @@ const urlDiff = ( } => { const datasource = !isEqual(currentUrlState?.datasource, oldUrlState?.datasource); const queries = !isEqual(currentUrlState?.queries, oldUrlState?.queries); - const range = !areRangesEqual(currentUrlState?.range || DEFAULT_RANGE, oldUrlState?.range || DEFAULT_RANGE); + const range = !isEqual(currentUrlState?.range || DEFAULT_RANGE, oldUrlState?.range || DEFAULT_RANGE); const panelsState = !isEqual(currentUrlState?.panelsState, oldUrlState?.panelsState); return { @@ -369,20 +369,13 @@ const urlDiff = ( }; }; -const areRangesEqual = (a: RawTimeRange, b: RawTimeRange): boolean => { - const parsedA = toURLTimeRange(a); - const parsedB = toURLTimeRange(b); - - return parsedA.from === parsedB.from && parsedA.to === parsedB.to; -}; - export function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState { return { // datasourceInstance should not be undefined anymore here but in case there is some path for it to be undefined // lets just fallback instead of crashing. datasource: pane.datasourceInstance?.uid || '', queries: pane.queries.map(clearQueryKeys), - range: toURLTimeRange(pane.range.raw), + range: toURLRange(pane.range.raw), // don't include panelsState in the url unless a piece of state is actually set panelsState: pruneObject(pane.panelsState), }; diff --git a/public/app/features/explore/state/utils.test.ts b/public/app/features/explore/state/utils.test.ts index 74e568e8420..e58c1fe4b43 100644 --- a/public/app/features/explore/state/utils.test.ts +++ b/public/app/features/explore/state/utils.test.ts @@ -8,7 +8,7 @@ jest.mock('app/features/plugins/datasource_srv', () => ({ getDatasourceSrv: jest.fn(() => dataSourceMock), })); -import { loadAndInitDatasource, getRange } from './utils'; +import { loadAndInitDatasource, getRange, fromURLRange } from './utils'; const DEFAULT_DATASOURCE = { uid: 'abc123', name: 'Default' }; const TEST_DATASOURCE = { uid: 'def789', name: 'Test' }; @@ -58,17 +58,17 @@ describe('getRange', () => { const result = getRange(range, 'browser'); expect(result.raw).toEqual(range); }); +}); +describe('fromURLRange', () => { it('should parse epoch strings', () => { const range = { from: dateTime('2020-10-22T10:00:00Z').valueOf().toString(), to: dateTime('2020-10-22T11:00:00Z').valueOf().toString(), }; - const result = getRange(range, 'browser'); + const result = fromURLRange(range); expect(result.from.valueOf()).toEqual(dateTime('2020-10-22T10:00:00Z').valueOf()); expect(result.to.valueOf()).toEqual(dateTime('2020-10-22T11:00:00Z').valueOf()); - expect(result.raw.from.valueOf()).toEqual(dateTime('2020-10-22T10:00:00Z').valueOf()); - expect(result.raw.to.valueOf()).toEqual(dateTime('2020-10-22T11:00:00Z').valueOf()); }); it('should parse ISO strings', () => { @@ -76,10 +76,8 @@ describe('getRange', () => { from: dateTime('2020-10-22T10:00:00Z').toISOString(), to: dateTime('2020-10-22T11:00:00Z').toISOString(), }; - const result = getRange(range, 'browser'); + const result = fromURLRange(range); expect(result.from.valueOf()).toEqual(dateTime('2020-10-22T10:00:00Z').valueOf()); expect(result.to.valueOf()).toEqual(dateTime('2020-10-22T11:00:00Z').valueOf()); - expect(result.raw.from.valueOf()).toEqual(dateTime('2020-10-22T10:00:00Z').valueOf()); - expect(result.raw.to.valueOf()).toEqual(dateTime('2020-10-22T11:00:00Z').valueOf()); }); }); diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 6ba81adbe9e..661b8fd973e 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -16,6 +16,8 @@ import { DateTime, isDateTime, toUtc, + URLRange, + URLRangeValue, } from '@grafana/data'; import { DataQuery, DataSourceRef, TimeZone } from '@grafana/schema'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; @@ -144,12 +146,7 @@ export function getResultsFromCache( return cacheValue; } -export function getRange(range: RawTimeRange, timeZone: TimeZone): TimeRange { - const raw = { - from: parseRawTime(range.from)!, - to: parseRawTime(range.to)!, - }; - +export function getRange(raw: RawTimeRange, timeZone: TimeZone): TimeRange { return { from: dateMath.parse(raw.from, false, timeZone)!, to: dateMath.parse(raw.to, true, timeZone)!, @@ -157,10 +154,7 @@ export function getRange(range: RawTimeRange, timeZone: TimeZone): TimeRange { }; } -/** - * @param range RawTimeRange - Note: Range in the URL is not RawTimeRange compliant (see #72578 for more details) - */ -export function fromURLRange(range: RawTimeRange): RawTimeRange { +export function fromURLRange(range: URLRange): RawTimeRange { let rawTimeRange: RawTimeRange = DEFAULT_RANGE; let parsedRange = { from: parseRawTime(range.from), @@ -172,35 +166,22 @@ export function fromURLRange(range: RawTimeRange): RawTimeRange { return rawTimeRange; } -/** - * @param range RawTimeRange - Note: Range in the URL is not RawTimeRange compliant (see #72578 for more details) - */ -export const toURLTimeRange = (range: RawTimeRange): RawTimeRange => { - let from = range.from; - if (isDateTime(from)) { - from = from.valueOf().toString(); - } - - let to = range.to; - if (isDateTime(to)) { - to = to.valueOf().toString(); - } - - return { - from, - to, - }; -}; - -function parseRawTime(value: string | DateTime): TimeFragment | null { - if (value === null) { +function parseRawTime(urlRangeValue: URLRangeValue | DateTime): TimeFragment | null { + if (urlRangeValue === null) { return null; } - if (isDateTime(value)) { - return value; + if (isDateTime(urlRangeValue)) { + return urlRangeValue; } + if (typeof urlRangeValue !== 'string') { + return null; + } + + // it can only be a string now + const value = urlRangeValue; + if (value.indexOf('now') !== -1) { return value; }