mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
18a364eb2f
commit
3dc60cd2d7
@ -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,
|
||||
|
@ -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',
|
||||
partitionByValues = 'partitionByValues',
|
||||
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 { 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] : []),
|
||||
];
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user