Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
18a364e
commit 3dc60cd
Showing
6 changed files
with
270 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
packages/grafana-data/src/transformations/transformers/formatTime.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
]); | ||
}); | ||
}); |
76 changes: 76 additions & 0 deletions
76
packages/grafana-data/src/transformations/transformers/formatTime.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters