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);
|
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', () => {
|
it('With custom matcher and displayName mapping', () => {
|
||||||
const options: ConfigFromQueryTransformOptions = {
|
const options: ConfigFromQueryTransformOptions = {
|
||||||
configRefId: 'A',
|
configRefId: 'A',
|
||||||
@ -102,6 +116,7 @@ describe('value mapping from data', () => {
|
|||||||
const config = toDataFrame({
|
const config = toDataFrame({
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
|
{ 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: 'text', type: FieldType.string, values: ['one', 'two', 'three'] },
|
||||||
{ name: 'color', type: FieldType.string, values: ['red', 'blue', 'green'] },
|
{ name: 'color', type: FieldType.string, values: ['red', 'blue', 'green'] },
|
||||||
],
|
],
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
evaluteFieldMappings,
|
evaluateFieldMappings,
|
||||||
FieldToConfigMapping,
|
FieldToConfigMapping,
|
||||||
getFieldConfigFromFrame,
|
getFieldConfigFromFrame,
|
||||||
} from '../fieldToConfigMapping/fieldToConfigMapping';
|
} from '../fieldToConfigMapping/fieldToConfigMapping';
|
||||||
@ -42,7 +42,7 @@ export function extractConfigFromQuery(options: ConfigFromQueryTransformOptions,
|
|||||||
length: 1,
|
length: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mappingResult = evaluteFieldMappings(configFrame, options.mappings ?? [], false);
|
const mappingResult = evaluateFieldMappings(configFrame, options.mappings ?? [], false);
|
||||||
|
|
||||||
// reduce config frame
|
// reduce config frame
|
||||||
for (const field of configFrame.fields) {
|
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' }])
|
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 {
|
import {
|
||||||
configMapHandlers,
|
configMapHandlers,
|
||||||
evaluteFieldMappings,
|
evaluateFieldMappings,
|
||||||
FieldToConfigMapHandler,
|
FieldToConfigMapHandler,
|
||||||
FieldToConfigMapping,
|
FieldToConfigMapping,
|
||||||
|
HandlerArguments,
|
||||||
lookUpConfigHandler as findConfigHandlerFor,
|
lookUpConfigHandler as findConfigHandlerFor,
|
||||||
} from '../fieldToConfigMapping/fieldToConfigMapping';
|
} from '../fieldToConfigMapping/fieldToConfigMapping';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createsArgumentsEditor,
|
||||||
|
FieldConfigMappingHandlerArgumentsEditor,
|
||||||
|
} from './FieldConfigMappingHandlerArgumentsEditor';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
frame: DataFrame;
|
frame: DataFrame;
|
||||||
mappings: FieldToConfigMapping[];
|
mappings: FieldToConfigMapping[];
|
||||||
@ -27,6 +33,10 @@ export function FieldToConfigMappingEditor({ frame, mappings, onChange, withRedu
|
|||||||
const configProps = configMapHandlers.map((def) => configHandlerToSelectOption(def, false)) as Array<
|
const configProps = configMapHandlers.map((def) => configHandlerToSelectOption(def, false)) as Array<
|
||||||
SelectableValue<string>
|
SelectableValue<string>
|
||||||
>;
|
>;
|
||||||
|
const hasAdditionalSettings = mappings.reduce(
|
||||||
|
(prev, mapping) => prev || createsArgumentsEditor(mapping.handlerKey),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
const onChangeConfigProperty = (row: FieldToConfigRowViewModel, value: SelectableValue<string | null>) => {
|
const onChangeConfigProperty = (row: FieldToConfigRowViewModel, value: SelectableValue<string | null>) => {
|
||||||
const existingIdx = mappings.findIndex((x) => x.fieldName === row.fieldName);
|
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 (
|
return (
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
@ -67,6 +89,7 @@ export function FieldToConfigMappingEditor({ frame, mappings, onChange, withRedu
|
|||||||
<th>Field</th>
|
<th>Field</th>
|
||||||
<th>Use as</th>
|
<th>Use as</th>
|
||||||
{withReducers && <th>Select</th>}
|
{withReducers && <th>Select</th>}
|
||||||
|
{hasAdditionalSettings && <th>Additional settings</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -91,6 +114,15 @@ export function FieldToConfigMappingEditor({ frame, mappings, onChange, withRedu
|
|||||||
/>
|
/>
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -105,6 +137,7 @@ interface FieldToConfigRowViewModel {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
missingInFrame?: boolean;
|
missingInFrame?: boolean;
|
||||||
reducerId: string;
|
reducerId: string;
|
||||||
|
handlerArguments: HandlerArguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getViewModelRows(
|
function getViewModelRows(
|
||||||
@ -113,7 +146,7 @@ function getViewModelRows(
|
|||||||
withNameAndValue?: boolean
|
withNameAndValue?: boolean
|
||||||
): FieldToConfigRowViewModel[] {
|
): FieldToConfigRowViewModel[] {
|
||||||
const rows: FieldToConfigRowViewModel[] = [];
|
const rows: FieldToConfigRowViewModel[] = [];
|
||||||
const mappingResult = evaluteFieldMappings(frame, mappings ?? [], withNameAndValue);
|
const mappingResult = evaluateFieldMappings(frame, mappings ?? [], withNameAndValue);
|
||||||
|
|
||||||
for (const field of frame.fields) {
|
for (const field of frame.fields) {
|
||||||
const fieldName = getFieldDisplayName(field, frame);
|
const fieldName = getFieldDisplayName(field, frame);
|
||||||
@ -126,6 +159,7 @@ function getViewModelRows(
|
|||||||
placeholder: mapping.automatic ? option?.label : 'Choose',
|
placeholder: mapping.automatic ? option?.label : 'Choose',
|
||||||
handlerKey: mapping.handler?.key ?? null,
|
handlerKey: mapping.handler?.key ?? null,
|
||||||
reducerId: mapping.reducerId,
|
reducerId: mapping.reducerId,
|
||||||
|
handlerArguments: mapping.handlerArguments,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +174,7 @@ function getViewModelRows(
|
|||||||
configOption: configHandlerToSelectOption(handler, false),
|
configOption: configHandlerToSelectOption(handler, false),
|
||||||
missingInFrame: true,
|
missingInFrame: true,
|
||||||
reducerId: mapping.reducerId ?? ReducerID.lastNotNull,
|
reducerId: mapping.reducerId ?? ReducerID.lastNotNull,
|
||||||
|
handlerArguments: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,19 @@ import {
|
|||||||
FieldType,
|
FieldType,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
|
export interface ThresholdArguments {
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HandlerArguments {
|
||||||
|
threshold?: ThresholdArguments;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FieldToConfigMapping {
|
export interface FieldToConfigMapping {
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
reducerId?: ReducerID;
|
reducerId?: ReducerID;
|
||||||
handlerKey: string | null;
|
handlerKey: string | null;
|
||||||
|
handlerArguments?: HandlerArguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,7 +68,7 @@ export function getFieldConfigFromFrame(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newValue = handler.processor(configValue, config, context);
|
const newValue = handler.processor(configValue, config, context, mapping.handlerArguments);
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
(config as any)[handler.targetProperty ?? handler.key] = newValue;
|
(config as any)[handler.targetProperty ?? handler.key] = newValue;
|
||||||
}
|
}
|
||||||
@ -78,7 +87,12 @@ interface FieldToConfigContext {
|
|||||||
mappingTexts?: string[];
|
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 {
|
export interface FieldToConfigMapHandler {
|
||||||
key: string;
|
key: string;
|
||||||
@ -143,8 +157,9 @@ export const configMapHandlers: FieldToConfigMapHandler[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'threshold1',
|
key: 'threshold1',
|
||||||
|
name: 'Threshold',
|
||||||
targetProperty: 'thresholds',
|
targetProperty: 'thresholds',
|
||||||
processor: (value, config) => {
|
processor: (value, config, _, handlerArguments) => {
|
||||||
const numeric = anyToNumber(value);
|
const numeric = anyToNumber(value);
|
||||||
|
|
||||||
if (isNaN(numeric)) {
|
if (isNaN(numeric)) {
|
||||||
@ -160,7 +175,7 @@ export const configMapHandlers: FieldToConfigMapHandler[] = [
|
|||||||
|
|
||||||
config.thresholds.steps.push({
|
config.thresholds.steps.push({
|
||||||
value: numeric,
|
value: numeric,
|
||||||
color: 'red',
|
color: handlerArguments.threshold?.color ?? 'red',
|
||||||
});
|
});
|
||||||
|
|
||||||
return config.thresholds;
|
return config.thresholds;
|
||||||
@ -278,6 +293,7 @@ export function lookUpConfigHandler(key: string | null): FieldToConfigMapHandler
|
|||||||
export interface EvaluatedMapping {
|
export interface EvaluatedMapping {
|
||||||
automatic: boolean;
|
automatic: boolean;
|
||||||
handler: FieldToConfigMapHandler | null;
|
handler: FieldToConfigMapHandler | null;
|
||||||
|
handlerArguments: HandlerArguments;
|
||||||
reducerId: ReducerID;
|
reducerId: ReducerID;
|
||||||
}
|
}
|
||||||
export interface EvaluatedMappingResult {
|
export interface EvaluatedMappingResult {
|
||||||
@ -286,7 +302,7 @@ export interface EvaluatedMappingResult {
|
|||||||
valueField?: Field;
|
valueField?: Field;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function evaluteFieldMappings(
|
export function evaluateFieldMappings(
|
||||||
frame: DataFrame,
|
frame: DataFrame,
|
||||||
mappings: FieldToConfigMapping[],
|
mappings: FieldToConfigMapping[],
|
||||||
withNameAndValue?: boolean
|
withNameAndValue?: boolean
|
||||||
@ -337,6 +353,7 @@ export function evaluteFieldMappings(
|
|||||||
result.index[fieldName] = {
|
result.index[fieldName] = {
|
||||||
automatic: !mapping,
|
automatic: !mapping,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
|
handlerArguments: mapping?.handlerArguments ?? {},
|
||||||
reducerId: mapping?.reducerId ?? handler?.defaultReducer ?? ReducerID.lastNotNull,
|
reducerId: mapping?.reducerId ?? handler?.defaultReducer ?? ReducerID.lastNotNull,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { DataFrame, DataTransformerID, DataTransformerInfo, Field, getFieldDispl
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
EvaluatedMappingResult,
|
EvaluatedMappingResult,
|
||||||
evaluteFieldMappings,
|
evaluateFieldMappings,
|
||||||
FieldConfigHandlerKey,
|
FieldConfigHandlerKey,
|
||||||
FieldToConfigMapping,
|
FieldToConfigMapping,
|
||||||
getFieldConfigFromFrame,
|
getFieldConfigFromFrame,
|
||||||
@ -35,7 +35,7 @@ export const rowsToFieldsTransformer: DataTransformerInfo<RowToFieldsTransformOp
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function rowsToFields(options: RowToFieldsTransformOptions, data: DataFrame): DataFrame {
|
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;
|
const { nameField, valueField } = mappingResult;
|
||||||
|
|
||||||
if (!nameField || !valueField) {
|
if (!nameField || !valueField) {
|
||||||
|
Loading…
Reference in New Issue
Block a user