From 188aed05f99cc1d7ee3df13451191a297ee824b2 Mon Sep 17 00:00:00 2001 From: Lars Stegman Date: Fri, 22 Mar 2024 22:39:01 +0100 Subject: [PATCH] Transformer: Config from Query: set threshold colours (#83366) * Fix typo * Create handler arguments * Create handler arguments editor * Create link in editor * Add labels to arguments editor fields * Create unit tests * Hide "Additional settings" header when no additional settings possible * Remove div * Fix invalid DOM --------- Co-authored-by: jev forsberg --- .../configFromQuery/configFromQuery.test.ts | 15 +++++ .../configFromQuery/configFromQuery.ts | 4 +- ...nfigMappingHandlerArgumentsEditor.test.tsx | 37 ++++++++++++ ...eldConfigMappingHandlerArgumentsEditor.tsx | 57 +++++++++++++++++++ .../FieldToConfigMappingEditor.test.tsx | 14 +++++ .../FieldToConfigMappingEditor.tsx | 39 ++++++++++++- .../fieldToConfigMapping.ts | 27 +++++++-- .../transformers/rowsToFields/rowsToFields.ts | 4 +- 8 files changed, 186 insertions(+), 11 deletions(-) create mode 100644 public/app/features/transformers/fieldToConfigMapping/FieldConfigMappingHandlerArgumentsEditor.test.tsx create mode 100644 public/app/features/transformers/fieldToConfigMapping/FieldConfigMappingHandlerArgumentsEditor.tsx diff --git a/public/app/features/transformers/configFromQuery/configFromQuery.test.ts b/public/app/features/transformers/configFromQuery/configFromQuery.test.ts index 08f0f7efb87..dbf6e024eb2 100644 --- a/public/app/features/transformers/configFromQuery/configFromQuery.test.ts +++ b/public/app/features/transformers/configFromQuery/configFromQuery.test.ts @@ -85,6 +85,20 @@ describe('config from data', () => { expect(results[0].fields[1].config.max).toBe(1); }); + it('With threshold', () => { + const options: ConfigFromQueryTransformOptions = { + configRefId: 'A', + mappings: [{ fieldName: 'Max', handlerKey: 'threshold1', handlerArguments: { threshold: { color: 'orange' } } }], + }; + + const results = extractConfigFromQuery(options, [config, seriesA]); + expect(results.length).toBe(1); + const thresholdConfig = results[0].fields[1].config.thresholds?.steps[1]; + expect(thresholdConfig).toBeDefined(); + expect(thresholdConfig?.color).toBe('orange'); + expect(thresholdConfig?.value).toBe(50); + }); + it('With custom matcher and displayName mapping', () => { const options: ConfigFromQueryTransformOptions = { configRefId: 'A', @@ -102,6 +116,7 @@ describe('value mapping from data', () => { const config = toDataFrame({ fields: [ { name: 'value', type: FieldType.number, values: [1, 2, 3] }, + { name: 'threshold', type: FieldType.number, values: [4] }, { name: 'text', type: FieldType.string, values: ['one', 'two', 'three'] }, { name: 'color', type: FieldType.string, values: ['red', 'blue', 'green'] }, ], diff --git a/public/app/features/transformers/configFromQuery/configFromQuery.ts b/public/app/features/transformers/configFromQuery/configFromQuery.ts index 511e5257ac8..44cd15c2117 100644 --- a/public/app/features/transformers/configFromQuery/configFromQuery.ts +++ b/public/app/features/transformers/configFromQuery/configFromQuery.ts @@ -12,7 +12,7 @@ import { } from '@grafana/data'; import { - evaluteFieldMappings, + evaluateFieldMappings, FieldToConfigMapping, getFieldConfigFromFrame, } from '../fieldToConfigMapping/fieldToConfigMapping'; @@ -42,7 +42,7 @@ export function extractConfigFromQuery(options: ConfigFromQueryTransformOptions, length: 1, }; - const mappingResult = evaluteFieldMappings(configFrame, options.mappings ?? [], false); + const mappingResult = evaluateFieldMappings(configFrame, options.mappings ?? [], false); // reduce config frame for (const field of configFrame.fields) { diff --git a/public/app/features/transformers/fieldToConfigMapping/FieldConfigMappingHandlerArgumentsEditor.test.tsx b/public/app/features/transformers/fieldToConfigMapping/FieldConfigMappingHandlerArgumentsEditor.test.tsx new file mode 100644 index 00000000000..76a81dde407 --- /dev/null +++ b/public/app/features/transformers/fieldToConfigMapping/FieldConfigMappingHandlerArgumentsEditor.test.tsx @@ -0,0 +1,37 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { FieldConfigMappingHandlerArgumentsEditor, Props } from './FieldConfigMappingHandlerArgumentsEditor'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +const mockOnChange = jest.fn(); + +const props: Props = { + handlerKey: null, + handlerArguments: {}, + onChange: mockOnChange, +}; + +const setup = (testProps?: Partial) => { + const editorProps = { ...props, ...testProps }; + return render(); +}; + +describe('FieldConfigMappingHandlerArgumentsEditor', () => { + it('Should show a color picker when thresholds are selected', async () => { + setup({ handlerKey: 'threshold1' }); + + expect(await screen.findByDisplayValue('Threshold color')).toBeInTheDocument(); + expect(await screen.findByLabelText('red color')).toBeInTheDocument(); + }); + + it('Should show the correct selected color', async () => { + setup({ handlerKey: 'threshold1', handlerArguments: { threshold: { color: 'orange' } } }); + + expect(await screen.findByDisplayValue('Threshold color')).toBeInTheDocument(); + expect(await screen.findByLabelText('orange color')).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/transformers/fieldToConfigMapping/FieldConfigMappingHandlerArgumentsEditor.tsx b/public/app/features/transformers/fieldToConfigMapping/FieldConfigMappingHandlerArgumentsEditor.tsx new file mode 100644 index 00000000000..474eeb1a0b3 --- /dev/null +++ b/public/app/features/transformers/fieldToConfigMapping/FieldConfigMappingHandlerArgumentsEditor.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { ColorPicker, Input } from '@grafana/ui'; + +import { HandlerArguments } from './fieldToConfigMapping'; + +export interface Props { + handlerKey: string | null; + handlerArguments: HandlerArguments; + onChange: (args: HandlerArguments) => void; +} + +export function createsArgumentsEditor(handlerKey: string | null) { + switch (handlerKey) { + case 'threshold1': + return true; + default: + return false; + } +} + +export function FieldConfigMappingHandlerArgumentsEditor({ handlerArguments, handlerKey, onChange }: Props) { + const onChangeThreshold = (color: string | null) => { + if (color) { + onChange({ + ...handlerArguments, + threshold: { + ...handlerArguments.threshold, + color: color, + }, + }); + } else { + onChange({ ...handlerArguments, threshold: undefined }); + } + }; + + return ( + <> + {handlerKey === 'threshold1' && ( + + } + /> + )} + + ); +} diff --git a/public/app/features/transformers/fieldToConfigMapping/FieldToConfigMappingEditor.test.tsx b/public/app/features/transformers/fieldToConfigMapping/FieldToConfigMappingEditor.test.tsx index e1b04225761..17cb3cfbfe1 100644 --- a/public/app/features/transformers/fieldToConfigMapping/FieldToConfigMappingEditor.test.tsx +++ b/public/app/features/transformers/fieldToConfigMapping/FieldToConfigMappingEditor.test.tsx @@ -89,4 +89,18 @@ describe('FieldToConfigMappingEditor', () => { expect.arrayContaining([{ fieldName: 'max', handlerKey: 'max', reducerId: 'last' }]) ); }); + + it('Shows additional settings', async () => { + setup({ mappings: [{ fieldName: 'max', handlerKey: 'threshold1' }] }); + + const select = await screen.findByText('Additional settings'); + expect(select).toBeInTheDocument(); + }); + + it('Does not show additional settings', async () => { + setup(); + + const select = screen.queryByText('Additional settings'); + expect(select).not.toBeInTheDocument(); + }); }); diff --git a/public/app/features/transformers/fieldToConfigMapping/FieldToConfigMappingEditor.tsx b/public/app/features/transformers/fieldToConfigMapping/FieldToConfigMappingEditor.tsx index 8ca57453baa..1bcd9ab7418 100644 --- a/public/app/features/transformers/fieldToConfigMapping/FieldToConfigMappingEditor.tsx +++ b/public/app/features/transformers/fieldToConfigMapping/FieldToConfigMappingEditor.tsx @@ -7,12 +7,18 @@ import { Select, StatsPicker, useStyles2 } from '@grafana/ui'; import { configMapHandlers, - evaluteFieldMappings, + evaluateFieldMappings, FieldToConfigMapHandler, FieldToConfigMapping, + HandlerArguments, lookUpConfigHandler as findConfigHandlerFor, } from '../fieldToConfigMapping/fieldToConfigMapping'; +import { + createsArgumentsEditor, + FieldConfigMappingHandlerArgumentsEditor, +} from './FieldConfigMappingHandlerArgumentsEditor'; + export interface Props { frame: DataFrame; mappings: FieldToConfigMapping[]; @@ -27,6 +33,10 @@ export function FieldToConfigMappingEditor({ frame, mappings, onChange, withRedu const configProps = configMapHandlers.map((def) => configHandlerToSelectOption(def, false)) as Array< SelectableValue >; + const hasAdditionalSettings = mappings.reduce( + (prev, mapping) => prev || createsArgumentsEditor(mapping.handlerKey), + false + ); const onChangeConfigProperty = (row: FieldToConfigRowViewModel, value: SelectableValue) => { const existingIdx = mappings.findIndex((x) => x.fieldName === row.fieldName); @@ -60,6 +70,18 @@ export function FieldToConfigMappingEditor({ frame, mappings, onChange, withRedu } }; + const onChangeHandlerArguments = (row: FieldToConfigRowViewModel, handlerArguments: HandlerArguments) => { + const existingIdx = mappings.findIndex((x) => x.fieldName === row.fieldName); + + if (existingIdx !== -1) { + const update = [...mappings]; + update.splice(existingIdx, 1, { ...mappings[existingIdx], handlerArguments }); + onChange(update); + } else { + onChange([...mappings, { fieldName: row.fieldName, handlerKey: row.handlerKey, handlerArguments }]); + } + }; + return ( @@ -67,6 +89,7 @@ export function FieldToConfigMappingEditor({ frame, mappings, onChange, withRedu {withReducers && } + {hasAdditionalSettings && } @@ -91,6 +114,15 @@ export function FieldToConfigMappingEditor({ frame, mappings, onChange, withRedu /> )} + {hasAdditionalSettings && ( + + )} ))} @@ -105,6 +137,7 @@ interface FieldToConfigRowViewModel { placeholder?: string; missingInFrame?: boolean; reducerId: string; + handlerArguments: HandlerArguments; } function getViewModelRows( @@ -113,7 +146,7 @@ function getViewModelRows( withNameAndValue?: boolean ): FieldToConfigRowViewModel[] { const rows: FieldToConfigRowViewModel[] = []; - const mappingResult = evaluteFieldMappings(frame, mappings ?? [], withNameAndValue); + const mappingResult = evaluateFieldMappings(frame, mappings ?? [], withNameAndValue); for (const field of frame.fields) { const fieldName = getFieldDisplayName(field, frame); @@ -126,6 +159,7 @@ function getViewModelRows( placeholder: mapping.automatic ? option?.label : 'Choose', handlerKey: mapping.handler?.key ?? null, reducerId: mapping.reducerId, + handlerArguments: mapping.handlerArguments, }); } @@ -140,6 +174,7 @@ function getViewModelRows( configOption: configHandlerToSelectOption(handler, false), missingInFrame: true, reducerId: mapping.reducerId ?? ReducerID.lastNotNull, + handlerArguments: {}, }); } } diff --git a/public/app/features/transformers/fieldToConfigMapping/fieldToConfigMapping.ts b/public/app/features/transformers/fieldToConfigMapping/fieldToConfigMapping.ts index 8eca18f14d1..b059f29cc20 100644 --- a/public/app/features/transformers/fieldToConfigMapping/fieldToConfigMapping.ts +++ b/public/app/features/transformers/fieldToConfigMapping/fieldToConfigMapping.ts @@ -15,10 +15,19 @@ import { FieldType, } from '@grafana/data'; +export interface ThresholdArguments { + color: string; +} + +export interface HandlerArguments { + threshold?: ThresholdArguments; +} + export interface FieldToConfigMapping { fieldName: string; reducerId?: ReducerID; handlerKey: string | null; + handlerArguments?: HandlerArguments; } /** @@ -59,7 +68,7 @@ export function getFieldConfigFromFrame( continue; } - const newValue = handler.processor(configValue, config, context); + const newValue = handler.processor(configValue, config, context, mapping.handlerArguments); if (newValue != null) { (config as any)[handler.targetProperty ?? handler.key] = newValue; } @@ -78,7 +87,12 @@ interface FieldToConfigContext { mappingTexts?: string[]; } -type FieldToConfigMapHandlerProcessor = (value: any, config: FieldConfig, context: FieldToConfigContext) => any; +type FieldToConfigMapHandlerProcessor = ( + value: any, + config: FieldConfig, + context: FieldToConfigContext, + handlerArguments: HandlerArguments +) => any; export interface FieldToConfigMapHandler { key: string; @@ -143,8 +157,9 @@ export const configMapHandlers: FieldToConfigMapHandler[] = [ }, { key: 'threshold1', + name: 'Threshold', targetProperty: 'thresholds', - processor: (value, config) => { + processor: (value, config, _, handlerArguments) => { const numeric = anyToNumber(value); if (isNaN(numeric)) { @@ -160,7 +175,7 @@ export const configMapHandlers: FieldToConfigMapHandler[] = [ config.thresholds.steps.push({ value: numeric, - color: 'red', + color: handlerArguments.threshold?.color ?? 'red', }); return config.thresholds; @@ -278,6 +293,7 @@ export function lookUpConfigHandler(key: string | null): FieldToConfigMapHandler export interface EvaluatedMapping { automatic: boolean; handler: FieldToConfigMapHandler | null; + handlerArguments: HandlerArguments; reducerId: ReducerID; } export interface EvaluatedMappingResult { @@ -286,7 +302,7 @@ export interface EvaluatedMappingResult { valueField?: Field; } -export function evaluteFieldMappings( +export function evaluateFieldMappings( frame: DataFrame, mappings: FieldToConfigMapping[], withNameAndValue?: boolean @@ -337,6 +353,7 @@ export function evaluteFieldMappings( result.index[fieldName] = { automatic: !mapping, handler: handler, + handlerArguments: mapping?.handlerArguments ?? {}, reducerId: mapping?.reducerId ?? handler?.defaultReducer ?? ReducerID.lastNotNull, }; } diff --git a/public/app/features/transformers/rowsToFields/rowsToFields.ts b/public/app/features/transformers/rowsToFields/rowsToFields.ts index 51f0b9fc911..6232f3f5546 100644 --- a/public/app/features/transformers/rowsToFields/rowsToFields.ts +++ b/public/app/features/transformers/rowsToFields/rowsToFields.ts @@ -4,7 +4,7 @@ import { DataFrame, DataTransformerID, DataTransformerInfo, Field, getFieldDispl import { EvaluatedMappingResult, - evaluteFieldMappings, + evaluateFieldMappings, FieldConfigHandlerKey, FieldToConfigMapping, getFieldConfigFromFrame, @@ -35,7 +35,7 @@ export const rowsToFieldsTransformer: DataTransformerInfo
Field Use asSelectAdditional settings
+ onChangeHandlerArguments(row, args)} + /> +