FieldType: Add enum type and include it in testdata scenarios (#64059)

This commit is contained in:
Ryan McKinley
2023-03-03 13:37:56 -08:00
committed by GitHub
parent aed020d9b5
commit b7d8589588
9 changed files with 200 additions and 89 deletions

View File

@@ -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}`,

View File

@@ -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),
};
}

View File

@@ -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 {
/**

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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';
}