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,248 @@
import { toDataFrame } from '../../dataframe/processDataFrame';
import { Field, 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',
};

const formatter = createTimeFormatter(options.timeField, options.outputFormat);
const frame = toDataFrame({
fields: [
{
name: 'time',
type: FieldType.time,
values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, 1691011200000],
},
],
});

const newFrame = formatter(frame.fields, [frame], frame);
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',
};

const formatter = createTimeFormatter(options.timeField, options.outputFormat);
const frame = toDataFrame({
fields: [
{
name: 'time',
type: FieldType.time,
values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, 1691011200000],
},
],
});

const newFrame = formatter(frame.fields, [frame], frame);
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',
};

const formatter = createTimeFormatter(options.timeField, options.outputFormat);
const frame = toDataFrame({
fields: [
{
name: 'time',
type: FieldType.time,
values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, null],
},
],
});

const newFrame = formatter(frame.fields, [frame], frame);
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',
]);
});
});

describe('field convert types transformer', () => {
it('can convert multiple fields', () => {
const options = {
conversions: [
{ targetField: 'stringy nums', destinationType: FieldType.number },
{ targetField: 'proper dates', destinationType: FieldType.time },
],
};

const stringyNumbers = toDataFrame({
fields: [
{ name: 'A', type: FieldType.number, values: [1, 2, 3, 4, 5] },
{
name: 'proper dates',
type: FieldType.string,
values: [
'2021-07-19 00:00:00.000',
'2021-07-23 00:00:00.000',
'2021-07-25 00:00:00.000',
'2021-08-01 00:00:00.000',
'2021-08-02 00:00:00.000',
],
},
{ name: 'stringy nums', type: FieldType.string, values: ['10', '12', '30', '14', '10'] },
],
});

const numbers = convertFieldTypes(options, [stringyNumbers]);
expect(
numbers[0].fields.map((f) => ({
type: f.type,
values: f.values,
}))
).toEqual([
{ type: FieldType.number, values: [1, 2, 3, 4, 5] },
{
type: FieldType.time,
values: [1626674400000, 1627020000000, 1627192800000, 1627797600000, 1627884000000],
},
{
type: FieldType.number,
values: [10, 12, 30, 14, 10],
},
]);
});

it('will convert field to complex objects', () => {
const options = {
conversions: [
{ targetField: 'numbers', destinationType: FieldType.other },
{ targetField: 'objects', destinationType: FieldType.other },
{ targetField: 'arrays', destinationType: FieldType.other },
{ targetField: 'invalids', destinationType: FieldType.other },
{ targetField: 'mixed', destinationType: FieldType.other },
],
};

const comboTypes = toDataFrame({
fields: [
{
name: 'numbers',
type: FieldType.number,
values: [-1, 1, null],
},
{
name: 'objects',
type: FieldType.string,
values: [
'{ "neg": -100, "zero": 0, "pos": 1, "null": null, "array": [0, 1, 2], "nested": { "number": 1 } }',
'{ "string": "abcd" }',
'{}',
],
},
{
name: 'arrays',
type: FieldType.string,
values: ['[true]', '[99]', '["2021-08-02 00:00:00.000"]'],
},
{
name: 'invalids',
type: FieldType.string,
values: ['abcd', '{ invalidJson }', '[unclosed array'],
},
{
name: 'mixed',
type: FieldType.string,
values: [
'{ "neg": -100, "zero": 0, "pos": 1, "null": null, "array": [0, 1, 2], "nested": { "number": 1 } }',
'["a string", 1234, {"a complex": "object"}]',
'["this is invalid JSON]',
],
},
],
});

const complex = convertFieldTypes(options, [comboTypes]);
expect(
complex[0].fields.map((f) => ({
type: f.type,
values: f.values,
}))
).toEqual([
{
type: FieldType.other,
values: [-1, 1, null],
},
{
type: FieldType.other,
values: [
{ neg: -100, zero: 0, pos: 1, null: null, array: [0, 1, 2], nested: { number: 1 } },
{ string: 'abcd' },
{},
],
},
{ type: FieldType.other, values: [[true], [99], ['2021-08-02 00:00:00.000']] },
{ type: FieldType.other, values: [null, null, null] },
{
type: FieldType.other,
values: [
{ neg: -100, zero: 0, pos: 1, null: null, array: [0, 1, 2], nested: { number: 1 } },
['a string', 1234, { 'a complex': 'object' }],
null,
],
},
]);
});

it('will convert field to strings', () => {
const options = {
conversions: [{ targetField: 'numbers', destinationType: FieldType.string }],
};

const comboTypes = toDataFrame({
fields: [
{ name: 'numbers', type: FieldType.number, values: [-100, 0, 1, null, NaN] },
{
name: 'strings',
type: FieldType.string,
values: ['true', 'false', '0', '99', '2021-08-02 00:00:00.000'],
},
],
});

const stringified = convertFieldTypes(options, [comboTypes]);
expect(
stringified[0].fields.map((f) => ({
type: f.type,
values: f.values,
}))
).toEqual([
{
type: FieldType.string,
values: ['-100', '0', '1', 'null', 'NaN'],
},
{
type: FieldType.string,
values: ['true', 'false', '0', '99', '2021-08-02 00:00:00.000'],
},
]);
});
});
@@ -0,0 +1,61 @@
import { map } from 'rxjs/operators';

import { dateTime } from '../../datetime';
import { DataFrame, Field, FieldType } from '../../types';
import { DataTransformerInfo } from '../../types/transformations';

import { DataTransformerID } from './ids';

export interface FormatTimeTransformerOptions {
timeField: string;
outputFormat: string;
}

export const formatTimeTransformer: DataTransformerInfo<FormatTimeTransformerOptions> = {
id: DataTransformerID.formatTime,
name: 'Format Time',
description: 'Set the output format of a time field',
defaultOptions: { timeField: '', outputFormat: '' },
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);

if (!Array.isArray(data) || data.length === 0) {
return data;
}

return data.map((frame) => ({
...frame,
fields: formatter(frame.fields, data, frame),
}));
})
),
};

/**
* @internal
*/
export const createTimeFormatter =
(timeField: string, outputFormat: string) => (fields: Field[], data: DataFrame[], frame: DataFrame) => {
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 moment = dateTime(value);
return moment.format(outputFormat);
codeincarnate marked this conversation as resolved.
Show resolved Hide resolved
});

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'
}