mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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 <jev.forsberg@grafana.com>
This commit is contained in:
parent
5b0b8cb4bf
commit
188aed05f9
@ -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'] },
|
||||
],
|
||||
|
@ -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) {
|
||||
|
@ -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<Props>) => {
|
||||
const editorProps = { ...props, ...testProps };
|
||||
return render(<FieldConfigMappingHandlerArgumentsEditor {...editorProps} />);
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
@ -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' && (
|
||||
<Input
|
||||
type="text"
|
||||
value={'Threshold color'}
|
||||
aria-label={'Threshold color'}
|
||||
disabled
|
||||
width={20}
|
||||
prefix={
|
||||
<ColorPicker
|
||||
color={handlerArguments.threshold?.color ?? 'red'}
|
||||
onChange={onChangeThreshold}
|
||||
enableNamedColors={true}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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<string>
|
||||
>;
|
||||
const hasAdditionalSettings = mappings.reduce(
|
||||
(prev, mapping) => prev || createsArgumentsEditor(mapping.handlerKey),
|
||||
false
|
||||
);
|
||||
|
||||
const onChangeConfigProperty = (row: FieldToConfigRowViewModel, value: SelectableValue<string | null>) => {
|
||||
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 (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
@ -67,6 +89,7 @@ export function FieldToConfigMappingEditor({ frame, mappings, onChange, withRedu
|
||||
<th>Field</th>
|
||||
<th>Use as</th>
|
||||
{withReducers && <th>Select</th>}
|
||||
{hasAdditionalSettings && <th>Additional settings</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -91,6 +114,15 @@ export function FieldToConfigMappingEditor({ frame, mappings, onChange, withRedu
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{hasAdditionalSettings && (
|
||||
<td data-testid={`${row.fieldName}-handler-arg`} className={styles.selectCell}>
|
||||
<FieldConfigMappingHandlerArgumentsEditor
|
||||
handlerKey={row.handlerKey}
|
||||
handlerArguments={row.handlerArguments}
|
||||
onChange={(args) => onChangeHandlerArguments(row, args)}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@ -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: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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<RowToFieldsTransformOp
|
||||
};
|
||||
|
||||
export function rowsToFields(options: RowToFieldsTransformOptions, data: DataFrame): DataFrame {
|
||||
const mappingResult = evaluteFieldMappings(data, options.mappings ?? [], true);
|
||||
const mappingResult = evaluateFieldMappings(data, options.mappings ?? [], true);
|
||||
const { nameField, valueField } = mappingResult;
|
||||
|
||||
if (!nameField || !valueField) {
|
||||
|
Loading…
Reference in New Issue
Block a user