mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
d5945bc26e
commit
1a34cf6b0e
@ -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<Convert
|
||||
description: 'Convert a field to a specified field type.',
|
||||
defaultOptions: {
|
||||
fields: {},
|
||||
conversions: [{ targetField: undefined, destinationType: undefined, dateFormat: undefined }],
|
||||
conversions: [{ targetField: undefined, destinationType: undefined, dateFormat: undefined, timezone: undefined }],
|
||||
},
|
||||
|
||||
operator: (options, ctx) => (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:
|
||||
|
@ -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',
|
||||
]);
|
||||
});
|
||||
|
@ -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<FormatTimeTransformerOptions> = {
|
||||
@ -23,7 +24,7 @@ export const formatTimeTransformer: DataTransformerInfo<FormatTimeTransformerOpt
|
||||
map((data) => {
|
||||
// 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<FormatTimeTransformerOpt
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const createTimeFormatter =
|
||||
(timeField: string, outputFormat: string, useTimezone: boolean) => (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;
|
||||
});
|
||||
};
|
||||
|
@ -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<string, FieldNamePickerConfigSettings>;
|
||||
@ -31,6 +34,15 @@ export const ConvertFieldTypeTransformerEditor = ({
|
||||
onChange,
|
||||
}: TransformerUIProps<ConvertFieldTypeTransformerOptions>) => {
|
||||
const allTypes = allFieldTypeIconOptions.filter((v) => v.value !== FieldType.trace);
|
||||
const timeZoneOptions: Array<SelectableValue<string>> = 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<string>) => {
|
||||
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) && (
|
||||
<InlineField label="Date format" tooltip="Specify the output format.">
|
||||
<Input
|
||||
value={c.dateFormat}
|
||||
placeholder={'e.g. YYYY-MM-DD'}
|
||||
onChange={onInputFormat(idx)}
|
||||
width={24}
|
||||
/>
|
||||
</InlineField>
|
||||
<>
|
||||
<InlineField label="Date format" tooltip="Specify the output format.">
|
||||
<Input
|
||||
value={c.dateFormat}
|
||||
placeholder={'e.g. YYYY-MM-DD'}
|
||||
onChange={onInputFormat(idx)}
|
||||
width={24}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Set timezone" tooltip="Set the timezone of the date manually">
|
||||
<Select options={timeZoneOptions} value={c.timezone} onChange={onTzChange(idx)} isClearable />
|
||||
</InlineField>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
size="md"
|
||||
|
@ -10,7 +10,9 @@ import {
|
||||
PluginState,
|
||||
} from '@grafana/data';
|
||||
import { FormatTimeTransformerOptions } from '@grafana/data/src/transformations/transformers/formatTime';
|
||||
import { Select, InlineFieldRow, InlineField, Input, InlineSwitch } from '@grafana/ui';
|
||||
import { Select, InlineFieldRow, InlineField, Input } from '@grafana/ui';
|
||||
|
||||
import { getTimezoneOptions } from '../utils';
|
||||
|
||||
export function FormatTimeTransfomerEditor({
|
||||
input,
|
||||
@ -18,6 +20,7 @@ export function FormatTimeTransfomerEditor({
|
||||
onChange,
|
||||
}: TransformerUIProps<FormatTimeTransformerOptions>) {
|
||||
const timeFields: Array<SelectableValue<string>> = [];
|
||||
const timeZoneOptions: Array<SelectableValue<string>> = getTimezoneOptions(true);
|
||||
|
||||
// Get time fields
|
||||
for (const frame of input) {
|
||||
@ -51,12 +54,16 @@ export function FormatTimeTransfomerEditor({
|
||||
[onChange, options]
|
||||
);
|
||||
|
||||
const onUseTzChange = useCallback(() => {
|
||||
onChange({
|
||||
...options,
|
||||
useTimezone: !options.useTimezone,
|
||||
});
|
||||
}, [onChange, options]);
|
||||
const onTzChange = useCallback(
|
||||
(value: SelectableValue<string>) => {
|
||||
const val = value?.value !== undefined ? value.value : '';
|
||||
onChange({
|
||||
...options,
|
||||
timezone: val,
|
||||
});
|
||||
},
|
||||
[onChange, options]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -87,12 +94,8 @@ export function FormatTimeTransfomerEditor({
|
||||
>
|
||||
<Input onChange={onFormatChange} value={options.outputFormat} />
|
||||
</InlineField>
|
||||
<InlineField
|
||||
label="Use Timezone"
|
||||
tooltip="Use the user's configured timezone when formatting time."
|
||||
labelWidth={20}
|
||||
>
|
||||
<InlineSwitch value={options.useTimezone} transparent={true} onChange={onUseTzChange} />
|
||||
<InlineField label="Set Timezone" tooltip="Set the timezone of the date manually" labelWidth={20}>
|
||||
<Select options={timeZoneOptions} value={options.timezone} onChange={onTzChange} isClearable />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { DataFrame, getFieldDisplayName, TransformerCategory } from '@grafana/data';
|
||||
import { DataFrame, getFieldDisplayName, TransformerCategory, SelectableValue, getTimeZones } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
export function useAllFieldNamesFromDataFrames(input: DataFrame[]): string[] {
|
||||
@ -61,3 +61,23 @@ export const numberOrVariableValidator = (value: string | number) => {
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export function getTimezoneOptions(includeInternal: boolean) {
|
||||
const timeZoneOptions: Array<SelectableValue<string>> = [];
|
||||
|
||||
// There are currently only two internal timezones
|
||||
// Browser and UTC. We add the manually to avoid
|
||||
// funky string manipulation.
|
||||
if (includeInternal) {
|
||||
timeZoneOptions.push({ label: 'Browser', value: 'browser' });
|
||||
timeZoneOptions.push({ label: 'UTC', value: 'utc' });
|
||||
}
|
||||
|
||||
// Add all other timezones
|
||||
const tzs = getTimeZones();
|
||||
for (const tz of tzs) {
|
||||
timeZoneOptions.push({ label: tz, value: tz });
|
||||
}
|
||||
|
||||
return timeZoneOptions;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user