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:
@@ -6,6 +6,7 @@ import { filterFieldsTransformer, filterFramesTransformer } from './transformers
|
|||||||
import { filterFieldsByNameTransformer } from './transformers/filterByName';
|
import { filterFieldsByNameTransformer } from './transformers/filterByName';
|
||||||
import { filterFramesByRefIdTransformer } from './transformers/filterByRefId';
|
import { filterFramesByRefIdTransformer } from './transformers/filterByRefId';
|
||||||
import { filterByValueTransformer } from './transformers/filterByValue';
|
import { filterByValueTransformer } from './transformers/filterByValue';
|
||||||
|
import { formatTimeTransformer } from './transformers/formatTime';
|
||||||
import { groupByTransformer } from './transformers/groupBy';
|
import { groupByTransformer } from './transformers/groupBy';
|
||||||
import { groupingToMatrixTransformer } from './transformers/groupingToMatrix';
|
import { groupingToMatrixTransformer } from './transformers/groupingToMatrix';
|
||||||
import { histogramTransformer } from './transformers/histogram';
|
import { histogramTransformer } from './transformers/histogram';
|
||||||
@@ -29,6 +30,7 @@ export const standardTransformers = {
|
|||||||
filterFramesTransformer,
|
filterFramesTransformer,
|
||||||
filterFramesByRefIdTransformer,
|
filterFramesByRefIdTransformer,
|
||||||
filterByValueTransformer,
|
filterByValueTransformer,
|
||||||
|
formatTimeTransformer,
|
||||||
orderFieldsTransformer,
|
orderFieldsTransformer,
|
||||||
organizeFieldsTransformer,
|
organizeFieldsTransformer,
|
||||||
reduceTransformer,
|
reduceTransformer,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -37,4 +37,5 @@ export enum DataTransformerID {
|
|||||||
limit = 'limit',
|
limit = 'limit',
|
||||||
partitionByValues = 'partitionByValues',
|
partitionByValues = 'partitionByValues',
|
||||||
timeSeriesTable = 'timeSeriesTable',
|
timeSeriesTable = 'timeSeriesTable',
|
||||||
|
formatTime = 'formatTime',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import { concatenateTransformRegistryItem } from './editors/ConcatenateTransform
|
|||||||
import { convertFieldTypeTransformRegistryItem } from './editors/ConvertFieldTypeTransformerEditor';
|
import { convertFieldTypeTransformRegistryItem } from './editors/ConvertFieldTypeTransformerEditor';
|
||||||
import { filterFieldsByNameTransformRegistryItem } from './editors/FilterByNameTransformerEditor';
|
import { filterFieldsByNameTransformRegistryItem } from './editors/FilterByNameTransformerEditor';
|
||||||
import { filterFramesByRefIdTransformRegistryItem } from './editors/FilterByRefIdTransformerEditor';
|
import { filterFramesByRefIdTransformRegistryItem } from './editors/FilterByRefIdTransformerEditor';
|
||||||
|
import { formatTimeTransformerRegistryItem } from './editors/FormatTimeTransformerEditor';
|
||||||
import { groupByTransformRegistryItem } from './editors/GroupByTransformerEditor';
|
import { groupByTransformRegistryItem } from './editors/GroupByTransformerEditor';
|
||||||
import { groupingToMatrixTransformRegistryItem } from './editors/GroupingToMatrixTransformerEditor';
|
import { groupingToMatrixTransformRegistryItem } from './editors/GroupingToMatrixTransformerEditor';
|
||||||
import { histogramTransformRegistryItem } from './editors/HistogramTransformerEditor';
|
import { histogramTransformRegistryItem } from './editors/HistogramTransformerEditor';
|
||||||
@@ -59,6 +60,7 @@ export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> =
|
|||||||
limitTransformRegistryItem,
|
limitTransformRegistryItem,
|
||||||
joinByLabelsTransformRegistryItem,
|
joinByLabelsTransformRegistryItem,
|
||||||
partitionByValuesTransformRegistryItem,
|
partitionByValuesTransformRegistryItem,
|
||||||
|
formatTimeTransformerRegistryItem,
|
||||||
...(config.featureToggles.timeSeriesTable ? [timeSeriesTableTransformRegistryItem] : []),
|
...(config.featureToggles.timeSeriesTable ? [timeSeriesTableTransformRegistryItem] : []),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user