OptionsUI: add standard field name picker (#36732)

This commit is contained in:
Ryan McKinley 2021-07-14 11:54:58 -07:00 committed by GitHub
parent 2f595fa144
commit 6d87b26d6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 140 additions and 11 deletions

View File

@ -1,4 +1,5 @@
import { DataLink, FieldOverrideContext, SelectableValue, ThresholdsConfig, ValueMapping } from '../../types';
import { ComponentType } from 'react';
import { DataLink, Field, FieldOverrideContext, SelectableValue, ThresholdsConfig, ValueMapping } from '../../types';
export const identityOverrideProcessor = <T>(value: T, _context: FieldOverrideContext, _settings: any) => {
return value;
@ -158,3 +159,27 @@ export interface StatsPickerConfigSettings {
*/
defaultStat?: string;
}
interface FieldNamePickerInfoProps {
name?: string;
field?: Field;
}
export interface FieldNamePickerConfigSettings {
/**
* Function is a predicate, to test each element of the array.
* Return a value that coerces to true to keep the field, or to false otherwise.
*/
filter?: (field: Field) => boolean;
/**
* Show this text when no values are found
*/
noFieldsMessage?: string;
/**
* When a field is selected, this component can show aditional
* information, including validation etc
*/
info?: ComponentType<FieldNamePickerInfoProps> | null;
}

View File

@ -15,6 +15,7 @@ import {
identityOverrideProcessor,
UnitFieldConfigSettings,
unitOverrideProcessor,
FieldNamePickerConfigSettings,
} from '../field';
/**
@ -235,4 +236,14 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
editor: standardEditorsRegistry.get('unit').editor as any,
});
}
addFieldNamePicker<TSettings = any>(
config: PanelOptionsEditorConfig<TOptions, TSettings & FieldNamePickerConfigSettings, string>
): this {
return this.addCustomEditor({
...config,
id: config.path,
editor: standardEditorsRegistry.get('field-name').editor as any,
});
}
}

View File

@ -0,0 +1,39 @@
import React, { useCallback } from 'react';
import { FieldNamePickerConfigSettings, SelectableValue, StandardEditorProps } from '@grafana/data';
import { Select } from '../Select/Select';
import { useFieldDisplayNames, useSelectOptions, frameHasName } from './utils';
// Pick a field name out of the fulds
export const FieldNamePicker: React.FC<StandardEditorProps<string, FieldNamePickerConfigSettings>> = ({
value,
onChange,
context,
item,
}) => {
const settings: FieldNamePickerConfigSettings = item.settings ?? {};
const names = useFieldDisplayNames(context.data, settings?.filter);
const selectOptions = useSelectOptions(names, value);
const onSelectChange = useCallback(
(selection: SelectableValue<string>) => {
if (!frameHasName(selection.value, names)) {
return;
}
return onChange(selection.value!);
},
[names, onChange]
);
const selectedOption = selectOptions.find((v) => v.value === value);
return (
<>
<Select
value={selectedOption}
options={selectOptions}
onChange={onSelectChange}
noOptionsMessage={settings.noFieldsMessage}
/>
{settings.info && <settings.info name={value} field={names.fields.get(value)} />}
</>
);
};

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { DataFrame, getFieldDisplayName, SelectableValue } from '@grafana/data';
import { DataFrame, Field, getFieldDisplayName, SelectableValue } from '@grafana/data';
/**
* @internal
@ -10,6 +10,9 @@ export interface FrameFieldsDisplayNames {
// raw field names (that are explicitly not visible)
raw: Set<string>;
// Field mappings (duplicates are not supported)
fields: Map<string, Field>;
}
/**
@ -25,18 +28,24 @@ export function frameHasName(name: string | undefined, names: FrameFieldsDisplay
/**
* Retuns the distinct names in a set of frames
*/
function getFrameFieldsDisplayNames(data: DataFrame[]): FrameFieldsDisplayNames {
function getFrameFieldsDisplayNames(data: DataFrame[], filter?: (field: Field) => boolean): FrameFieldsDisplayNames {
const names: FrameFieldsDisplayNames = {
display: new Set<string>(),
raw: new Set<string>(),
fields: new Map<string, Field>(),
};
for (const frame of data) {
for (const field of frame.fields) {
if (filter && !filter(field)) {
continue;
}
const disp = getFieldDisplayName(field, frame, data);
names.display.add(disp);
names.fields.set(disp, field);
if (field.name && disp !== field.name) {
names.raw.add(field.name);
names.fields.set(field.name, field);
}
}
}
@ -46,10 +55,10 @@ function getFrameFieldsDisplayNames(data: DataFrame[]): FrameFieldsDisplayNames
/**
* @internal
*/
export function useFieldDisplayNames(data: DataFrame[]): FrameFieldsDisplayNames {
export function useFieldDisplayNames(data: DataFrame[], filter?: (field: Field) => boolean): FrameFieldsDisplayNames {
return useMemo(() => {
return getFrameFieldsDisplayNames(data);
}, [data]);
return getFrameFieldsDisplayNames(data, filter);
}, [data, filter]);
}
/**

View File

@ -23,6 +23,7 @@ import {
FieldColorConfigSettings,
StatsPickerConfigSettings,
displayNameOverrideProcessor,
FieldNamePickerConfigSettings,
} from '@grafana/data';
import { Switch } from '../components/Switch/Switch';
@ -43,6 +44,7 @@ import { DataLinksValueEditor } from '../components/OptionsUI/links';
import { ColorValueEditor } from '../components/OptionsUI/color';
import { FieldColorEditor } from '../components/OptionsUI/fieldColor';
import { StatsPickerEditor } from '../components/OptionsUI/stats';
import { FieldNamePicker } from '../components/MatchersUI/FieldNamePicker';
/**
* Returns collection of common field config properties definitions
@ -347,6 +349,13 @@ export const getStandardOptionEditors = () => {
editor: TimeZonePicker as any,
};
const fieldName: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
id: 'field-name',
name: 'Field name',
description: 'Time zone selection',
editor: FieldNamePicker as any,
};
return [
text,
number,
@ -364,5 +373,6 @@ export const getStandardOptionEditors = () => {
fieldColor,
color,
multiSelect,
fieldName,
];
};

View File

@ -1,4 +1,5 @@
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, GrafanaTheme2, reduceField, ReducerID, FieldCalcs } from '@grafana/data';
import React from 'react';
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, GrafanaTheme2, reduceField, ReducerID, FieldCalcs, FieldType } from '@grafana/data';
import { dataFrameToPoints } from './utils'
import { FieldMappingOptions, QueryFormat } from '../../types'
import Map from 'ol/Map';
@ -133,31 +134,65 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
],
},
})
.addTextInput({
.addFieldNamePicker({
path: 'fieldMapping.latitudeField',
name: 'Latitude Field',
defaultValue: defaultOptions.fieldMapping.latitudeField,
settings: {
filter: (f) => f.type === FieldType.number,
noFieldsMessage: 'No numeric fields found',
},
showIf: (config) =>
config.queryFormat.locationType === 'coordinates',
})
.addTextInput({
.addFieldNamePicker({
path: 'fieldMapping.longitudeField',
name: 'Longitude Field',
defaultValue: defaultOptions.fieldMapping.longitudeField,
settings: {
filter: (f) => f.type === FieldType.number,
noFieldsMessage: 'No numeric fields found',
},
showIf: (config) =>
config.queryFormat.locationType === 'coordinates',
})
.addTextInput({
.addFieldNamePicker({
path: 'fieldMapping.geohashField',
name: 'Geohash Field',
defaultValue: defaultOptions.fieldMapping.geohashField,
settings: {
filter: (f) => f.type === FieldType.string,
noFieldsMessage: 'No strings fields found',
info: ({
name,
field,
}) => {
if(!name || !field) {
return <div>Select a field that contains <a href="https://en.wikipedia.org/wiki/Geohash">geohash</a> values in each row.</div>
}
const first = reduceField({field, reducers:[ReducerID.firstNotNull]})[ReducerID.firstNotNull] as string;
if(!first) {
return <div>No values found</div>
}
// const coords = decodeGeohash(first);
// if(coords) {
// return <div>First value: {`${coords}`} // {new Date().toISOString()}</div>
// }
// return <div>Invalid first value: {`${first}`}</div>;
return null;
}
},
showIf: (config) =>
config.queryFormat.locationType === 'geohash',
})
.addTextInput({
.addFieldNamePicker({
path: 'fieldMapping.metricField',
name: 'Metric Field',
defaultValue: defaultOptions.fieldMapping.metricField,
settings: {
filter: (f) => f.type === FieldType.number,
noFieldsMessage: 'No numeric fields found',
},
})
.addNumberInput({
path: 'minSize',