mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FieldType: Add enum type and include it in testdata scenarios (#64059)
This commit is contained in:
@@ -6,7 +6,7 @@ import { getFieldTypeFromValue } from '../dataframe/processDataFrame';
|
||||
import { toUtc, dateTimeParse } from '../datetime';
|
||||
import { GrafanaTheme2 } from '../themes/types';
|
||||
import { KeyValue, TimeZone } from '../types';
|
||||
import { Field, FieldType } from '../types/dataFrame';
|
||||
import { EnumFieldConfig, Field, FieldType } from '../types/dataFrame';
|
||||
import { DecimalCount, DisplayProcessor, DisplayValue } from '../types/displayValue';
|
||||
import { anyToNumber } from '../utils/anyToNumber';
|
||||
import { getValueMappingResult } from '../utils/valueMappings';
|
||||
@@ -70,6 +70,8 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
|
||||
}
|
||||
} else if (!unit && field.type === FieldType.string) {
|
||||
unit = 'string';
|
||||
} else if (field.type === FieldType.enum) {
|
||||
return getEnumDisplayProcessor(options.theme, config.type?.enum);
|
||||
}
|
||||
|
||||
const hasCurrencyUnit = unit?.startsWith('currency');
|
||||
@@ -190,6 +192,41 @@ function toStringProcessor(value: unknown): DisplayValue {
|
||||
return { text: toString(value), numeric: anyToNumber(value) };
|
||||
}
|
||||
|
||||
export function getEnumDisplayProcessor(theme: GrafanaTheme2, cfg?: EnumFieldConfig): DisplayProcessor {
|
||||
const config = {
|
||||
text: cfg?.text ?? [],
|
||||
color: cfg?.color ?? [],
|
||||
};
|
||||
// use the theme specific color values
|
||||
config.color = config.color.map((v) => theme.visualization.getColorByName(v));
|
||||
|
||||
return (value: unknown) => {
|
||||
if (value == null) {
|
||||
return {
|
||||
text: '',
|
||||
numeric: NaN,
|
||||
};
|
||||
}
|
||||
const idx = +value;
|
||||
let text = config.text[idx];
|
||||
if (text == null) {
|
||||
text = `${value}`; // the original value
|
||||
}
|
||||
let color = config.color[idx];
|
||||
if (color == null) {
|
||||
// constant color for index
|
||||
const { palette } = theme.visualization;
|
||||
color = palette[idx % palette.length];
|
||||
config.color[idx] = color;
|
||||
}
|
||||
return {
|
||||
text,
|
||||
numeric: idx,
|
||||
color,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function getRawDisplayProcessor(): DisplayProcessor {
|
||||
return (value: unknown) => ({
|
||||
text: getFieldTypeFromValue(value) === 'other' ? `${JSON.stringify(value, getCircularReplacer())}` : `${value}`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { map } from 'rxjs/operators';
|
||||
|
||||
import { dateTimeParse } from '../../datetime';
|
||||
import { SynchronousDataTransformerInfo } from '../../types';
|
||||
import { DataFrame, Field, FieldType } from '../../types/dataFrame';
|
||||
import { DataFrame, EnumFieldConfig, Field, FieldType } from '../../types/dataFrame';
|
||||
import { ArrayVector } from '../../vector';
|
||||
import { fieldMatchers } from '../matchers';
|
||||
import { FieldMatcherID } from '../matchers/ids';
|
||||
@@ -26,6 +26,9 @@ export interface ConvertFieldTypeOptions {
|
||||
* Date format to parse a string datetime
|
||||
*/
|
||||
dateFormat?: string;
|
||||
|
||||
/** When converting to an enumeration, this is the target config */
|
||||
enumConfig?: EnumFieldConfig;
|
||||
}
|
||||
|
||||
export const convertFieldTypeTransformer: SynchronousDataTransformerInfo<ConvertFieldTypeTransformerOptions> = {
|
||||
@@ -44,11 +47,7 @@ export const convertFieldTypeTransformer: SynchronousDataTransformerInfo<Convert
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return data;
|
||||
}
|
||||
const timeParsed = convertFieldTypes(options, data);
|
||||
if (!timeParsed) {
|
||||
return [];
|
||||
}
|
||||
return timeParsed;
|
||||
return convertFieldTypes(options, data) ?? [];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -101,6 +100,8 @@ export function convertFieldType(field: Field, opts: ConvertFieldTypeOptions): F
|
||||
return fieldToStringField(field, opts.dateFormat);
|
||||
case FieldType.boolean:
|
||||
return fieldToBooleanField(field);
|
||||
case FieldType.enum:
|
||||
return fieldToEnumField(field, opts.enumConfig);
|
||||
case FieldType.other:
|
||||
return fieldToComplexField(field);
|
||||
default:
|
||||
@@ -241,3 +242,37 @@ export function ensureTimeField(field: Field, dateFormat?: string): Field {
|
||||
}
|
||||
return fieldToTimeField(field, dateFormat);
|
||||
}
|
||||
|
||||
function fieldToEnumField(field: Field, cfg?: EnumFieldConfig): Field {
|
||||
const enumConfig = { ...cfg };
|
||||
const enumValues = field.values.toArray().slice();
|
||||
const lookup = new Map<unknown, number>();
|
||||
if (enumConfig.text) {
|
||||
for (let i = 0; i < enumConfig.text.length; i++) {
|
||||
lookup.set(enumConfig.text[i], i);
|
||||
}
|
||||
} else {
|
||||
enumConfig.text = [];
|
||||
}
|
||||
|
||||
for (let i = 0; i < enumValues.length; i++) {
|
||||
const v = enumValues[i];
|
||||
if (!lookup.has(v)) {
|
||||
enumConfig.text[lookup.size] = v;
|
||||
lookup.set(v, lookup.size);
|
||||
}
|
||||
enumValues[i] = lookup.get(v);
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
config: {
|
||||
...field.config,
|
||||
type: {
|
||||
enum: enumConfig,
|
||||
},
|
||||
},
|
||||
type: FieldType.enum,
|
||||
values: new ArrayVector(enumValues),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export enum FieldType {
|
||||
// Used to detect that the value is some kind of trace data to help with the visualisation and processing.
|
||||
trace = 'trace',
|
||||
geo = 'geo',
|
||||
enum = 'enum',
|
||||
other = 'other', // Object, Array, etc
|
||||
}
|
||||
|
||||
@@ -91,10 +92,24 @@ export interface FieldConfig<TOptions = any> {
|
||||
// Alternative to empty string
|
||||
noValue?: string;
|
||||
|
||||
// The field type may map to specific config
|
||||
type?: FieldTypeConfig;
|
||||
|
||||
// Panel Specific Values
|
||||
custom?: TOptions;
|
||||
}
|
||||
|
||||
export interface FieldTypeConfig {
|
||||
enum?: EnumFieldConfig;
|
||||
}
|
||||
|
||||
export interface EnumFieldConfig {
|
||||
text?: string[];
|
||||
color?: string[];
|
||||
icon?: string[];
|
||||
description?: string[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ValueLinkConfig {
|
||||
/**
|
||||
|
||||
@@ -147,6 +147,7 @@ export const availableIconsIndex = {
|
||||
link: true,
|
||||
'list-ui-alt': true,
|
||||
'list-ul': true,
|
||||
'list-ol': true,
|
||||
lock: true,
|
||||
'map-marker': true,
|
||||
message: true,
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { memo, useMemo, useCallback } from 'react';
|
||||
|
||||
import { FieldMatcherID, fieldMatchers, SelectableValue, FieldType, DataFrame } from '@grafana/data';
|
||||
|
||||
import { getFieldTypeIconName } from '../../types/icon';
|
||||
import { Select } from '../Select/Select';
|
||||
|
||||
import { MatcherUIProps, FieldMatcherUIRegistryItem } from './types';
|
||||
@@ -23,19 +24,22 @@ export const FieldTypeMatcherEditor = memo<MatcherUIProps<string>>((props) => {
|
||||
});
|
||||
FieldTypeMatcherEditor.displayName = 'FieldTypeMatcherEditor';
|
||||
|
||||
const allTypes: Array<SelectableValue<FieldType>> = [
|
||||
{ value: FieldType.number, label: 'Numeric' },
|
||||
{ value: FieldType.string, label: 'String' },
|
||||
{ value: FieldType.time, label: 'Time' },
|
||||
{ value: FieldType.boolean, label: 'Boolean' },
|
||||
{ value: FieldType.trace, label: 'Traces' },
|
||||
{ value: FieldType.other, label: 'Other' },
|
||||
// Select options for all field types.
|
||||
// This is not eported to the published package, but used internally
|
||||
export const allFieldTypeIconOptions: Array<SelectableValue<FieldType>> = [
|
||||
{ value: FieldType.number, label: 'Number', icon: getFieldTypeIconName(FieldType.number) },
|
||||
{ value: FieldType.string, label: 'String', icon: getFieldTypeIconName(FieldType.string) },
|
||||
{ value: FieldType.time, label: 'Time', icon: getFieldTypeIconName(FieldType.time) },
|
||||
{ value: FieldType.boolean, label: 'Boolean', icon: getFieldTypeIconName(FieldType.boolean) },
|
||||
{ value: FieldType.trace, label: 'Traces', icon: getFieldTypeIconName(FieldType.trace) },
|
||||
{ value: FieldType.enum, label: 'Enum', icon: getFieldTypeIconName(FieldType.enum) },
|
||||
{ value: FieldType.other, label: 'Other', icon: getFieldTypeIconName(FieldType.other) },
|
||||
];
|
||||
|
||||
const useFieldCounts = (data: DataFrame[]): Map<FieldType, number> => {
|
||||
return useMemo(() => {
|
||||
const counts: Map<FieldType, number> = new Map();
|
||||
for (const t of allTypes) {
|
||||
for (const t of allFieldTypeIconOptions) {
|
||||
counts.set(t.value!, 0);
|
||||
}
|
||||
for (const frame of data) {
|
||||
@@ -56,7 +60,7 @@ const useSelectOptions = (counts: Map<string, number>, opt?: string): Array<Sele
|
||||
return useMemo(() => {
|
||||
let found = false;
|
||||
const options: Array<SelectableValue<string>> = [];
|
||||
for (const t of allTypes) {
|
||||
for (const t of allFieldTypeIconOptions) {
|
||||
const count = counts.get(t.value!);
|
||||
const match = opt === t.value;
|
||||
if (count || match) {
|
||||
|
||||
@@ -13,12 +13,16 @@ export type IconSize = ComponentSize | 'xl' | 'xxl' | 'xxxl';
|
||||
export const getAvailableIcons = () => Object.keys(availableIconsIndex);
|
||||
|
||||
/**
|
||||
* Get the icon for a given field type
|
||||
* @deprecated use getFieldTypeIconName
|
||||
* Get the icon for a given field
|
||||
*/
|
||||
export function getFieldTypeIcon(field?: Field): IconName {
|
||||
if (field) {
|
||||
switch (field.type) {
|
||||
return getFieldTypeIconName(field?.type);
|
||||
}
|
||||
|
||||
/** Get an icon for a given field type */
|
||||
export function getFieldTypeIconName(type?: FieldType): IconName {
|
||||
if (type) {
|
||||
switch (type) {
|
||||
case FieldType.time:
|
||||
return 'clock-nine';
|
||||
case FieldType.string:
|
||||
@@ -29,6 +33,8 @@ export function getFieldTypeIcon(field?: Field): IconName {
|
||||
return 'toggle-on';
|
||||
case FieldType.trace:
|
||||
return 'info-circle';
|
||||
case FieldType.enum:
|
||||
return 'list-ol';
|
||||
case FieldType.geo:
|
||||
return 'map-marker';
|
||||
case FieldType.other:
|
||||
@@ -37,24 +43,3 @@ export function getFieldTypeIcon(field?: Field): IconName {
|
||||
}
|
||||
return 'question-circle';
|
||||
}
|
||||
|
||||
/** Get the icon for a given field type */
|
||||
export function getFieldTypeIconName(fieldType?: FieldType): IconName {
|
||||
switch (fieldType) {
|
||||
case FieldType.time:
|
||||
return 'clock-nine';
|
||||
case FieldType.string:
|
||||
return 'font';
|
||||
case FieldType.number:
|
||||
return 'calculator-alt';
|
||||
case FieldType.boolean:
|
||||
return 'toggle-on';
|
||||
case FieldType.trace:
|
||||
return 'info-circle';
|
||||
case FieldType.geo:
|
||||
return 'map-marker';
|
||||
case FieldType.other:
|
||||
return 'brackets-curly';
|
||||
}
|
||||
return 'question-circle';
|
||||
}
|
||||
|
||||
@@ -712,15 +712,29 @@ func randomWalkTable(query backend.DataQuery, model *simplejson.Json) *data.Fram
|
||||
walker := model.Get("startValue").MustFloat64(rand.Float64() * 100)
|
||||
spread := 2.5
|
||||
|
||||
stateField := data.NewFieldFromFieldType(data.FieldTypeEnum, 0)
|
||||
stateField.Name = "State"
|
||||
stateField.Config = &data.FieldConfig{
|
||||
TypeConfig: &data.FieldTypeConfig{
|
||||
Enum: &data.EnumFieldConfig{
|
||||
Text: []string{
|
||||
"Unknown", "Up", "Down", // 0,1,2
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
frame := data.NewFrame(query.RefID,
|
||||
data.NewField("Time", nil, []*time.Time{}),
|
||||
data.NewField("Value", nil, []*float64{}),
|
||||
data.NewField("Min", nil, []*float64{}),
|
||||
data.NewField("Max", nil, []*float64{}),
|
||||
data.NewField("Info", nil, []*string{}),
|
||||
stateField,
|
||||
)
|
||||
|
||||
var info strings.Builder
|
||||
state := data.EnumItemIndex(0)
|
||||
|
||||
for i := int64(0); i < query.MaxDataPoints && timeWalkerMs < to; i++ {
|
||||
delta := rand.Float64() - 0.5
|
||||
@@ -729,8 +743,10 @@ func randomWalkTable(query backend.DataQuery, model *simplejson.Json) *data.Fram
|
||||
info.Reset()
|
||||
if delta > 0 {
|
||||
info.WriteString("up")
|
||||
state = 1
|
||||
} else {
|
||||
info.WriteString("down")
|
||||
state = 2
|
||||
}
|
||||
if math.Abs(delta) > .4 {
|
||||
info.WriteString(" fast")
|
||||
@@ -748,11 +764,12 @@ func randomWalkTable(query backend.DataQuery, model *simplejson.Json) *data.Fram
|
||||
for i := range vals {
|
||||
if rand.Float64() > .2 {
|
||||
vals[i] = nil
|
||||
state = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frame.AppendRow(&t, vals[0], vals[1], vals[2], &infoString)
|
||||
frame.AppendRow(&t, vals[0], vals[1], vals[2], &infoString, state)
|
||||
|
||||
timeWalkerMs += query.Interval.Milliseconds()
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestTestdataScenarios(t *testing.T) {
|
||||
require.Len(t, dResp.Frames, 1)
|
||||
frame := dResp.Frames[0]
|
||||
require.Greater(t, frame.Rows(), 50)
|
||||
require.Len(t, frame.Fields, 5)
|
||||
require.Len(t, frame.Fields, 6)
|
||||
require.Equal(t, "Time", frame.Fields[0].Name)
|
||||
require.Equal(t, "Value", frame.Fields[1].Name)
|
||||
require.Equal(t, "Min", frame.Fields[2].Name)
|
||||
@@ -152,12 +152,13 @@ func TestTestdataScenarios(t *testing.T) {
|
||||
require.Len(t, dResp.Frames, 1)
|
||||
frame := dResp.Frames[0]
|
||||
require.Greater(t, frame.Rows(), 50)
|
||||
require.Len(t, frame.Fields, 5)
|
||||
require.Len(t, frame.Fields, 6)
|
||||
require.Equal(t, "Time", frame.Fields[0].Name)
|
||||
require.Equal(t, "Value", frame.Fields[1].Name)
|
||||
require.Equal(t, "Min", frame.Fields[2].Name)
|
||||
require.Equal(t, "Max", frame.Fields[3].Name)
|
||||
require.Equal(t, "Info", frame.Fields[4].Name)
|
||||
require.Equal(t, "State", frame.Fields[5].Name)
|
||||
|
||||
valNil := false
|
||||
minNil := false
|
||||
|
||||
@@ -14,8 +14,10 @@ import {
|
||||
ConvertFieldTypeOptions,
|
||||
ConvertFieldTypeTransformerOptions,
|
||||
} from '@grafana/data/src/transformations/transformers/convertFieldType';
|
||||
import { Button, InlineField, InlineFieldRow, Input, Select, getFieldTypeIconName } from '@grafana/ui';
|
||||
import { Button, InlineField, InlineFieldRow, Input, Select } from '@grafana/ui';
|
||||
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
|
||||
import { allFieldTypeIconOptions } from '@grafana/ui/src/components/MatchersUI/FieldTypeMatcherEditor';
|
||||
import { hasAlphaPanels } from 'app/core/config';
|
||||
import { findField } from 'app/features/dimensions';
|
||||
|
||||
const fieldNamePickerSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
|
||||
@@ -27,18 +29,12 @@ export const ConvertFieldTypeTransformerEditor = ({
|
||||
options,
|
||||
onChange,
|
||||
}: TransformerUIProps<ConvertFieldTypeTransformerOptions>) => {
|
||||
const allTypes: Array<SelectableValue<FieldType>> = [
|
||||
{ value: FieldType.number, label: 'Number', icon: getFieldTypeIconName(FieldType.number) },
|
||||
{ value: FieldType.string, label: 'String', icon: getFieldTypeIconName(FieldType.string) },
|
||||
{ value: FieldType.time, label: 'Time', icon: getFieldTypeIconName(FieldType.time) },
|
||||
{ value: FieldType.boolean, label: 'Boolean', icon: getFieldTypeIconName(FieldType.boolean) },
|
||||
{ value: FieldType.other, label: 'JSON', icon: getFieldTypeIconName(FieldType.other) },
|
||||
];
|
||||
const allTypes = allFieldTypeIconOptions.filter((v) => v.value !== FieldType.trace);
|
||||
|
||||
const onSelectField = useCallback(
|
||||
(idx: number) => (value: string | undefined) => {
|
||||
const conversions = options.conversions;
|
||||
conversions[idx] = { ...conversions[idx], targetField: value ?? '' };
|
||||
conversions[idx] = { ...conversions[idx], targetField: value ?? '', dateFormat: undefined };
|
||||
onChange({
|
||||
...options,
|
||||
conversions: conversions,
|
||||
@@ -97,45 +93,65 @@ export const ConvertFieldTypeTransformerEditor = ({
|
||||
<>
|
||||
{options.conversions.map((c: ConvertFieldTypeOptions, idx: number) => {
|
||||
return (
|
||||
<InlineFieldRow key={`${c.targetField}-${idx}`}>
|
||||
<InlineField label={'Field'}>
|
||||
<FieldNamePicker
|
||||
context={{ data: input }}
|
||||
value={c.targetField ?? ''}
|
||||
onChange={onSelectField(idx)}
|
||||
item={fieldNamePickerSettings}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label={'as'}>
|
||||
<Select
|
||||
options={allTypes}
|
||||
value={c.destinationType}
|
||||
placeholder={'Type'}
|
||||
onChange={onSelectDestinationType(idx)}
|
||||
width={18}
|
||||
/>
|
||||
</InlineField>
|
||||
{c.destinationType === FieldType.time && (
|
||||
<InlineField
|
||||
label="Input format"
|
||||
tooltip="Specify the format of the input field so Grafana can parse the date string correctly."
|
||||
>
|
||||
<Input value={c.dateFormat} placeholder={'e.g. YYYY-MM-DD'} onChange={onInputFormat(idx)} width={24} />
|
||||
<div key={`${c.targetField}-${idx}`}>
|
||||
<InlineFieldRow>
|
||||
<InlineField label={'Field'}>
|
||||
<FieldNamePicker
|
||||
context={{ data: input }}
|
||||
value={c.targetField ?? ''}
|
||||
onChange={onSelectField(idx)}
|
||||
item={fieldNamePickerSettings}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
{c.destinationType === FieldType.string && (c.dateFormat || findField(input?.[0], c.targetField)) && (
|
||||
<InlineField label="Date format" tooltip="Specify the output format.">
|
||||
<Input value={c.dateFormat} placeholder={'e.g. YYYY-MM-DD'} onChange={onInputFormat(idx)} width={24} />
|
||||
<InlineField label={'as'}>
|
||||
<Select
|
||||
options={allTypes}
|
||||
value={c.destinationType}
|
||||
placeholder={'Type'}
|
||||
onChange={onSelectDestinationType(idx)}
|
||||
width={18}
|
||||
/>
|
||||
</InlineField>
|
||||
{c.destinationType === FieldType.time && (
|
||||
<InlineField
|
||||
label="Input format"
|
||||
tooltip="Specify the format of the input field so Grafana can parse the date string correctly."
|
||||
>
|
||||
<Input
|
||||
value={c.dateFormat}
|
||||
placeholder={'e.g. YYYY-MM-DD'}
|
||||
onChange={onInputFormat(idx)}
|
||||
width={24}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
{c.destinationType === FieldType.string &&
|
||||
(c.dateFormat || findField(input?.[0], c.targetField)?.type === FieldType.time) && (
|
||||
<InlineField label="Date format" tooltip="Specify the output format.">
|
||||
<Input
|
||||
value={c.dateFormat}
|
||||
placeholder={'e.g. YYYY-MM-DD'}
|
||||
onChange={onInputFormat(idx)}
|
||||
width={24}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
<Button
|
||||
size="md"
|
||||
icon="trash-alt"
|
||||
variant="secondary"
|
||||
onClick={() => onRemoveConvertFieldType(idx)}
|
||||
aria-label={'Remove convert field type transformer'}
|
||||
/>
|
||||
</InlineFieldRow>
|
||||
{c.destinationType === FieldType.enum && hasAlphaPanels && (
|
||||
<InlineFieldRow>
|
||||
<InlineField label={''} labelWidth={6}>
|
||||
<div>TODO... show options here (alpha panels enabled)</div>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
<Button
|
||||
size="md"
|
||||
icon="trash-alt"
|
||||
variant="secondary"
|
||||
onClick={() => onRemoveConvertFieldType(idx)}
|
||||
aria-label={'Remove convert field type transformer'}
|
||||
/>
|
||||
</InlineFieldRow>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user