From 1a34cf6b0ec0cbb9e22cce95e07091ebce11cb5b Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Fri, 13 Oct 2023 01:20:00 -0500 Subject: [PATCH] Transformations: Add support for setting timezone in Format time and Convert field type transformations (#76316) * Better TZ support. * Clean things up * Update convertFieldType * Update tooltips * Remove unecessary code * Prettier * Add internal TZs * Prettier * Fix tests * Make sure tests expect time with timezone * Use true lol * Centralize logic for options --- .../transformers/convertFieldType.ts | 28 ++++++--- .../transformers/formatTime.test.ts | 30 +++++----- .../transformers/formatTime.ts | 59 +++++++------------ .../ConvertFieldTypeTransformerEditor.tsx | 45 +++++++++++--- .../editors/FormatTimeTransformerEditor.tsx | 29 +++++---- public/app/features/transformers/utils.ts | 22 ++++++- 6 files changed, 132 insertions(+), 81 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/convertFieldType.ts b/packages/grafana-data/src/transformations/transformers/convertFieldType.ts index 5225d77d4f0..f4ef713c709 100644 --- a/packages/grafana-data/src/transformations/transformers/convertFieldType.ts +++ b/packages/grafana-data/src/transformations/transformers/convertFieldType.ts @@ -1,6 +1,8 @@ import { map } from 'rxjs/operators'; -import { dateTimeParse } from '../../datetime'; +import { TimeZone } from '@grafana/schema'; + +import { DateTimeOptionsWhenParsing, dateTimeParse } from '../../datetime'; import { SynchronousDataTransformerInfo } from '../../types'; import { DataFrame, EnumFieldConfig, Field, FieldType } from '../../types/dataFrame'; import { fieldMatchers } from '../matchers'; @@ -25,8 +27,13 @@ export interface ConvertFieldTypeOptions { * Date format to parse a string datetime */ dateFormat?: string; - - /** When converting to an enumeration, this is the target config */ + /** + * When converting a date to a string an option timezone. + */ + timezone?: TimeZone; + /** + * When converting to an enumeration, this is the target config + */ enumConfig?: EnumFieldConfig; } @@ -36,7 +43,7 @@ export const convertFieldTypeTransformer: SynchronousDataTransformerInfo (source) => @@ -96,7 +103,7 @@ export function convertFieldType(field: Field, opts: ConvertFieldTypeOptions): F case FieldType.number: return fieldToNumberField(field); case FieldType.string: - return fieldToStringField(field, opts.dateFormat); + return fieldToStringField(field, opts.dateFormat, { timeZone: opts.timezone }); case FieldType.boolean: return fieldToBooleanField(field); case FieldType.enum: @@ -179,12 +186,19 @@ function fieldToBooleanField(field: Field): Field { }; } -function fieldToStringField(field: Field, dateFormat?: string): Field { +/** + * @internal + */ +export function fieldToStringField( + field: Field, + dateFormat?: string, + parseOptions?: DateTimeOptionsWhenParsing +): Field { let values = field.values; switch (field.type) { case FieldType.time: - values = values.map((v) => dateTimeParse(v).format(dateFormat)); + values = values.map((v) => dateTimeParse(v, parseOptions).format(dateFormat)); break; case FieldType.other: diff --git a/packages/grafana-data/src/transformations/transformers/formatTime.test.ts b/packages/grafana-data/src/transformations/transformers/formatTime.test.ts index 5a63b53bfe7..bf3700418a3 100644 --- a/packages/grafana-data/src/transformations/transformers/formatTime.test.ts +++ b/packages/grafana-data/src/transformations/transformers/formatTime.test.ts @@ -13,10 +13,10 @@ describe('Format Time Transformer', () => { const options = { timeField: 'time', outputFormat: 'YYYY-MM', - useTimezone: false, + timezone: 'utc', }; - const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.timezone); const frame = toDataFrame({ fields: [ { @@ -35,10 +35,10 @@ describe('Format Time Transformer', () => { const options = { timeField: 'time', outputFormat: 'YYYY-MM h:mm:ss a', - useTimezone: false, + timezone: 'utc', }; - const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.timezone); const frame = toDataFrame({ fields: [ { @@ -51,11 +51,11 @@ describe('Format Time Transformer', () => { const newFrame = formatter(frame.fields); expect(newFrame[0].values).toEqual([ - '2021-02 1:46:40 am', - '2023-07 2:00:00 pm', - '2023-04 3:20:00 pm', - '2023-07 5:34:49 pm', - '2023-08 3:20:00 pm', + '2021-02 6:46:40 am', + '2023-07 8:00:00 pm', + '2023-04 9:20:00 pm', + '2023-07 11:34:49 pm', + '2023-08 9:20:00 pm', ]); }); @@ -63,10 +63,10 @@ describe('Format Time Transformer', () => { const options = { timeField: 'time', outputFormat: 'YYYY-MM h:mm:ss a', - useTimezone: false, + timezone: 'utc', }; - const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.timezone); const frame = toDataFrame({ fields: [ { @@ -79,10 +79,10 @@ describe('Format Time Transformer', () => { const newFrame = formatter(frame.fields); expect(newFrame[0].values).toEqual([ - '2021-02 1:46:40 am', - '2023-07 2:00:00 pm', - '2023-04 3:20:00 pm', - '2023-07 5:34:49 pm', + '2021-02 6:46:40 am', + '2023-07 8:00:00 pm', + '2023-04 9:20:00 pm', + '2023-07 11:34:49 pm', 'Invalid date', ]); }); diff --git a/packages/grafana-data/src/transformations/transformers/formatTime.ts b/packages/grafana-data/src/transformations/transformers/formatTime.ts index 66406f0ef48..662fcf153fa 100644 --- a/packages/grafana-data/src/transformations/transformers/formatTime.ts +++ b/packages/grafana-data/src/transformations/transformers/formatTime.ts @@ -1,16 +1,17 @@ -import moment from 'moment-timezone'; import { map } from 'rxjs/operators'; -import { getTimeZone, getTimeZoneInfo } from '../../datetime'; -import { Field, FieldType } from '../../types'; +import { TimeZone } from '@grafana/schema'; + +import { Field } from '../../types'; import { DataTransformerInfo } from '../../types/transformations'; +import { fieldToStringField } from './convertFieldType'; import { DataTransformerID } from './ids'; export interface FormatTimeTransformerOptions { timeField: string; outputFormat: string; - useTimezone: boolean; + timezone: TimeZone; } export const formatTimeTransformer: DataTransformerInfo = { @@ -23,7 +24,7 @@ export const formatTimeTransformer: DataTransformerInfo { // If a field and a format are configured // then format the time output - const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.timezone); if (!Array.isArray(data) || data.length === 0) { return data; @@ -40,37 +41,21 @@ export const formatTimeTransformer: DataTransformerInfo (fields: Field[]) => { - const tz = getTimeZone(); - - return fields.map((field) => { - // Find the configured field - if (field.name === timeField) { - // Update values to use the configured format - const newVals = field.values.map((value) => { - const date = moment(value); - - // Apply configured timezone if the - // option has been set. Otherwise - // use the date directly - if (useTimezone) { - const info = getTimeZoneInfo(tz, value); - const realTz = info !== undefined ? info.ianaName : 'UTC'; - - return date.tz(realTz).format(outputFormat); - } else { - return date.format(outputFormat); - } - }); - - return { - ...field, - type: FieldType.string, - values: newVals, - }; +export const createTimeFormatter = (timeField: string, outputFormat: string, timezone: string) => (fields: Field[]) => { + return fields.map((field) => { + // Find the configured field + if (field.name === timeField) { + // Update values to use the configured format + let formattedField = null; + if (timezone) { + formattedField = fieldToStringField(field, outputFormat, { timeZone: timezone }); + } else { + formattedField = fieldToStringField(field, outputFormat); } - return field; - }); - }; + return formattedField; + } + + return field; + }); +}; diff --git a/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx b/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx index cadf1f324e7..8c02c19d3ba 100644 --- a/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx @@ -10,6 +10,7 @@ import { TransformerRegistryItem, TransformerUIProps, TransformerCategory, + getTimeZones, } from '@grafana/data'; import { ConvertFieldTypeOptions, @@ -21,6 +22,8 @@ import { allFieldTypeIconOptions } from '@grafana/ui/src/components/MatchersUI/F import { hasAlphaPanels } from 'app/core/config'; import { findField } from 'app/features/dimensions'; +import { getTimezoneOptions } from '../utils'; + const fieldNamePickerSettings = { settings: { width: 24, isClearable: false }, } as StandardEditorsRegistryItem; @@ -31,6 +34,15 @@ export const ConvertFieldTypeTransformerEditor = ({ onChange, }: TransformerUIProps) => { const allTypes = allFieldTypeIconOptions.filter((v) => v.value !== FieldType.trace); + const timeZoneOptions: Array> = getTimezoneOptions(true); + + // Format timezone options + const tzs = getTimeZones(); + timeZoneOptions.push({ label: 'Browser', value: 'browser' }); + timeZoneOptions.push({ label: 'UTC', value: 'utc' }); + for (const tz of tzs) { + timeZoneOptions.push({ label: tz, value: tz }); + } const onSelectField = useCallback( (idx: number) => (value: string | undefined) => { @@ -90,6 +102,18 @@ export const ConvertFieldTypeTransformerEditor = ({ [onChange, options] ); + const onTzChange = useCallback( + (idx: number) => (value: SelectableValue) => { + const conversions = options.conversions; + conversions[idx] = { ...conversions[idx], timezone: value?.value }; + onChange({ + ...options, + conversions: conversions, + }); + }, + [onChange, options] + ); + return ( <> {options.conversions.map((c: ConvertFieldTypeOptions, idx: number) => { @@ -128,14 +152,19 @@ export const ConvertFieldTypeTransformerEditor = ({ )} {c.destinationType === FieldType.string && (c.dateFormat || findField(input?.[0], c.targetField)?.type === FieldType.time) && ( - - - + <> + + + + + - - + +