mirror of https://github.com/grafana/grafana
NewPanelEditor: fluent API for custom field config and panel options creation (#23070)
* Registry of standard option editors * Move override processors to grafana data * API for declaratively creating field config/panel options * Enable declarative API in PanelPlugin for options and field config * Use new api in react table panel * Add color and unit picker to option registries * Add some docs and tests * Fix testspull/23127/head
parent
0c9e50c7da
commit
4eae1b5483
@ -0,0 +1,108 @@ |
||||
import { DataLink, FieldOverrideContext, SelectableValue, ThresholdsConfig, ValueMapping } from '../../types'; |
||||
|
||||
export const identityOverrideProcessor = <T>(value: T, _context: FieldOverrideContext, _settings: any) => { |
||||
return value; |
||||
}; |
||||
|
||||
export interface NumberFieldConfigSettings { |
||||
placeholder?: string; |
||||
integer?: boolean; |
||||
min?: number; |
||||
max?: number; |
||||
step?: number; |
||||
} |
||||
|
||||
export const numberOverrideProcessor = ( |
||||
value: any, |
||||
context: FieldOverrideContext, |
||||
settings: NumberFieldConfigSettings |
||||
) => { |
||||
const v = parseFloat(`${value}`); |
||||
if (settings.max && v > settings.max) { |
||||
// ????
|
||||
} |
||||
return v; |
||||
}; |
||||
|
||||
export interface DataLinksFieldConfigSettings {} |
||||
|
||||
export const dataLinksOverrideProcessor = ( |
||||
value: any, |
||||
_context: FieldOverrideContext, |
||||
_settings: DataLinksFieldConfigSettings |
||||
) => { |
||||
return value as DataLink[]; |
||||
}; |
||||
|
||||
export interface ValueMappingFieldConfigSettings {} |
||||
|
||||
export const valueMappingsOverrideProcessor = ( |
||||
value: any, |
||||
_context: FieldOverrideContext, |
||||
_settings: ValueMappingFieldConfigSettings |
||||
) => { |
||||
return value as ValueMapping[]; // !!!! likely not !!!!
|
||||
}; |
||||
|
||||
export interface SelectFieldConfigSettings<T> { |
||||
options: Array<SelectableValue<T>>; |
||||
} |
||||
|
||||
export const selectOverrideProcessor = ( |
||||
value: any, |
||||
_context: FieldOverrideContext, |
||||
_settings: SelectFieldConfigSettings<any> |
||||
) => { |
||||
return value; |
||||
}; |
||||
|
||||
export interface StringFieldConfigSettings { |
||||
placeholder?: string; |
||||
maxLength?: number; |
||||
expandTemplateVars?: boolean; |
||||
} |
||||
|
||||
export const stringOverrideProcessor = ( |
||||
value: any, |
||||
context: FieldOverrideContext, |
||||
settings: StringFieldConfigSettings |
||||
) => { |
||||
if (settings.expandTemplateVars && context.replaceVariables) { |
||||
return context.replaceVariables(value, context.field!.config.scopedVars); |
||||
} |
||||
return `${value}`; |
||||
}; |
||||
|
||||
export interface ThresholdsFieldConfigSettings { |
||||
// Anything?
|
||||
} |
||||
|
||||
export const thresholdsOverrideProcessor = ( |
||||
value: any, |
||||
_context: FieldOverrideContext, |
||||
_settings: ThresholdsFieldConfigSettings |
||||
) => { |
||||
return value as ThresholdsConfig; // !!!! likely not !!!!
|
||||
}; |
||||
|
||||
export interface UnitFieldConfigSettings {} |
||||
|
||||
export const unitOverrideProcessor = ( |
||||
value: boolean, |
||||
_context: FieldOverrideContext, |
||||
_settings: UnitFieldConfigSettings |
||||
) => { |
||||
return value; |
||||
}; |
||||
|
||||
export const booleanOverrideProcessor = ( |
||||
value: boolean, |
||||
_context: FieldOverrideContext, |
||||
_settings: ThresholdsFieldConfigSettings |
||||
) => { |
||||
return value; // !!!! likely not !!!!
|
||||
}; |
||||
|
||||
export interface ColorFieldConfigSettings { |
||||
enableNamedColors?: boolean; |
||||
} |
@ -1,4 +1,16 @@ |
||||
import { FieldConfigEditorRegistry, FieldPropertyEditorItem } from '../types/fieldOverrides'; |
||||
import { Registry } from '../utils/Registry'; |
||||
import { Registry, RegistryItem } from '../utils/Registry'; |
||||
import { ComponentType } from 'react'; |
||||
|
||||
export interface StandardEditorProps<TValue = any, TSettings = any> { |
||||
value: TValue; |
||||
onChange: (value?: TValue) => void; |
||||
item: StandardEditorsRegistryItem<TValue, TSettings>; |
||||
} |
||||
export interface StandardEditorsRegistryItem<TValue = any, TSettings = any> extends RegistryItem { |
||||
editor: ComponentType<StandardEditorProps<TValue, TSettings>>; |
||||
settings?: TSettings; |
||||
} |
||||
export const standardFieldConfigEditorRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(); |
||||
|
||||
export const standardEditorsRegistry = new Registry<StandardEditorsRegistryItem<any>>(); |
||||
|
@ -0,0 +1,48 @@ |
||||
import React from 'react'; |
||||
import { identityOverrideProcessor } from '../field'; |
||||
import { PanelPlugin } from './PanelPlugin'; |
||||
|
||||
describe('PanelPlugin', () => { |
||||
describe('declarative options', () => { |
||||
test('field config UI API', () => { |
||||
const panel = new PanelPlugin(() => { |
||||
return <div>Panel</div>; |
||||
}); |
||||
|
||||
panel.setCustomFieldConfigEditor(builder => { |
||||
builder.addCustomEditor({ |
||||
id: 'custom', |
||||
name: 'Custom', |
||||
description: 'Custom field config property description', |
||||
editor: () => <div>Editor</div>, |
||||
override: () => <div>Editor</div>, |
||||
process: identityOverrideProcessor, |
||||
settings: {}, |
||||
shouldApply: () => true, |
||||
}); |
||||
}); |
||||
|
||||
expect(panel.customFieldConfigs).toBeDefined(); |
||||
expect(panel.customFieldConfigs!.list()).toHaveLength(1); |
||||
}); |
||||
|
||||
test('options UI API', () => { |
||||
const panel = new PanelPlugin(() => { |
||||
return <div>Panel</div>; |
||||
}); |
||||
|
||||
panel.setOptionsEditor(builder => { |
||||
builder.addCustomEditor({ |
||||
id: 'option', |
||||
name: 'Option editor', |
||||
description: 'Option editor description', |
||||
editor: () => <div>Editor</div>, |
||||
settings: {}, |
||||
}); |
||||
}); |
||||
|
||||
expect(panel.optionEditors).toBeDefined(); |
||||
expect(panel.optionEditors!.list()).toHaveLength(1); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,191 @@ |
||||
import { |
||||
FieldConfigEditorRegistry, |
||||
FieldConfigSource, |
||||
GrafanaPlugin, |
||||
PanelEditorProps, |
||||
PanelMigrationHandler, |
||||
PanelOptionEditorsRegistry, |
||||
PanelPluginMeta, |
||||
PanelProps, |
||||
PanelTypeChangedHandler, |
||||
} from '../types'; |
||||
import { FieldConfigEditorBuilder, PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders'; |
||||
import { ComponentClass, ComponentType } from 'react'; |
||||
|
||||
export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta> { |
||||
private customFieldConfigsUIBuilder = new FieldConfigEditorBuilder(); |
||||
private _customFieldConfigs?: FieldConfigEditorRegistry; |
||||
private registerCustomFieldConfigs?: (builder: FieldConfigEditorBuilder) => void; |
||||
|
||||
private optionsUIBuilder = new PanelOptionsEditorBuilder(); |
||||
private _optionEditors?: PanelOptionEditorsRegistry; |
||||
private registerOptionEditors?: (builder: PanelOptionsEditorBuilder) => void; |
||||
|
||||
panel: ComponentType<PanelProps<TOptions>>; |
||||
editor?: ComponentClass<PanelEditorProps<TOptions>>; |
||||
defaults?: TOptions; |
||||
fieldConfigDefaults?: FieldConfigSource = { |
||||
defaults: {}, |
||||
overrides: [], |
||||
}; |
||||
onPanelMigration?: PanelMigrationHandler<TOptions>; |
||||
onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>; |
||||
noPadding?: boolean; |
||||
|
||||
/** |
||||
* Legacy angular ctrl. If this exists it will be used instead of the panel |
||||
*/ |
||||
angularPanelCtrl?: any; |
||||
|
||||
constructor(panel: ComponentType<PanelProps<TOptions>>) { |
||||
super(); |
||||
this.panel = panel; |
||||
} |
||||
|
||||
get customFieldConfigs() { |
||||
if (!this._customFieldConfigs && this.registerCustomFieldConfigs) { |
||||
this.registerCustomFieldConfigs(this.customFieldConfigsUIBuilder); |
||||
this._customFieldConfigs = this.customFieldConfigsUIBuilder.getRegistry(); |
||||
} |
||||
|
||||
return this._customFieldConfigs; |
||||
} |
||||
|
||||
get optionEditors() { |
||||
if (!this._optionEditors && this.registerOptionEditors) { |
||||
this.registerOptionEditors(this.optionsUIBuilder); |
||||
this._optionEditors = this.optionsUIBuilder.getRegistry(); |
||||
} |
||||
|
||||
return this._optionEditors; |
||||
} |
||||
|
||||
setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) { |
||||
this.editor = editor; |
||||
return this; |
||||
} |
||||
|
||||
setDefaults(defaults: TOptions) { |
||||
this.defaults = defaults; |
||||
return this; |
||||
} |
||||
|
||||
setNoPadding() { |
||||
this.noPadding = true; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* This function is called before the panel first loads if |
||||
* the current version is different than the version that was saved. |
||||
* |
||||
* This is a good place to support any changes to the options model |
||||
*/ |
||||
setMigrationHandler(handler: PanelMigrationHandler) { |
||||
this.onPanelMigration = handler; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* This function is called when the visualization was changed. This |
||||
* passes in the panel model for previous visualisation options inspection |
||||
* and panel model updates. |
||||
* |
||||
* This is useful for supporting PanelModel API updates when changing |
||||
* between Angular and React panels. |
||||
*/ |
||||
setPanelChangeHandler(handler: PanelTypeChangedHandler) { |
||||
this.onPanelTypeChanged = handler; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Enables custom field properties editor creation |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* |
||||
* import { ShapePanel } from './ShapePanel'; |
||||
* |
||||
* interface ShapePanelOptions {} |
||||
* |
||||
* export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel) |
||||
* .setCustomFieldConfigEditor(builder => { |
||||
* builder |
||||
* .addNumberInput({ |
||||
* id: 'shapeBorderWidth', |
||||
* name: 'Border width', |
||||
* description: 'Border width of the shape', |
||||
* settings: { |
||||
* min: 1, |
||||
* max: 5, |
||||
* }, |
||||
* }) |
||||
* .addSelect({ |
||||
* id: 'displayMode', |
||||
* name: 'Display mode', |
||||
* description: 'How the shape shout be rendered' |
||||
* settings: { |
||||
* options: [{value: 'fill', label: 'Fill' }, {value: 'transparent', label: 'Transparent }] |
||||
* }, |
||||
* }) |
||||
* }) |
||||
* ``` |
||||
* |
||||
* @public |
||||
**/ |
||||
setCustomFieldConfigEditor(builder: (builder: FieldConfigEditorBuilder) => void) { |
||||
// builder is applied lazily when custom field configs are accessed
|
||||
this.registerCustomFieldConfigs = builder; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Enables panel options editor creation |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* |
||||
* import { ShapePanel } from './ShapePanel'; |
||||
* |
||||
* interface ShapePanelOptions {} |
||||
* |
||||
* export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel) |
||||
* .setOptionsEditor(builder => { |
||||
* builder |
||||
* .addSelect({ |
||||
* id: 'shape', |
||||
* name: 'Shape', |
||||
* description: 'Select shape to render' |
||||
* settings: { |
||||
* options: [ |
||||
* {value: 'circle', label: 'Circle' }, |
||||
* {value: 'square', label: 'Square }, |
||||
* {value: 'triangle', label: 'Triangle } |
||||
* ] |
||||
* }, |
||||
* }) |
||||
* }) |
||||
* ``` |
||||
* |
||||
* @public |
||||
**/ |
||||
setOptionsEditor(builder: (builder: PanelOptionsEditorBuilder) => void) { |
||||
// builder is applied lazily when options UI is created
|
||||
this.registerOptionEditors = builder; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Enables configuration of panel's default field config |
||||
*/ |
||||
setFieldConfigDefaults(defaultConfig: Partial<FieldConfigSource>) { |
||||
this.fieldConfigDefaults = { |
||||
defaults: {}, |
||||
overrides: [], |
||||
...defaultConfig, |
||||
}; |
||||
|
||||
return this; |
||||
} |
||||
} |
@ -0,0 +1,73 @@ |
||||
import { ComponentType } from 'react'; |
||||
import { RegistryItem, Registry } from '../utils/Registry'; |
||||
import { NumberFieldConfigSettings, SelectFieldConfigSettings, StringFieldConfigSettings } from '../field'; |
||||
|
||||
/** |
||||
* Option editor registry item |
||||
*/ |
||||
interface OptionsEditorItem<TSettings, TEditorProps> extends RegistryItem { |
||||
settings?: TSettings; |
||||
editor?: ComponentType<TEditorProps>; |
||||
} |
||||
|
||||
/** |
||||
* Configuration of option editor registry item |
||||
*/ |
||||
type OptionEditorConfig<TSettings, TEditorProps> = Pick< |
||||
OptionsEditorItem<TSettings, TEditorProps>, |
||||
'id' | 'name' | 'description' | 'editor' | 'settings' |
||||
>; |
||||
|
||||
/** |
||||
* Describes an API for option editors UI builder |
||||
*/ |
||||
export interface OptionsUIRegistryBuilderAPI<TEditorProps, T extends OptionsEditorItem<any, TEditorProps>> { |
||||
addNumberInput?<TSettings extends NumberFieldConfigSettings = NumberFieldConfigSettings>( |
||||
config: OptionEditorConfig<TSettings, TEditorProps> |
||||
): this; |
||||
|
||||
addTextInput?<TSettings extends StringFieldConfigSettings = StringFieldConfigSettings>( |
||||
config: OptionEditorConfig<TSettings, TEditorProps> |
||||
): this; |
||||
|
||||
addSelect?<TOption, TSettings extends SelectFieldConfigSettings<TOption>>( |
||||
config: OptionEditorConfig<TSettings, TEditorProps> |
||||
): this; |
||||
|
||||
addRadio?<TOption, TSettings extends SelectFieldConfigSettings<TOption> = SelectFieldConfigSettings<TOption>>( |
||||
config: OptionEditorConfig<TSettings, TEditorProps> |
||||
): this; |
||||
|
||||
addBooleanSwitch?<TSettings = any>(config: OptionEditorConfig<TSettings, TEditorProps>): this; |
||||
|
||||
addUnitPicker?<TSettings = any>(config: OptionEditorConfig<TSettings, TEditorProps>): this; |
||||
|
||||
addColorPicker?<TSettings = any>(config: OptionEditorConfig<TSettings, TEditorProps>): this; |
||||
|
||||
/** |
||||
* Enables custom editor definition |
||||
* @param config |
||||
*/ |
||||
addCustomEditor<TSettings>(config: OptionsEditorItem<TSettings, TEditorProps>): this; |
||||
|
||||
/** |
||||
* Returns registry of option editors |
||||
*/ |
||||
getRegistry: () => Registry<T>; |
||||
} |
||||
|
||||
export abstract class OptionsUIRegistryBuilder<TEditorProps, T extends OptionsEditorItem<any, TEditorProps>> |
||||
implements OptionsUIRegistryBuilderAPI<TEditorProps, T> { |
||||
private properties: T[] = []; |
||||
|
||||
addCustomEditor<TValue>(config: T & OptionsEditorItem<TValue, TEditorProps>): this { |
||||
this.properties.push(config); |
||||
return this; |
||||
} |
||||
|
||||
getRegistry() { |
||||
return new Registry(() => { |
||||
return this.properties; |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,166 @@ |
||||
import { |
||||
FieldType, |
||||
FieldConfigEditorProps, |
||||
FieldPropertyEditorItem, |
||||
PanelOptionsEditorConfig, |
||||
PanelOptionsEditorItem, |
||||
FieldConfigEditorConfig, |
||||
} from '../types'; |
||||
import { OptionsUIRegistryBuilder } from '../types/OptionsUIRegistryBuilder'; |
||||
import { |
||||
numberOverrideProcessor, |
||||
selectOverrideProcessor, |
||||
stringOverrideProcessor, |
||||
booleanOverrideProcessor, |
||||
standardEditorsRegistry, |
||||
SelectFieldConfigSettings, |
||||
StandardEditorProps, |
||||
StringFieldConfigSettings, |
||||
NumberFieldConfigSettings, |
||||
ColorFieldConfigSettings, |
||||
identityOverrideProcessor, |
||||
UnitFieldConfigSettings, |
||||
unitOverrideProcessor, |
||||
} from '../field'; |
||||
|
||||
/** |
||||
* Fluent API for declarative creation of field config option editors |
||||
*/ |
||||
export class FieldConfigEditorBuilder extends OptionsUIRegistryBuilder< |
||||
FieldConfigEditorProps<any, any>, |
||||
FieldPropertyEditorItem |
||||
> { |
||||
addNumberInput<TSettings>(config: FieldConfigEditorConfig<TSettings & NumberFieldConfigSettings>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
override: standardEditorsRegistry.get('number').editor as any, |
||||
editor: standardEditorsRegistry.get('number').editor as any, |
||||
process: numberOverrideProcessor, |
||||
shouldApply: config.shouldApply ? config.shouldApply : field => field.type === FieldType.number, |
||||
settings: config.settings || {}, |
||||
}); |
||||
} |
||||
|
||||
addTextInput<TSettings>(config: FieldConfigEditorConfig<TSettings & StringFieldConfigSettings>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
override: standardEditorsRegistry.get('text').editor as any, |
||||
editor: standardEditorsRegistry.get('text').editor as any, |
||||
process: stringOverrideProcessor, |
||||
shouldApply: config.shouldApply ? config.shouldApply : field => field.type === FieldType.string, |
||||
settings: config.settings || {}, |
||||
}); |
||||
} |
||||
|
||||
addSelect<TOption, TSettings = any>(config: FieldConfigEditorConfig<TSettings & SelectFieldConfigSettings<TOption>>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
override: standardEditorsRegistry.get('select').editor as any, |
||||
editor: standardEditorsRegistry.get('select').editor as any, |
||||
process: selectOverrideProcessor, |
||||
// ???
|
||||
shouldApply: config.shouldApply ? config.shouldApply : () => true, |
||||
settings: config.settings || { options: [] }, |
||||
}); |
||||
} |
||||
|
||||
addRadio<TOption, TSettings = any>(config: FieldConfigEditorConfig<TSettings & SelectFieldConfigSettings<TOption>>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
override: standardEditorsRegistry.get('radio').editor as any, |
||||
editor: standardEditorsRegistry.get('radio').editor as any, |
||||
process: selectOverrideProcessor, |
||||
// ???
|
||||
shouldApply: config.shouldApply ? config.shouldApply : () => true, |
||||
settings: config.settings || { options: [] }, |
||||
}); |
||||
} |
||||
|
||||
addBooleanSwitch<TSettings = any>(config: FieldConfigEditorConfig<TSettings>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
editor: standardEditorsRegistry.get('boolean').editor as any, |
||||
override: standardEditorsRegistry.get('boolean').editor as any, |
||||
process: booleanOverrideProcessor, |
||||
shouldApply: config.shouldApply ? config.shouldApply : () => true, |
||||
settings: config.settings || {}, |
||||
}); |
||||
} |
||||
|
||||
addColorPicker<TSettings = any>(config: FieldConfigEditorConfig<TSettings & ColorFieldConfigSettings>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
editor: standardEditorsRegistry.get('color').editor as any, |
||||
override: standardEditorsRegistry.get('color').editor as any, |
||||
process: identityOverrideProcessor, |
||||
shouldApply: config.shouldApply ? config.shouldApply : () => true, |
||||
settings: config.settings || {}, |
||||
}); |
||||
} |
||||
|
||||
addUnitPicker<TSettings = any>(config: FieldConfigEditorConfig<TSettings & UnitFieldConfigSettings>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
editor: standardEditorsRegistry.get('unit').editor as any, |
||||
override: standardEditorsRegistry.get('unit').editor as any, |
||||
process: unitOverrideProcessor, |
||||
shouldApply: config.shouldApply ? config.shouldApply : () => true, |
||||
settings: config.settings || {}, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Fluent API for declarative creation of panel options |
||||
*/ |
||||
export class PanelOptionsEditorBuilder extends OptionsUIRegistryBuilder<StandardEditorProps, PanelOptionsEditorItem> { |
||||
addNumberInput<TSettings>(config: PanelOptionsEditorConfig<TSettings & NumberFieldConfigSettings>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
editor: standardEditorsRegistry.get('number').editor as any, |
||||
}); |
||||
} |
||||
|
||||
addTextInput<TSettings>(config: PanelOptionsEditorConfig<TSettings & StringFieldConfigSettings>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
editor: standardEditorsRegistry.get('text').editor as any, |
||||
}); |
||||
} |
||||
|
||||
addSelect<TOption, TSettings>(config: PanelOptionsEditorConfig<TSettings & SelectFieldConfigSettings<TOption>>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
editor: standardEditorsRegistry.get('select').editor as any, |
||||
}); |
||||
} |
||||
|
||||
addRadio<TOption, TSettings>(config: PanelOptionsEditorConfig<TSettings & SelectFieldConfigSettings<TOption>>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
editor: standardEditorsRegistry.get('radio').editor as any, |
||||
}); |
||||
} |
||||
|
||||
addBooleanSwitch<TSettings = any>(config: PanelOptionsEditorConfig<TSettings>) { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
editor: standardEditorsRegistry.get('boolean').editor as any, |
||||
}); |
||||
} |
||||
|
||||
addColorPicker<TSettings = any>(config: PanelOptionsEditorConfig<TSettings & ColorFieldConfigSettings>): this { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
editor: standardEditorsRegistry.get('color').editor as any, |
||||
settings: config.settings || {}, |
||||
}); |
||||
} |
||||
|
||||
addUnitPicker<TSettings = any>(config: PanelOptionsEditorConfig<TSettings & UnitFieldConfigSettings>): this { |
||||
return this.addCustomEditor({ |
||||
...config, |
||||
editor: standardEditorsRegistry.get('unit').editor as any, |
||||
}); |
||||
} |
||||
} |
@ -1,43 +0,0 @@ |
||||
import { FieldOverrideContext, FieldConfigEditorProps, DataLink, FieldOverrideEditorProps } from '@grafana/data'; |
||||
import React from 'react'; |
||||
import { DataLinksInlineEditor } from '../DataLinks/DataLinksInlineEditor/DataLinksInlineEditor'; |
||||
|
||||
export interface DataLinksFieldConfigSettings {} |
||||
|
||||
export const dataLinksOverrideProcessor = ( |
||||
value: any, |
||||
context: FieldOverrideContext, |
||||
_settings: DataLinksFieldConfigSettings |
||||
) => { |
||||
return value as DataLink[]; |
||||
}; |
||||
|
||||
export const DataLinksValueEditor: React.FC<FieldConfigEditorProps<DataLink[], DataLinksFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
context, |
||||
}) => { |
||||
return ( |
||||
<DataLinksInlineEditor |
||||
links={value} |
||||
onChange={onChange} |
||||
data={context.data} |
||||
suggestions={context.getSuggestions ? context.getSuggestions() : []} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export const DataLinksOverrideEditor: React.FC<FieldOverrideEditorProps<DataLink[], DataLinksFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
context, |
||||
}) => { |
||||
return ( |
||||
<DataLinksInlineEditor |
||||
links={value} |
||||
onChange={onChange} |
||||
data={context.data} |
||||
suggestions={context.getSuggestions ? context.getSuggestions() : []} |
||||
/> |
||||
); |
||||
}; |
@ -1,50 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
import { FieldOverrideContext, FieldOverrideEditorProps, FieldConfigEditorProps, ValueMapping } from '@grafana/data'; |
||||
import { ValueMappingsEditor } from '../ValueMappingsEditor/ValueMappingsEditor'; |
||||
|
||||
export interface ValueMappingFieldConfigSettings {} |
||||
|
||||
export const valueMappingsOverrideProcessor = ( |
||||
value: any, |
||||
context: FieldOverrideContext, |
||||
settings: ValueMappingFieldConfigSettings |
||||
) => { |
||||
return value as ValueMapping[]; // !!!! likely not !!!!
|
||||
}; |
||||
|
||||
export class ValueMappingsValueEditor extends React.PureComponent< |
||||
FieldConfigEditorProps<ValueMapping[], ValueMappingFieldConfigSettings> |
||||
> { |
||||
constructor(props: FieldConfigEditorProps<ValueMapping[], ValueMappingFieldConfigSettings>) { |
||||
super(props); |
||||
} |
||||
|
||||
render() { |
||||
const { onChange } = this.props; |
||||
let value = this.props.value; |
||||
if (!value) { |
||||
value = []; |
||||
} |
||||
|
||||
return <ValueMappingsEditor valueMappings={value} onChange={onChange} />; |
||||
} |
||||
} |
||||
|
||||
export class ValueMappingsOverrideEditor extends React.PureComponent< |
||||
FieldOverrideEditorProps<ValueMapping[], ValueMappingFieldConfigSettings> |
||||
> { |
||||
constructor(props: FieldOverrideEditorProps<ValueMapping[], ValueMappingFieldConfigSettings>) { |
||||
super(props); |
||||
} |
||||
|
||||
render() { |
||||
const { onChange } = this.props; |
||||
let value = this.props.value; |
||||
if (!value) { |
||||
value = []; |
||||
} |
||||
|
||||
return <ValueMappingsEditor valueMappings={value} onChange={onChange} />; |
||||
} |
||||
} |
@ -1,76 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
import { |
||||
FieldOverrideContext, |
||||
FieldOverrideEditorProps, |
||||
FieldConfigEditorProps, |
||||
toIntegerOrUndefined, |
||||
toFloatOrUndefined, |
||||
} from '@grafana/data'; |
||||
import Forms from '../Forms'; |
||||
|
||||
export interface NumberFieldConfigSettings { |
||||
placeholder?: string; |
||||
integer?: boolean; |
||||
min?: number; |
||||
max?: number; |
||||
step?: number; |
||||
} |
||||
|
||||
export const numberOverrideProcessor = ( |
||||
value: any, |
||||
context: FieldOverrideContext, |
||||
settings: NumberFieldConfigSettings |
||||
) => { |
||||
const v = parseFloat(`${value}`); |
||||
if (settings.max && v > settings.max) { |
||||
// ????
|
||||
} |
||||
return v; |
||||
}; |
||||
|
||||
export const NumberValueEditor: React.FC<FieldConfigEditorProps<number, NumberFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
item, |
||||
}) => { |
||||
const { settings } = item; |
||||
return ( |
||||
<Forms.Input |
||||
value={isNaN(value) ? '' : value} |
||||
min={settings.min} |
||||
max={settings.max} |
||||
type="number" |
||||
step={settings.step} |
||||
placeholder={settings.placeholder} |
||||
onChange={e => { |
||||
onChange( |
||||
settings.integer ? toIntegerOrUndefined(e.currentTarget.value) : toFloatOrUndefined(e.currentTarget.value) |
||||
); |
||||
}} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export const NumberOverrideEditor: React.FC<FieldOverrideEditorProps<number, NumberFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
item, |
||||
}) => { |
||||
const { settings } = item; |
||||
return ( |
||||
<Forms.Input |
||||
value={isNaN(value) ? '' : value} |
||||
min={settings.min} |
||||
max={settings.max} |
||||
type="number" |
||||
step={settings.step} |
||||
placeholder={settings.placeholder} |
||||
onChange={e => { |
||||
onChange( |
||||
settings.integer ? toIntegerOrUndefined(e.currentTarget.value) : toFloatOrUndefined(e.currentTarget.value) |
||||
); |
||||
}} |
||||
/> |
||||
); |
||||
}; |
@ -1,32 +0,0 @@ |
||||
import React, { FC } from 'react'; |
||||
|
||||
import { FieldOverrideContext, FieldOverrideEditorProps, FieldConfigEditorProps, SelectableValue } from '@grafana/data'; |
||||
import Forms from '../Forms'; |
||||
|
||||
export interface SelectFieldConfigSettings<T> { |
||||
options: Array<SelectableValue<T>>; |
||||
} |
||||
|
||||
export const selectOverrideProcessor = ( |
||||
value: any, |
||||
context: FieldOverrideContext, |
||||
settings: SelectFieldConfigSettings<any> |
||||
) => { |
||||
return value; |
||||
}; |
||||
|
||||
export const SelectValueEditor: FC<FieldConfigEditorProps<string, SelectFieldConfigSettings<any>>> = ({ |
||||
item, |
||||
value, |
||||
onChange, |
||||
}) => { |
||||
return <Forms.Select value={value || ''} onChange={e => onChange(e.value)} options={item.settings.options} />; |
||||
}; |
||||
|
||||
export const SelectOverrideEditor: FC<FieldOverrideEditorProps<string, SelectFieldConfigSettings<any>>> = ({ |
||||
item, |
||||
value, |
||||
onChange, |
||||
}) => { |
||||
return <Forms.Select value={value || ''} onChange={e => onChange(e.value)} options={item.settings.options} />; |
||||
}; |
@ -1,162 +0,0 @@ |
||||
import { DataLink, FieldPropertyEditorItem, FieldType, ThresholdsConfig, ValueMapping } from '@grafana/data'; |
||||
import { StringFieldConfigSettings, StringOverrideEditor, stringOverrideProcessor, StringValueEditor } from './string'; |
||||
import { NumberFieldConfigSettings, NumberOverrideEditor, numberOverrideProcessor, NumberValueEditor } from './number'; |
||||
import { UnitOverrideEditor, UnitValueEditor } from './units'; |
||||
import { |
||||
ThresholdsFieldConfigSettings, |
||||
ThresholdsOverrideEditor, |
||||
thresholdsOverrideProcessor, |
||||
ThresholdsValueEditor, |
||||
} from './thresholds'; |
||||
import { DataLinksOverrideEditor, dataLinksOverrideProcessor, DataLinksValueEditor } from './links'; |
||||
import { |
||||
ValueMappingFieldConfigSettings, |
||||
ValueMappingsOverrideEditor, |
||||
valueMappingsOverrideProcessor, |
||||
ValueMappingsValueEditor, |
||||
} from './mappings'; |
||||
|
||||
export const getStandardFieldConfigs = () => { |
||||
const title: FieldPropertyEditorItem<string, StringFieldConfigSettings> = { |
||||
id: 'title', // Match field properties
|
||||
name: 'Title', |
||||
description: 'The field title', |
||||
|
||||
editor: StringValueEditor, |
||||
override: StringOverrideEditor, |
||||
process: stringOverrideProcessor, |
||||
settings: { |
||||
placeholder: 'auto', |
||||
expandTemplateVars: true, |
||||
}, |
||||
shouldApply: field => field.type !== FieldType.time, |
||||
}; |
||||
|
||||
const unit: FieldPropertyEditorItem<string, StringFieldConfigSettings> = { |
||||
id: 'unit', // Match field properties
|
||||
name: 'Unit', |
||||
description: 'value units', |
||||
|
||||
editor: UnitValueEditor, |
||||
override: UnitOverrideEditor, |
||||
process: stringOverrideProcessor, |
||||
|
||||
settings: { |
||||
placeholder: 'none', |
||||
}, |
||||
|
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const min: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = { |
||||
id: 'min', // Match field properties
|
||||
name: 'Min', |
||||
description: 'Minimum expected value', |
||||
|
||||
editor: NumberValueEditor, |
||||
override: NumberOverrideEditor, |
||||
process: numberOverrideProcessor, |
||||
|
||||
settings: { |
||||
placeholder: 'auto', |
||||
}, |
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const max: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = { |
||||
id: 'max', // Match field properties
|
||||
name: 'Max', |
||||
description: 'Maximum expected value', |
||||
|
||||
editor: NumberValueEditor, |
||||
override: NumberOverrideEditor, |
||||
process: numberOverrideProcessor, |
||||
|
||||
settings: { |
||||
placeholder: 'auto', |
||||
}, |
||||
|
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const decimals: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = { |
||||
id: 'decimals', // Match field properties
|
||||
name: 'Decimals', |
||||
description: 'How many decimal places should be shown on a number', |
||||
|
||||
editor: NumberValueEditor, |
||||
override: NumberOverrideEditor, |
||||
process: numberOverrideProcessor, |
||||
|
||||
settings: { |
||||
placeholder: 'auto', |
||||
min: 0, |
||||
max: 15, |
||||
integer: true, |
||||
}, |
||||
|
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const thresholds: FieldPropertyEditorItem<ThresholdsConfig, ThresholdsFieldConfigSettings> = { |
||||
id: 'thresholds', // Match field properties
|
||||
name: 'Thresholds', |
||||
description: 'Manage Thresholds', |
||||
|
||||
editor: ThresholdsValueEditor, |
||||
override: ThresholdsOverrideEditor, |
||||
process: thresholdsOverrideProcessor, |
||||
|
||||
settings: { |
||||
// ??
|
||||
}, |
||||
|
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const mappings: FieldPropertyEditorItem<ValueMapping[], ValueMappingFieldConfigSettings> = { |
||||
id: 'mappings', // Match field properties
|
||||
name: 'Value mappings', |
||||
description: 'Manage value mappings', |
||||
|
||||
editor: ValueMappingsValueEditor, |
||||
override: ValueMappingsOverrideEditor, |
||||
process: valueMappingsOverrideProcessor, |
||||
settings: { |
||||
// ??
|
||||
}, |
||||
|
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const noValue: FieldPropertyEditorItem<string, StringFieldConfigSettings> = { |
||||
id: 'noValue', // Match field properties
|
||||
name: 'No Value', |
||||
description: 'What to show when there is no value', |
||||
|
||||
editor: StringValueEditor, |
||||
override: StringOverrideEditor, |
||||
process: stringOverrideProcessor, |
||||
|
||||
settings: { |
||||
placeholder: '-', |
||||
}, |
||||
// ??? any field with no value
|
||||
shouldApply: () => true, |
||||
}; |
||||
|
||||
const links: FieldPropertyEditorItem<DataLink[], StringFieldConfigSettings> = { |
||||
id: 'links', // Match field properties
|
||||
name: 'DataLinks', |
||||
description: 'Manage date links', |
||||
editor: DataLinksValueEditor, |
||||
override: DataLinksOverrideEditor, |
||||
process: dataLinksOverrideProcessor, |
||||
settings: { |
||||
placeholder: '-', |
||||
}, |
||||
shouldApply: () => true, |
||||
}; |
||||
|
||||
return [unit, min, max, decimals, title, noValue, thresholds, mappings, links]; |
||||
}; |
@ -1,35 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
import { FieldOverrideContext, FieldOverrideEditorProps, FieldConfigEditorProps } from '@grafana/data'; |
||||
import Forms from '../Forms'; |
||||
|
||||
export interface StringFieldConfigSettings { |
||||
placeholder?: string; |
||||
maxLength?: number; |
||||
expandTemplateVars?: boolean; |
||||
} |
||||
|
||||
export const stringOverrideProcessor = ( |
||||
value: any, |
||||
context: FieldOverrideContext, |
||||
settings: StringFieldConfigSettings |
||||
) => { |
||||
if (settings.expandTemplateVars && context.replaceVariables) { |
||||
return context.replaceVariables(value, context.field!.config.scopedVars); |
||||
} |
||||
return `${value}`; |
||||
}; |
||||
|
||||
export const StringValueEditor: React.FC<FieldConfigEditorProps<string, StringFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
}) => { |
||||
return <Forms.Input value={value || ''} onChange={e => onChange(e.currentTarget.value)} />; |
||||
}; |
||||
|
||||
export const StringOverrideEditor: React.FC<FieldOverrideEditorProps<string, StringFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
}) => { |
||||
return <Forms.Input value={value || ''} onChange={e => onChange(e.currentTarget.value)} />; |
||||
}; |
@ -1,59 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
import { |
||||
FieldOverrideContext, |
||||
FieldOverrideEditorProps, |
||||
FieldConfigEditorProps, |
||||
ThresholdsConfig, |
||||
ThresholdsMode, |
||||
} from '@grafana/data'; |
||||
import { ThresholdsEditor } from '../ThresholdsEditorNew/ThresholdsEditor'; |
||||
|
||||
export interface ThresholdsFieldConfigSettings { |
||||
// Anything?
|
||||
} |
||||
|
||||
export const thresholdsOverrideProcessor = ( |
||||
value: any, |
||||
context: FieldOverrideContext, |
||||
settings: ThresholdsFieldConfigSettings |
||||
) => { |
||||
return value as ThresholdsConfig; // !!!! likely not !!!!
|
||||
}; |
||||
|
||||
export class ThresholdsValueEditor extends React.PureComponent< |
||||
FieldConfigEditorProps<ThresholdsConfig, ThresholdsFieldConfigSettings> |
||||
> { |
||||
constructor(props: FieldConfigEditorProps<ThresholdsConfig, ThresholdsFieldConfigSettings>) { |
||||
super(props); |
||||
} |
||||
|
||||
render() { |
||||
const { onChange } = this.props; |
||||
let value = this.props.value; |
||||
if (!value) { |
||||
value = { |
||||
mode: ThresholdsMode.Percentage, |
||||
|
||||
// Must be sorted by 'value', first value is always -Infinity
|
||||
steps: [ |
||||
// anything?
|
||||
], |
||||
}; |
||||
} |
||||
|
||||
return <ThresholdsEditor thresholds={value} onChange={onChange} />; |
||||
} |
||||
} |
||||
|
||||
export class ThresholdsOverrideEditor extends React.PureComponent< |
||||
FieldOverrideEditorProps<ThresholdsConfig, ThresholdsFieldConfigSettings> |
||||
> { |
||||
constructor(props: FieldOverrideEditorProps<ThresholdsConfig, ThresholdsFieldConfigSettings>) { |
||||
super(props); |
||||
} |
||||
|
||||
render() { |
||||
return <div>THRESHOLDS OVERRIDE EDITOR {this.props.item.name}</div>; |
||||
} |
||||
} |
@ -1,22 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
import { FieldOverrideEditorProps, FieldConfigEditorProps } from '@grafana/data'; |
||||
import { UnitPicker } from '../UnitPicker/UnitPicker'; |
||||
|
||||
export interface UnitFieldConfigSettings { |
||||
// ??
|
||||
} |
||||
|
||||
export const UnitValueEditor: React.FC<FieldConfigEditorProps<string, UnitFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
}) => { |
||||
return <UnitPicker value={value} onChange={onChange} useNewForms />; |
||||
}; |
||||
|
||||
export const UnitOverrideEditor: React.FC<FieldOverrideEditorProps<string, UnitFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
}) => { |
||||
return <UnitPicker value={value} onChange={onChange} useNewForms />; |
||||
}; |
@ -0,0 +1,11 @@ |
||||
import React from 'react'; |
||||
import { FieldConfigEditorProps, ColorFieldConfigSettings } from '@grafana/data'; |
||||
import { ColorPicker } from '../ColorPicker/ColorPicker'; |
||||
|
||||
export const ColorValueEditor: React.FC<FieldConfigEditorProps<string, ColorFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
item, |
||||
}) => { |
||||
return <ColorPicker color={value} onChange={onChange} enableNamedColors={!!item.settings.enableNamedColors} />; |
||||
}; |
@ -0,0 +1,18 @@ |
||||
import React from 'react'; |
||||
import { FieldConfigEditorProps, DataLink, DataLinksFieldConfigSettings } from '@grafana/data'; |
||||
import { DataLinksInlineEditor } from '../DataLinks/DataLinksInlineEditor/DataLinksInlineEditor'; |
||||
|
||||
export const DataLinksValueEditor: React.FC<FieldConfigEditorProps<DataLink[], DataLinksFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
context, |
||||
}) => { |
||||
return ( |
||||
<DataLinksInlineEditor |
||||
links={value} |
||||
onChange={onChange} |
||||
data={context.data} |
||||
suggestions={context.getSuggestions ? context.getSuggestions() : []} |
||||
/> |
||||
); |
||||
}; |
@ -0,0 +1,22 @@ |
||||
import React from 'react'; |
||||
|
||||
import { FieldConfigEditorProps, ValueMapping, ValueMappingFieldConfigSettings } from '@grafana/data'; |
||||
import { ValueMappingsEditor } from '../ValueMappingsEditor/ValueMappingsEditor'; |
||||
|
||||
export class ValueMappingsValueEditor extends React.PureComponent< |
||||
FieldConfigEditorProps<ValueMapping[], ValueMappingFieldConfigSettings> |
||||
> { |
||||
constructor(props: FieldConfigEditorProps<ValueMapping[], ValueMappingFieldConfigSettings>) { |
||||
super(props); |
||||
} |
||||
|
||||
render() { |
||||
const { onChange } = this.props; |
||||
let value = this.props.value; |
||||
if (!value) { |
||||
value = []; |
||||
} |
||||
|
||||
return <ValueMappingsEditor valueMappings={value} onChange={onChange} />; |
||||
} |
||||
} |
@ -0,0 +1,31 @@ |
||||
import React from 'react'; |
||||
import { |
||||
FieldConfigEditorProps, |
||||
toIntegerOrUndefined, |
||||
toFloatOrUndefined, |
||||
NumberFieldConfigSettings, |
||||
} from '@grafana/data'; |
||||
import Forms from '../Forms'; |
||||
|
||||
export const NumberValueEditor: React.FC<FieldConfigEditorProps<number, NumberFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
item, |
||||
}) => { |
||||
const { settings } = item; |
||||
return ( |
||||
<Forms.Input |
||||
value={isNaN(value) ? '' : value} |
||||
min={settings.min} |
||||
max={settings.max} |
||||
type="number" |
||||
step={settings.step} |
||||
placeholder={settings.placeholder} |
||||
onChange={e => { |
||||
onChange( |
||||
settings.integer ? toIntegerOrUndefined(e.currentTarget.value) : toFloatOrUndefined(e.currentTarget.value) |
||||
); |
||||
}} |
||||
/> |
||||
); |
||||
}; |
@ -0,0 +1,11 @@ |
||||
import React from 'react'; |
||||
import { FieldConfigEditorProps, SelectFieldConfigSettings } from '@grafana/data'; |
||||
import Forms from '../Forms'; |
||||
|
||||
export function SelectValueEditor<T>({ |
||||
value, |
||||
onChange, |
||||
item, |
||||
}: FieldConfigEditorProps<T, SelectFieldConfigSettings<T>>) { |
||||
return <Forms.Select<T> defaultValue={value} onChange={e => onChange(e.value)} options={item.settings.options} />; |
||||
} |
@ -0,0 +1,10 @@ |
||||
import React from 'react'; |
||||
import { FieldConfigEditorProps, StringFieldConfigSettings } from '@grafana/data'; |
||||
import Forms from '../Forms'; |
||||
|
||||
export const StringValueEditor: React.FC<FieldConfigEditorProps<string, StringFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
}) => { |
||||
return <Forms.Input value={value || ''} onChange={e => onChange(e.currentTarget.value)} />; |
||||
}; |
@ -0,0 +1,28 @@ |
||||
import React from 'react'; |
||||
import { FieldConfigEditorProps, ThresholdsConfig, ThresholdsMode, ThresholdsFieldConfigSettings } from '@grafana/data'; |
||||
import { ThresholdsEditor } from '../ThresholdsEditorNew/ThresholdsEditor'; |
||||
|
||||
export class ThresholdsValueEditor extends React.PureComponent< |
||||
FieldConfigEditorProps<ThresholdsConfig, ThresholdsFieldConfigSettings> |
||||
> { |
||||
constructor(props: FieldConfigEditorProps<ThresholdsConfig, ThresholdsFieldConfigSettings>) { |
||||
super(props); |
||||
} |
||||
|
||||
render() { |
||||
const { onChange } = this.props; |
||||
let value = this.props.value; |
||||
if (!value) { |
||||
value = { |
||||
mode: ThresholdsMode.Percentage, |
||||
|
||||
// Must be sorted by 'value', first value is always -Infinity
|
||||
steps: [ |
||||
// anything?
|
||||
], |
||||
}; |
||||
} |
||||
|
||||
return <ThresholdsEditor thresholds={value} onChange={onChange} />; |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
import React from 'react'; |
||||
|
||||
import { FieldConfigEditorProps, UnitFieldConfigSettings } from '@grafana/data'; |
||||
import { UnitPicker } from '../UnitPicker/UnitPicker'; |
||||
|
||||
export const UnitValueEditor: React.FC<FieldConfigEditorProps<string, UnitFieldConfigSettings>> = ({ |
||||
value, |
||||
onChange, |
||||
}) => { |
||||
return <UnitPicker value={value} onChange={onChange} useNewForms />; |
||||
}; |
@ -0,0 +1,269 @@ |
||||
import React from 'react'; |
||||
import { |
||||
DataLink, |
||||
dataLinksOverrideProcessor, |
||||
FieldPropertyEditorItem, |
||||
FieldType, |
||||
identityOverrideProcessor, |
||||
NumberFieldConfigSettings, |
||||
numberOverrideProcessor, |
||||
standardEditorsRegistry, |
||||
StandardEditorsRegistryItem, |
||||
StringFieldConfigSettings, |
||||
stringOverrideProcessor, |
||||
ThresholdsConfig, |
||||
ThresholdsFieldConfigSettings, |
||||
thresholdsOverrideProcessor, |
||||
ValueMapping, |
||||
ValueMappingFieldConfigSettings, |
||||
valueMappingsOverrideProcessor, |
||||
} from '@grafana/data'; |
||||
import { NumberValueEditor, Forms, StringValueEditor } from '../components'; |
||||
import { ValueMappingsValueEditor } from '../components/OptionsUI/mappings'; |
||||
import { ThresholdsValueEditor } from '../components/OptionsUI/thresholds'; |
||||
import { UnitValueEditor } from '../components/OptionsUI/units'; |
||||
import { DataLinksValueEditor } from '../components/OptionsUI/links'; |
||||
import { ColorValueEditor } from '../components/OptionsUI/color'; |
||||
|
||||
/** |
||||
* Returns collection of common field config properties definitions |
||||
*/ |
||||
export const getStandardFieldConfigs = () => { |
||||
const title: FieldPropertyEditorItem<string, StringFieldConfigSettings> = { |
||||
id: 'title', |
||||
name: 'Title', |
||||
description: "Field's title", |
||||
editor: standardEditorsRegistry.get('text').editor as any, |
||||
override: standardEditorsRegistry.get('text').editor as any, |
||||
process: stringOverrideProcessor, |
||||
settings: { |
||||
placeholder: 'auto', |
||||
expandTemplateVars: true, |
||||
}, |
||||
shouldApply: field => field.type !== FieldType.time, |
||||
}; |
||||
|
||||
const unit: FieldPropertyEditorItem<string, StringFieldConfigSettings> = { |
||||
id: 'unit', |
||||
name: 'Unit', |
||||
description: 'Value units', |
||||
|
||||
editor: standardEditorsRegistry.get('unit').editor as any, |
||||
override: standardEditorsRegistry.get('unit').editor as any, |
||||
process: stringOverrideProcessor, |
||||
|
||||
settings: { |
||||
placeholder: 'none', |
||||
}, |
||||
|
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const min: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = { |
||||
id: 'min', |
||||
name: 'Min', |
||||
description: 'Minimum expected value', |
||||
|
||||
editor: standardEditorsRegistry.get('number').editor as any, |
||||
override: standardEditorsRegistry.get('number').editor as any, |
||||
process: numberOverrideProcessor, |
||||
|
||||
settings: { |
||||
placeholder: 'auto', |
||||
}, |
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const max: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = { |
||||
id: 'max', |
||||
name: 'Max', |
||||
description: 'Maximum expected value', |
||||
|
||||
editor: standardEditorsRegistry.get('number').editor as any, |
||||
override: standardEditorsRegistry.get('number').editor as any, |
||||
process: numberOverrideProcessor, |
||||
|
||||
settings: { |
||||
placeholder: 'auto', |
||||
}, |
||||
|
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const decimals: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = { |
||||
id: 'decimals', |
||||
name: 'Decimals', |
||||
description: 'Number of decimal to be shown for a value', |
||||
|
||||
editor: standardEditorsRegistry.get('number').editor as any, |
||||
override: standardEditorsRegistry.get('number').editor as any, |
||||
process: numberOverrideProcessor, |
||||
|
||||
settings: { |
||||
placeholder: 'auto', |
||||
min: 0, |
||||
max: 15, |
||||
integer: true, |
||||
}, |
||||
|
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const thresholds: FieldPropertyEditorItem<ThresholdsConfig, ThresholdsFieldConfigSettings> = { |
||||
id: 'thresholds', |
||||
name: 'Thresholds', |
||||
description: 'Manage thresholds', |
||||
|
||||
editor: standardEditorsRegistry.get('thresholds').editor as any, |
||||
override: standardEditorsRegistry.get('thresholds').editor as any, |
||||
process: thresholdsOverrideProcessor, |
||||
|
||||
settings: { |
||||
// ??
|
||||
}, |
||||
|
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const mappings: FieldPropertyEditorItem<ValueMapping[], ValueMappingFieldConfigSettings> = { |
||||
id: 'mappings', |
||||
name: 'Value mappings', |
||||
description: 'Manage value mappings', |
||||
|
||||
editor: standardEditorsRegistry.get('mappings').editor as any, |
||||
override: standardEditorsRegistry.get('mappings').editor as any, |
||||
process: valueMappingsOverrideProcessor, |
||||
settings: { |
||||
// ??
|
||||
}, |
||||
|
||||
shouldApply: field => field.type === FieldType.number, |
||||
}; |
||||
|
||||
const noValue: FieldPropertyEditorItem<string, StringFieldConfigSettings> = { |
||||
id: 'noValue', |
||||
name: 'No Value', |
||||
description: 'What to show when there is no value', |
||||
|
||||
editor: standardEditorsRegistry.get('text').editor as any, |
||||
override: standardEditorsRegistry.get('text').editor as any, |
||||
process: stringOverrideProcessor, |
||||
|
||||
settings: { |
||||
placeholder: '-', |
||||
}, |
||||
// ??? any optionsUi with no value
|
||||
shouldApply: () => true, |
||||
}; |
||||
|
||||
const links: FieldPropertyEditorItem<DataLink[], StringFieldConfigSettings> = { |
||||
id: 'links', |
||||
name: 'DataLinks', |
||||
description: 'Manage date links', |
||||
editor: standardEditorsRegistry.get('links').editor as any, |
||||
override: standardEditorsRegistry.get('links').editor as any, |
||||
process: dataLinksOverrideProcessor, |
||||
settings: { |
||||
placeholder: '-', |
||||
}, |
||||
shouldApply: () => true, |
||||
}; |
||||
|
||||
const color: FieldPropertyEditorItem<string, StringFieldConfigSettings> = { |
||||
id: 'color', |
||||
name: 'Color', |
||||
description: 'Customise color', |
||||
editor: standardEditorsRegistry.get('color').editor as any, |
||||
override: standardEditorsRegistry.get('color').editor as any, |
||||
process: identityOverrideProcessor, |
||||
settings: { |
||||
placeholder: '-', |
||||
}, |
||||
shouldApply: () => true, |
||||
}; |
||||
|
||||
return [unit, min, max, decimals, title, noValue, thresholds, mappings, links, color]; |
||||
}; |
||||
|
||||
/** |
||||
* Returns collection of standard option editors definitions |
||||
*/ |
||||
export const getStandardOptionEditors = () => { |
||||
const number: StandardEditorsRegistryItem<number> = { |
||||
id: 'number', |
||||
name: 'Number', |
||||
description: 'Allows numeric values input', |
||||
editor: NumberValueEditor as any, |
||||
}; |
||||
|
||||
const text: StandardEditorsRegistryItem<string> = { |
||||
id: 'text', |
||||
name: 'Text', |
||||
description: 'Allows string values input', |
||||
editor: StringValueEditor as any, |
||||
}; |
||||
|
||||
const boolean: StandardEditorsRegistryItem<boolean> = { |
||||
id: 'boolean', |
||||
name: 'Boolean', |
||||
description: 'Allows boolean values input', |
||||
editor: props => <Forms.Switch {...props} onChange={e => props.onChange(e.currentTarget.checked)} />, |
||||
}; |
||||
|
||||
const select: StandardEditorsRegistryItem<any> = { |
||||
id: 'select', |
||||
name: 'Select', |
||||
description: 'Allows option selection', |
||||
editor: props => ( |
||||
<Forms.Select |
||||
defaultValue={props.value} |
||||
onChange={e => props.onChange(e.value)} |
||||
options={props.item.settings?.options} |
||||
/> |
||||
), |
||||
}; |
||||
|
||||
const radio: StandardEditorsRegistryItem<any> = { |
||||
id: 'radio', |
||||
name: 'Radio', |
||||
description: 'Allows option selection', |
||||
editor: props => <Forms.RadioButtonGroup {...props} options={props.item.settings?.options} />, |
||||
}; |
||||
|
||||
const unit: StandardEditorsRegistryItem<string> = { |
||||
id: 'unit', |
||||
name: 'Unit', |
||||
description: 'Allows unit input', |
||||
editor: UnitValueEditor as any, |
||||
}; |
||||
|
||||
const thresholds: StandardEditorsRegistryItem<ThresholdsConfig> = { |
||||
id: 'thresholds', |
||||
name: 'Thresholds', |
||||
description: 'Allows defining thresholds', |
||||
editor: ThresholdsValueEditor as any, |
||||
}; |
||||
|
||||
const mappings: StandardEditorsRegistryItem<ValueMapping[]> = { |
||||
id: 'mappings', |
||||
name: 'Mappings', |
||||
description: 'Allows defining value mappings', |
||||
editor: ValueMappingsValueEditor as any, |
||||
}; |
||||
|
||||
const color: StandardEditorsRegistryItem<string> = { |
||||
id: 'color', |
||||
name: 'Color', |
||||
description: 'Allows color selection', |
||||
editor: ColorValueEditor as any, |
||||
}; |
||||
|
||||
const links: StandardEditorsRegistryItem<DataLink[]> = { |
||||
id: 'links', |
||||
name: 'Links', |
||||
description: 'Allows defining data links', |
||||
editor: DataLinksValueEditor as any, |
||||
}; |
||||
|
||||
return [text, number, boolean, radio, select, unit, mappings, thresholds, links, color]; |
||||
}; |
@ -0,0 +1,32 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { PanelPlugin } from '@grafana/data'; |
||||
import { Forms } from '@grafana/ui'; |
||||
|
||||
interface PanelOptionsEditorProps<TOptions> { |
||||
plugin: PanelPlugin; |
||||
options: TOptions; |
||||
onChange: (options: TOptions) => void; |
||||
} |
||||
|
||||
export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({ plugin, options, onChange }) => { |
||||
const optionEditors = useMemo(() => plugin.optionEditors, [plugin]); |
||||
|
||||
const onOptionChange = (key: string, value: any) => { |
||||
onChange({ |
||||
...options, |
||||
[key]: value, |
||||
}); |
||||
}; |
||||
|
||||
return ( |
||||
<> |
||||
{optionEditors.list().map(e => { |
||||
return ( |
||||
<Forms.Field label={e.name} description={e.description}> |
||||
<e.editor value={options[e.id]} onChange={value => onOptionChange(e.id, value)} item={e} /> |
||||
</Forms.Field> |
||||
); |
||||
})} |
||||
</> |
||||
); |
||||
}; |
@ -1,54 +0,0 @@ |
||||
import { FieldPropertyEditorItem, Registry, FieldConfigEditorRegistry } from '@grafana/data'; |
||||
import { |
||||
NumberValueEditor, |
||||
NumberOverrideEditor, |
||||
numberOverrideProcessor, |
||||
NumberFieldConfigSettings, |
||||
selectOverrideProcessor, |
||||
SelectValueEditor, |
||||
SelectOverrideEditor, |
||||
SelectFieldConfigSettings, |
||||
} from '@grafana/ui'; |
||||
|
||||
export const tableFieldRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(() => { |
||||
const columWidth: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = { |
||||
id: 'width', // Match field properties
|
||||
name: 'Column width', |
||||
description: 'column width (for table)', |
||||
|
||||
editor: NumberValueEditor, |
||||
override: NumberOverrideEditor, |
||||
process: numberOverrideProcessor, |
||||
|
||||
settings: { |
||||
placeholder: 'auto', |
||||
min: 20, |
||||
max: 300, |
||||
}, |
||||
|
||||
shouldApply: () => true, |
||||
}; |
||||
|
||||
const cellDisplayMode: FieldPropertyEditorItem<string, SelectFieldConfigSettings<string>> = { |
||||
id: 'displayMode', // Match field properties
|
||||
name: 'Cell display mode', |
||||
description: 'Color value, background, show as gauge, etc', |
||||
|
||||
editor: SelectValueEditor, |
||||
override: SelectOverrideEditor, |
||||
process: selectOverrideProcessor, |
||||
|
||||
settings: { |
||||
options: [ |
||||
{ value: 'auto', label: 'Auto' }, |
||||
{ value: 'color-background', label: 'Color background' }, |
||||
{ value: 'gradient-gauge', label: 'Gradient gauge' }, |
||||
{ value: 'lcd-gauge', label: 'LCD gauge' }, |
||||
], |
||||
}, |
||||
|
||||
shouldApply: () => true, |
||||
}; |
||||
|
||||
return [columWidth, cellDisplayMode]; |
||||
}); |
@ -1,11 +1,40 @@ |
||||
import { PanelPlugin } from '@grafana/data'; |
||||
|
||||
import { TablePanelEditor } from './TablePanelEditor'; |
||||
import { TablePanel } from './TablePanel'; |
||||
import { tableFieldRegistry } from './custom'; |
||||
import { Options, defaults } from './types'; |
||||
|
||||
export const plugin = new PanelPlugin<Options>(TablePanel) |
||||
.setDefaults(defaults) |
||||
.setCustomFieldConfigs(tableFieldRegistry) |
||||
.setEditor(TablePanelEditor); |
||||
.setCustomFieldConfigEditor(builder => { |
||||
builder |
||||
.addNumberInput({ |
||||
id: 'width', |
||||
name: 'Column width', |
||||
description: 'column width (for table)', |
||||
settings: { |
||||
placeholder: 'auto', |
||||
min: 20, |
||||
max: 300, |
||||
}, |
||||
}) |
||||
.addSelect({ |
||||
id: 'displayMode', |
||||
name: 'Cell display mode', |
||||
description: 'Color value, background, show as gauge, etc', |
||||
|
||||
settings: { |
||||
options: [ |
||||
{ value: 'auto', label: 'Auto' }, |
||||
{ value: 'color-background', label: 'Color background' }, |
||||
{ value: 'gradient-gauge', label: 'Gradient gauge' }, |
||||
{ value: 'lcd-gauge', label: 'LCD gauge' }, |
||||
], |
||||
}, |
||||
}); |
||||
}) |
||||
.setOptionsEditor(builder => { |
||||
builder.addBooleanSwitch({ |
||||
id: 'showHeader', |
||||
name: 'Show header', |
||||
description: "To display table's header or not to display", |
||||
}); |
||||
}); |
||||
|
Loading…
Reference in new issue