Transforms: Add Format Time Transform (Alpha) (#72319)

* Stub transform editor

* Mostly working

* Get things working 💪

* Add tests

* Add alpha flag

* Timezone support

* Remove debug statement

* Fix tests

* Prettier fix

* Fix linter error

* One more linter fix
This commit is contained in:
Kyle Cunningham 2023-07-26 17:08:36 -05:00 committed by GitHub
parent 18a364eb2f
commit 3dc60cd2d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 270 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import { filterFieldsTransformer, filterFramesTransformer } from './transformers
import { filterFieldsByNameTransformer } from './transformers/filterByName';
import { filterFramesByRefIdTransformer } from './transformers/filterByRefId';
import { filterByValueTransformer } from './transformers/filterByValue';
import { formatTimeTransformer } from './transformers/formatTime';
import { groupByTransformer } from './transformers/groupBy';
import { groupingToMatrixTransformer } from './transformers/groupingToMatrix';
import { histogramTransformer } from './transformers/histogram';
@ -29,6 +30,7 @@ export const standardTransformers = {
filterFramesTransformer,
filterFramesByRefIdTransformer,
filterByValueTransformer,
formatTimeTransformer,
orderFieldsTransformer,
organizeFieldsTransformer,
reduceTransformer,

View File

@ -0,0 +1,89 @@
import { toDataFrame } from '../../dataframe/processDataFrame';
import { FieldType } from '../../types/dataFrame';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { createTimeFormatter, formatTimeTransformer } from './formatTime';
describe('Format Time Transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([formatTimeTransformer]);
});
it('will convert time to formatted string', () => {
const options = {
timeField: 'time',
outputFormat: 'YYYY-MM',
useTimezone: false,
};
const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone);
const frame = toDataFrame({
fields: [
{
name: 'time',
type: FieldType.time,
values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, 1691011200000],
},
],
});
const newFrame = formatter(frame.fields);
expect(newFrame[0].values).toEqual(['2021-02', '2023-07', '2023-04', '2023-07', '2023-08']);
});
it('will handle formats with times', () => {
const options = {
timeField: 'time',
outputFormat: 'YYYY-MM h:mm:ss a',
useTimezone: false,
};
const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone);
const frame = toDataFrame({
fields: [
{
name: 'time',
type: FieldType.time,
values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, 1691011200000],
},
],
});
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',
]);
});
it('will handle null times', () => {
const options = {
timeField: 'time',
outputFormat: 'YYYY-MM h:mm:ss a',
useTimezone: false,
};
const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone);
const frame = toDataFrame({
fields: [
{
name: 'time',
type: FieldType.time,
values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, null],
},
],
});
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',
'Invalid date',
]);
});
});

View File

@ -0,0 +1,76 @@
import moment from 'moment-timezone';
import { map } from 'rxjs/operators';
import { getTimeZone, getTimeZoneInfo } from '../../datetime';
import { Field, FieldType } from '../../types';
import { DataTransformerInfo } from '../../types/transformations';
import { DataTransformerID } from './ids';
export interface FormatTimeTransformerOptions {
timeField: string;
outputFormat: string;
useTimezone: boolean;
}
export const formatTimeTransformer: DataTransformerInfo<FormatTimeTransformerOptions> = {
id: DataTransformerID.formatTime,
name: 'Format Time',
description: 'Set the output format of a time field',
defaultOptions: { timeField: '', outputFormat: '', useTimezone: true },
operator: (options) => (source) =>
source.pipe(
map((data) => {
// If a field and a format are configured
// then format the time output
const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone);
if (!Array.isArray(data) || data.length === 0) {
return data;
}
return data.map((frame) => ({
...frame,
fields: formatter(frame.fields),
}));
})
),
};
/**
* @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,
};
}
return field;
});
};

View File

@ -37,4 +37,5 @@ export enum DataTransformerID {
limit = 'limit',
partitionByValues = 'partitionByValues',
timeSeriesTable = 'timeSeriesTable',
formatTime = 'formatTime',
}

View File

@ -0,0 +1,100 @@
import React, { useCallback, ChangeEvent } from 'react';
import {
DataTransformerID,
SelectableValue,
standardTransformers,
TransformerRegistryItem,
TransformerUIProps,
getFieldDisplayName,
PluginState,
} from '@grafana/data';
import { FormatTimeTransformerOptions } from '@grafana/data/src/transformations/transformers/formatTime';
import { Select, InlineFieldRow, InlineField, Input, InlineSwitch } from '@grafana/ui';
export function FormatTimeTransfomerEditor({
input,
options,
onChange,
}: TransformerUIProps<FormatTimeTransformerOptions>) {
const timeFields: Array<SelectableValue<string>> = [];
// Get time fields
for (const frame of input) {
for (const field of frame.fields) {
if (field.type === 'time') {
const name = getFieldDisplayName(field, frame, input);
timeFields.push({ label: name, value: name });
}
}
}
const onSelectField = useCallback(
(value: SelectableValue<string>) => {
const val = value?.value !== undefined ? value.value : '';
onChange({
...options,
timeField: val,
});
},
[onChange, options]
);
const onFormatChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
onChange({
...options,
outputFormat: val,
});
},
[onChange, options]
);
const onUseTzChange = useCallback(() => {
onChange({
...options,
useTimezone: !options.useTimezone,
});
}, [onChange, options]);
return (
<>
<InlineFieldRow>
<InlineField label="Time Field" labelWidth={15} grow>
<Select
options={timeFields}
value={options.timeField}
onChange={onSelectField}
placeholder="time"
isClearable
/>
</InlineField>
<InlineField
label="Format"
labelWidth={10}
tooltip="The output format for the field specified as a moment.js format string."
>
<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>
</InlineFieldRow>
</>
);
}
export const formatTimeTransformerRegistryItem: TransformerRegistryItem<FormatTimeTransformerOptions> = {
id: DataTransformerID.formatTime,
editor: FormatTimeTransfomerEditor,
transformation: standardTransformers.formatTimeTransformer,
name: standardTransformers.formatTimeTransformer.name,
state: PluginState.alpha,
description: standardTransformers.formatTimeTransformer.description,
};

View File

@ -9,6 +9,7 @@ import { concatenateTransformRegistryItem } from './editors/ConcatenateTransform
import { convertFieldTypeTransformRegistryItem } from './editors/ConvertFieldTypeTransformerEditor';
import { filterFieldsByNameTransformRegistryItem } from './editors/FilterByNameTransformerEditor';
import { filterFramesByRefIdTransformRegistryItem } from './editors/FilterByRefIdTransformerEditor';
import { formatTimeTransformerRegistryItem } from './editors/FormatTimeTransformerEditor';
import { groupByTransformRegistryItem } from './editors/GroupByTransformerEditor';
import { groupingToMatrixTransformRegistryItem } from './editors/GroupingToMatrixTransformerEditor';
import { histogramTransformRegistryItem } from './editors/HistogramTransformerEditor';
@ -59,6 +60,7 @@ export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> =
limitTransformRegistryItem,
joinByLabelsTransformRegistryItem,
partitionByValuesTransformRegistryItem,
formatTimeTransformerRegistryItem,
...(config.featureToggles.timeSeriesTable ? [timeSeriesTableTransformRegistryItem] : []),
];
};