diff --git a/packages/grafana-data/src/field/fieldComparers.ts b/packages/grafana-data/src/field/fieldComparers.ts index 374fc707206..32182c74edf 100644 --- a/packages/grafana-data/src/field/fieldComparers.ts +++ b/packages/grafana-data/src/field/fieldComparers.ts @@ -18,6 +18,9 @@ export const fieldIndexComparer = (field: Field, reverse = false): IndexComparer case FieldType.boolean: return booleanIndexComparer(values, reverse); case FieldType.time: + if (typeof field.values.get(0) === 'number') { + return timestampIndexComparer(values, reverse); + } return timeIndexComparer(values, reverse); default: return naturalIndexComparer(reverse); @@ -76,6 +79,12 @@ const falsyComparer = (a: unknown, b: unknown): number => { return 0; }; +const timestampIndexComparer = (values: Vector, reverse: boolean): IndexComparer => { + let vals = values.toArray(); + let mult = reverse ? -1 : 1; + return (a: number, b: number): number => mult * (vals[a] - vals[b]); +}; + const timeIndexComparer = (values: Vector, reverse: boolean): IndexComparer => { return (a: number, b: number): number => { const vA = values.get(a); diff --git a/packages/grafana-data/src/transformations/transformers/convertFieldType.test.ts b/packages/grafana-data/src/transformations/transformers/convertFieldType.test.ts index 96f55445c8f..64cd6e05d6d 100644 --- a/packages/grafana-data/src/transformations/transformers/convertFieldType.test.ts +++ b/packages/grafana-data/src/transformations/transformers/convertFieldType.test.ts @@ -391,12 +391,12 @@ describe('fieldToTimeField', () => { // this needs to run in a non-UTC timezone env to ensure the parsing is not dependent on env tz settings //process.env.TZ = 'Pacific/Easter'; - it('should always parse ISO 8601 date strings in UTC timezone (e.g. 2011-10-05T14:48:00.000Z)', () => { + it('should properly parse ISO 8601 date strings in UTC offset timezone', () => { const stringTimeField: Field = { config: {}, name: 'ISO 8601 date strings', type: FieldType.time, - values: new ArrayVector(['2021-11-11T19:45:00.000Z']), + values: new ArrayVector(['2021-11-11T19:45:00Z']), }; expect(fieldToTimeField(stringTimeField)).toEqual({ @@ -406,4 +406,32 @@ describe('fieldToTimeField', () => { values: new ArrayVector([1636659900000]), }); }); + + it('should properly parse additional ISO 8601 date strings with tz offsets and millis', () => { + const stringTimeField: Field = { + config: {}, + name: 'ISO 8601 date strings', + type: FieldType.time, + values: new ArrayVector([ + '2021-11-11T19:45:00+05:30', + '2021-11-11T19:45:00-05:30', + '2021-11-11T19:45:00+0530', + '2021-11-11T19:45:00-0530', + '2021-11-11T19:45:00.0000000000+05:30', + '2021-11-11T19:45:00.0000000000-0530', + '2021-11-11T19:45:00.000Z', + '2021-11-11T19:45:00.0000000000Z', + ]), + }; + + expect(fieldToTimeField(stringTimeField)).toEqual({ + config: {}, + name: 'ISO 8601 date strings', + type: FieldType.time, + values: new ArrayVector([ + 1636640100000, 1636679700000, 1636640100000, 1636679700000, 1636640100000, 1636679700000, 1636659900000, + 1636659900000, + ]), + }); + }); }); diff --git a/packages/grafana-data/src/transformations/transformers/convertFieldType.ts b/packages/grafana-data/src/transformations/transformers/convertFieldType.ts index 13d6db0d7c2..25b9d04076b 100644 --- a/packages/grafana-data/src/transformations/transformers/convertFieldType.ts +++ b/packages/grafana-data/src/transformations/transformers/convertFieldType.ts @@ -109,8 +109,8 @@ export function convertFieldType(field: Field, opts: ConvertFieldTypeOptions): F } } -// matches ISO 8601, e.g. 2021-11-11T19:45:00.000Z (float portion optional) -const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/; +// matches common ISO 8601 (see tests) +const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3,})?(?:Z|[-+]\d{2}:?\d{2})$/; /** * @internal diff --git a/public/app/plugins/panel/timeseries/utils.ts b/public/app/plugins/panel/timeseries/utils.ts index f6a1219556d..9a1dc00f449 100644 --- a/public/app/plugins/panel/timeseries/utils.ts +++ b/public/app/plugins/panel/timeseries/utils.ts @@ -12,6 +12,7 @@ import { SortedVector, TimeRange, } from '@grafana/data'; +import { convertFieldType } from '@grafana/data/src/transformations/transformers/convertFieldType'; import { GraphFieldConfig, LineInterpolation } from '@grafana/schema'; import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold'; import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue'; @@ -29,6 +30,17 @@ export function prepareGraphableFields( return null; } + // some datasources simply tag the field as time, but don't convert to milli epochs + // so we're stuck with doing the parsing here to avoid Moment slowness everywhere later + // this mutates (once) + for (let frame of series) { + for (let field of frame.fields) { + if (field.type === FieldType.time && typeof field.values.get(0) !== 'number') { + field.values = convertFieldType(field, { destinationType: FieldType.time }).values; + } + } + } + if (series.every((df) => df.meta?.type === DataFrameType.TimeSeriesLong)) { series = prepareTimeSeriesLong(series); }