mirror of https://github.com/grafana/grafana
Transforms: Add 'Format String' Transform (#73624)
* Format String Implementation * Prettier * Unify Every/Pascal/Camel cases * Reformat + add new cases * Add Trim and Substring to the transform options * Trim/Substring tests+formatting * refactor * docs + feature toggle * add category * docs. add svg. change description * revert weird add from merge * readd config. change description * docs change * Adding experimental shortcode * Add code formatting * change shortcode --------- Co-authored-by: Victor Marin <victor.marin@grafana.com> Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>pull/76153/head
parent
48ef88aed7
commit
889576ac1d
@ -0,0 +1,85 @@ |
||||
import { toDataFrame } from '../../dataframe/processDataFrame'; |
||||
import { FieldType } from '../../types/dataFrame'; |
||||
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; |
||||
import { fieldMatchers } from '../matchers'; |
||||
import { FieldMatcherID } from '../matchers/ids'; |
||||
|
||||
import { |
||||
createStringFormatter, |
||||
FormatStringOutput, |
||||
formatStringTransformer, |
||||
getFormatStringFunction, |
||||
} from './formatString'; |
||||
|
||||
const frame = toDataFrame({ |
||||
fields: [ |
||||
{ |
||||
name: 'names', |
||||
type: FieldType.string, |
||||
values: ['alice', 'BOB', ' charliE ', 'david frederick attenborough', 'Emma Fakename', ''], |
||||
}, |
||||
], |
||||
}); |
||||
|
||||
const fieldMatches = fieldMatchers.get(FieldMatcherID.byName).get('names'); |
||||
|
||||
const options = (format: FormatStringOutput, substringStart?: number, substringEnd?: number) => { |
||||
return { |
||||
stringField: 'names', |
||||
substringStart: substringStart ?? 0, |
||||
substringEnd: substringEnd ?? 100, |
||||
outputFormat: format, |
||||
}; |
||||
}; |
||||
|
||||
describe('Format String Transformer', () => { |
||||
beforeAll(() => { |
||||
mockTransformationsRegistry([formatStringTransformer]); |
||||
}); |
||||
|
||||
it('will convert string to each case', () => { |
||||
const formats = [ |
||||
FormatStringOutput.UpperCase, |
||||
FormatStringOutput.LowerCase, |
||||
FormatStringOutput.SentenceCase, |
||||
FormatStringOutput.TitleCase, |
||||
FormatStringOutput.PascalCase, |
||||
FormatStringOutput.CamelCase, |
||||
FormatStringOutput.SnakeCase, |
||||
FormatStringOutput.KebabCase, |
||||
FormatStringOutput.Trim, |
||||
]; |
||||
const newValues = []; |
||||
|
||||
for (let i = 0; i < formats.length; i++) { |
||||
const formatter = createStringFormatter(fieldMatches, getFormatStringFunction(options(formats[i]))); |
||||
const newFrame = formatter(frame, [frame]); |
||||
newValues.push(newFrame[0].values); |
||||
} |
||||
|
||||
const answers = [ |
||||
['ALICE', 'BOB', ' CHARLIE ', 'DAVID FREDERICK ATTENBOROUGH', 'EMMA FAKENAME', ''], // Upper Case
|
||||
['alice', 'bob', ' charlie ', 'david frederick attenborough', 'emma fakename', ''], // Lower Case
|
||||
['Alice', 'BOB', ' charliE ', 'David frederick attenborough', 'Emma Fakename', ''], // Sentence Case
|
||||
['Alice', 'Bob', ' Charlie ', 'David Frederick Attenborough', 'Emma Fakename', ''], // Title Case
|
||||
['Alice', 'Bob', 'Charlie', 'DavidFrederickAttenborough', 'EmmaFakename', ''], // Pascal Case
|
||||
['alice', 'bob', 'charlie', 'davidFrederickAttenborough', 'emmaFakename', ''], // Camel Case
|
||||
['alice', 'bob', '__charlie__', 'david_frederick_attenborough', 'emma_fakename', ''], // Snake Case
|
||||
['alice', 'bob', '--charlie--', 'david-frederick-attenborough', 'emma-fakename', ''], // Kebab Case
|
||||
['alice', 'BOB', 'charliE', 'david frederick attenborough', 'Emma Fakename', ''], // Trim
|
||||
]; |
||||
|
||||
expect(newValues).toEqual(answers); |
||||
}); |
||||
|
||||
it('will convert string to substring', () => { |
||||
const formatter = createStringFormatter( |
||||
fieldMatches, |
||||
getFormatStringFunction(options(FormatStringOutput.Substring, 2, 5)) |
||||
); |
||||
const newFrame = formatter(frame, [frame]); |
||||
const newValues = newFrame[0].values; |
||||
|
||||
expect(newValues).toEqual(['ice', 'B', 'cha', 'vid', 'ma ', '']); |
||||
}); |
||||
}); |
@ -0,0 +1,112 @@ |
||||
import { map } from 'rxjs/operators'; |
||||
|
||||
import { DataFrame, Field, FieldType } from '../../types'; |
||||
import { DataTransformerInfo, FieldMatcher } from '../../types/transformations'; |
||||
import { fieldMatchers } from '../matchers'; |
||||
import { FieldMatcherID } from '../matchers/ids'; |
||||
|
||||
import { DataTransformerID } from './ids'; |
||||
|
||||
export enum FormatStringOutput { |
||||
UpperCase = 'Upper Case', |
||||
LowerCase = 'Lower Case', |
||||
SentenceCase = 'Sentence Case', |
||||
TitleCase = 'Title Case', |
||||
PascalCase = 'Pascal Case', |
||||
CamelCase = 'Camel Case', |
||||
SnakeCase = 'Snake Case', |
||||
KebabCase = 'Kebab Case', |
||||
Trim = 'Trim', |
||||
Substring = 'Substring', |
||||
} |
||||
|
||||
export interface FormatStringTransformerOptions { |
||||
stringField: string; |
||||
substringStart: number; |
||||
substringEnd: number; |
||||
outputFormat: FormatStringOutput; |
||||
} |
||||
|
||||
const splitToCapitalWords = (input: string) => { |
||||
const arr = input.split(' '); |
||||
for (let i = 0; i < arr.length; i++) { |
||||
arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].slice(1).toLowerCase(); |
||||
} |
||||
return arr; |
||||
}; |
||||
|
||||
export const getFormatStringFunction = (options: FormatStringTransformerOptions) => { |
||||
return (field: Field) => |
||||
field.values.map((value: string) => { |
||||
switch (options.outputFormat) { |
||||
case FormatStringOutput.UpperCase: |
||||
return value.toUpperCase(); |
||||
case FormatStringOutput.LowerCase: |
||||
return value.toLowerCase(); |
||||
case FormatStringOutput.SentenceCase: |
||||
return value.charAt(0).toUpperCase() + value.slice(1); |
||||
case FormatStringOutput.TitleCase: |
||||
return splitToCapitalWords(value).join(' '); |
||||
case FormatStringOutput.PascalCase: |
||||
return splitToCapitalWords(value).join(''); |
||||
case FormatStringOutput.CamelCase: |
||||
value = splitToCapitalWords(value).join(''); |
||||
return value.charAt(0).toLowerCase() + value.slice(1); |
||||
case FormatStringOutput.SnakeCase: |
||||
return value.toLowerCase().split(' ').join('_'); |
||||
case FormatStringOutput.KebabCase: |
||||
return value.toLowerCase().split(' ').join('-'); |
||||
case FormatStringOutput.Trim: |
||||
return value.trim(); |
||||
case FormatStringOutput.Substring: |
||||
return value.substring(options.substringStart, options.substringEnd); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
export const formatStringTransformer: DataTransformerInfo<FormatStringTransformerOptions> = { |
||||
id: DataTransformerID.formatString, |
||||
name: 'Format string', |
||||
description: 'Manipulate string fields formatting', |
||||
defaultOptions: { stringField: '', outputFormat: FormatStringOutput.UpperCase }, |
||||
operator: (options) => (source) => |
||||
source.pipe( |
||||
map((data) => { |
||||
if (data.length === 0) { |
||||
return data; |
||||
} |
||||
|
||||
const fieldMatches = fieldMatchers.get(FieldMatcherID.byName).get(options.stringField); |
||||
const formatStringFunction = getFormatStringFunction(options); |
||||
|
||||
const formatter = createStringFormatter(fieldMatches, formatStringFunction); |
||||
|
||||
return data.map((frame) => ({ |
||||
...frame, |
||||
fields: formatter(frame, data), |
||||
})); |
||||
}) |
||||
), |
||||
}; |
||||
|
||||
/** |
||||
* @internal |
||||
*/ |
||||
export const createStringFormatter = |
||||
(fieldMatches: FieldMatcher, formatStringFunction: (field: Field) => string[]) => |
||||
(frame: DataFrame, allFrames: DataFrame[]) => { |
||||
return frame.fields.map((field) => { |
||||
// Find the configured field
|
||||
if (fieldMatches(field, frame, allFrames)) { |
||||
const newVals = formatStringFunction(field); |
||||
|
||||
return { |
||||
...field, |
||||
type: FieldType.string, |
||||
values: newVals, |
||||
}; |
||||
} |
||||
|
||||
return field; |
||||
}); |
||||
}; |
|
@ -0,0 +1,123 @@ |
||||
import React, { useCallback } from 'react'; |
||||
|
||||
import { |
||||
DataTransformerID, |
||||
SelectableValue, |
||||
standardTransformers, |
||||
TransformerRegistryItem, |
||||
TransformerUIProps, |
||||
PluginState, |
||||
FieldType, |
||||
StandardEditorsRegistryItem, |
||||
FieldNamePickerConfigSettings, |
||||
TransformerCategory, |
||||
} from '@grafana/data'; |
||||
import { |
||||
FormatStringOutput, |
||||
FormatStringTransformerOptions, |
||||
} from '@grafana/data/src/transformations/transformers/formatString'; |
||||
import { Select, InlineFieldRow, InlineField } from '@grafana/ui'; |
||||
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker'; |
||||
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; |
||||
|
||||
const fieldNamePickerSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = { |
||||
settings: { |
||||
width: 30, |
||||
filter: (f) => f.type === FieldType.string, |
||||
placeholderText: 'Select text field', |
||||
noFieldsMessage: 'No text fields found', |
||||
}, |
||||
name: '', |
||||
id: '', |
||||
editor: () => null, |
||||
}; |
||||
|
||||
function FormatStringTransfomerEditor({ |
||||
input, |
||||
options, |
||||
onChange, |
||||
}: TransformerUIProps<FormatStringTransformerOptions>) { |
||||
const onSelectField = useCallback( |
||||
(value: string | undefined) => { |
||||
const val = value ?? ''; |
||||
onChange({ |
||||
...options, |
||||
stringField: val, |
||||
}); |
||||
}, |
||||
[onChange, options] |
||||
); |
||||
|
||||
const onFormatChange = useCallback( |
||||
(value: SelectableValue<FormatStringOutput>) => { |
||||
const val = value.value ?? FormatStringOutput.UpperCase; |
||||
onChange({ |
||||
...options, |
||||
outputFormat: val, |
||||
}); |
||||
}, |
||||
[onChange, options] |
||||
); |
||||
|
||||
const onSubstringStartChange = useCallback( |
||||
(value?: number) => { |
||||
onChange({ |
||||
...options, |
||||
substringStart: value ?? 0, |
||||
}); |
||||
}, |
||||
[onChange, options] |
||||
); |
||||
|
||||
const onSubstringEndChange = useCallback( |
||||
(value?: number) => { |
||||
onChange({ |
||||
...options, |
||||
substringEnd: value ?? 0, |
||||
}); |
||||
}, |
||||
[onChange, options] |
||||
); |
||||
|
||||
const ops = Object.values(FormatStringOutput).map((value) => ({ label: value, value })); |
||||
|
||||
return ( |
||||
<> |
||||
<InlineFieldRow> |
||||
<InlineField label={'Field'} labelWidth={10}> |
||||
<FieldNamePicker |
||||
context={{ data: input }} |
||||
value={options.stringField ?? ''} |
||||
onChange={onSelectField} |
||||
item={fieldNamePickerSettings} |
||||
/> |
||||
</InlineField> |
||||
|
||||
<InlineField label="Format" labelWidth={10}> |
||||
<Select options={ops} value={options.outputFormat} onChange={onFormatChange} width={20} /> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
|
||||
{options.outputFormat === FormatStringOutput.Substring && ( |
||||
<InlineFieldRow> |
||||
<InlineField label="Substring range" labelWidth={15}> |
||||
<NumberInput min={0} value={options.substringStart ?? 0} onChange={onSubstringStartChange} width={7} /> |
||||
</InlineField> |
||||
<InlineField> |
||||
<NumberInput min={0} value={options.substringEnd ?? 0} onChange={onSubstringEndChange} width={7} /> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
)} |
||||
</> |
||||
); |
||||
} |
||||
|
||||
export const formatStringTransformerRegistryItem: TransformerRegistryItem<FormatStringTransformerOptions> = { |
||||
id: DataTransformerID.formatString, |
||||
editor: FormatStringTransfomerEditor, |
||||
transformation: standardTransformers.formatStringTransformer, |
||||
name: standardTransformers.formatStringTransformer.name, |
||||
state: PluginState.beta, |
||||
description: standardTransformers.formatStringTransformer.description, |
||||
categories: new Set([TransformerCategory.Reformat]), |
||||
}; |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 5.1 KiB |
Loading…
Reference in new issue