Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transforms: Add Alpha Format Time Transform #72319

Merged
merged 12 commits into from Jul 26, 2023
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] : []),
];
};