mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
OptionsUI: add standard field name picker (#36732)
This commit is contained in:
parent
2f595fa144
commit
6d87b26d6c
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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)} />}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
];
|
||||
};
|
||||
|
@ -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',
|
Loading…
Reference in New Issue
Block a user