Skip to content

Commit

Permalink
Transforms: Add Format Time Transform (Alpha) (#72319)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
codeincarnate committed Jul 26, 2023
1 parent 18a364e commit 3dc60cd
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/grafana-data/src/transformations/transformers.ts
Expand Up @@ -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';
Expand All @@ -29,6 +30,7 @@ export const standardTransformers = {
filterFramesTransformer,
filterFramesByRefIdTransformer,
filterByValueTransformer,
formatTimeTransformer,
orderFieldsTransformer,
organizeFieldsTransformer,
reduceTransformer,
Expand Down
@@ -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;
});
};
Expand Up @@ -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,
};
2 changes: 2 additions & 0 deletions public/app/features/transformers/standardTransformers.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -59,6 +60,7 @@ export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> =
limitTransformRegistryItem,
joinByLabelsTransformRegistryItem,
partitionByValuesTransformRegistryItem,
formatTimeTransformerRegistryItem,
...(config.featureToggles.timeSeriesTable ? [timeSeriesTableTransformRegistryItem] : []),
];
};

0 comments on commit 3dc60cd

Please sign in to comment.