diff --git a/packages/grafana-data/src/field/index.ts b/packages/grafana-data/src/field/index.ts index 87dfa77c138..2ba7edd4c50 100644 --- a/packages/grafana-data/src/field/index.ts +++ b/packages/grafana-data/src/field/index.ts @@ -2,5 +2,6 @@ export * from './fieldDisplay'; export * from './displayProcessor'; export * from './scale'; export * from './standardFieldConfigEditorRegistry'; +export * from './overrides/processors'; export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides'; diff --git a/packages/grafana-data/src/field/overrides/processors.ts b/packages/grafana-data/src/field/overrides/processors.ts new file mode 100644 index 00000000000..9d318b6a3ac --- /dev/null +++ b/packages/grafana-data/src/field/overrides/processors.ts @@ -0,0 +1,108 @@ +import { DataLink, FieldOverrideContext, SelectableValue, ThresholdsConfig, ValueMapping } from '../../types'; + +export const identityOverrideProcessor = (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 { + options: Array>; +} + +export const selectOverrideProcessor = ( + value: any, + _context: FieldOverrideContext, + _settings: SelectFieldConfigSettings +) => { + 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; +} diff --git a/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts b/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts index d1830320055..41f108d3496 100644 --- a/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts +++ b/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts @@ -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 { + value: TValue; + onChange: (value?: TValue) => void; + item: StandardEditorsRegistryItem; +} +export interface StandardEditorsRegistryItem extends RegistryItem { + editor: ComponentType>; + settings?: TSettings; +} export const standardFieldConfigEditorRegistry: FieldConfigEditorRegistry = new Registry(); + +export const standardEditorsRegistry = new Registry>(); diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 9d6a1a6c4ed..43a488d7095 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -12,3 +12,4 @@ export * from './datetime'; export * from './text'; export * from './valueFormats'; export * from './field'; +export { PanelPlugin } from './panel/PanelPlugin'; diff --git a/packages/grafana-data/src/panel/PanelPlugin.test.tsx b/packages/grafana-data/src/panel/PanelPlugin.test.tsx new file mode 100644 index 00000000000..70de0912f29 --- /dev/null +++ b/packages/grafana-data/src/panel/PanelPlugin.test.tsx @@ -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
Panel
; + }); + + panel.setCustomFieldConfigEditor(builder => { + builder.addCustomEditor({ + id: 'custom', + name: 'Custom', + description: 'Custom field config property description', + editor: () =>
Editor
, + override: () =>
Editor
, + process: identityOverrideProcessor, + settings: {}, + shouldApply: () => true, + }); + }); + + expect(panel.customFieldConfigs).toBeDefined(); + expect(panel.customFieldConfigs!.list()).toHaveLength(1); + }); + + test('options UI API', () => { + const panel = new PanelPlugin(() => { + return
Panel
; + }); + + panel.setOptionsEditor(builder => { + builder.addCustomEditor({ + id: 'option', + name: 'Option editor', + description: 'Option editor description', + editor: () =>
Editor
, + settings: {}, + }); + }); + + expect(panel.optionEditors).toBeDefined(); + expect(panel.optionEditors!.list()).toHaveLength(1); + }); + }); +}); diff --git a/packages/grafana-data/src/panel/PanelPlugin.ts b/packages/grafana-data/src/panel/PanelPlugin.ts new file mode 100644 index 00000000000..1c0271bad7d --- /dev/null +++ b/packages/grafana-data/src/panel/PanelPlugin.ts @@ -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 extends GrafanaPlugin { + 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>; + editor?: ComponentClass>; + defaults?: TOptions; + fieldConfigDefaults?: FieldConfigSource = { + defaults: {}, + overrides: [], + }; + onPanelMigration?: PanelMigrationHandler; + onPanelTypeChanged?: PanelTypeChangedHandler; + noPadding?: boolean; + + /** + * Legacy angular ctrl. If this exists it will be used instead of the panel + */ + angularPanelCtrl?: any; + + constructor(panel: ComponentType>) { + 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>) { + 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(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(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) { + this.fieldConfigDefaults = { + defaults: {}, + overrides: [], + ...defaultConfig, + }; + + return this; + } +} diff --git a/packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts b/packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts new file mode 100644 index 00000000000..9782582a6fb --- /dev/null +++ b/packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts @@ -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 extends RegistryItem { + settings?: TSettings; + editor?: ComponentType; +} + +/** + * Configuration of option editor registry item + */ +type OptionEditorConfig = Pick< + OptionsEditorItem, + 'id' | 'name' | 'description' | 'editor' | 'settings' +>; + +/** + * Describes an API for option editors UI builder + */ +export interface OptionsUIRegistryBuilderAPI> { + addNumberInput?( + config: OptionEditorConfig + ): this; + + addTextInput?( + config: OptionEditorConfig + ): this; + + addSelect?>( + config: OptionEditorConfig + ): this; + + addRadio? = SelectFieldConfigSettings>( + config: OptionEditorConfig + ): this; + + addBooleanSwitch?(config: OptionEditorConfig): this; + + addUnitPicker?(config: OptionEditorConfig): this; + + addColorPicker?(config: OptionEditorConfig): this; + + /** + * Enables custom editor definition + * @param config + */ + addCustomEditor(config: OptionsEditorItem): this; + + /** + * Returns registry of option editors + */ + getRegistry: () => Registry; +} + +export abstract class OptionsUIRegistryBuilder> + implements OptionsUIRegistryBuilderAPI { + private properties: T[] = []; + + addCustomEditor(config: T & OptionsEditorItem): this { + this.properties.push(config); + return this; + } + + getRegistry() { + return new Registry(() => { + return this.properties; + }); + } +} diff --git a/packages/grafana-data/src/types/fieldOverrides.ts b/packages/grafana-data/src/types/fieldOverrides.ts index 1dbb41ace44..48de8ce347d 100644 --- a/packages/grafana-data/src/types/fieldOverrides.ts +++ b/packages/grafana-data/src/types/fieldOverrides.ts @@ -11,6 +11,7 @@ import { } from '../types'; import { Registry, RegistryItem } from '../utils'; import { InterpolateFunction } from './panel'; +import { StandardEditorProps } from '../field'; export interface DynamicConfigValue { prop: string; @@ -31,13 +32,6 @@ export interface FieldConfigSource { overrides: ConfigOverrideRule[]; } -export interface FieldConfigEditorProps { - item: FieldPropertyEditorItem; // The property info - value: TValue; - context: FieldOverrideContext; - onChange: (value?: TValue) => void; -} - export interface FieldOverrideContext { field?: Field; dataFrameIndex?: number; // The index for the selected field frame @@ -46,11 +40,23 @@ export interface FieldOverrideContext { getSuggestions?: (scope?: VariableSuggestionsScope) => VariableSuggestion[]; } -export interface FieldOverrideEditorProps { - item: FieldPropertyEditorItem; +export interface FieldConfigEditorProps + extends Omit, 'item'> { + item: FieldPropertyEditorItem; // The property info value: TValue; context: FieldOverrideContext; - onChange: (value?: any) => void; + onChange: (value?: TValue) => void; +} + +export interface FieldOverrideEditorProps extends Omit, 'item'> { + item: FieldPropertyEditorItem; + context: FieldOverrideContext; +} + +export interface FieldConfigEditorConfig + extends Omit, 'id' | 'description' | 'name'>, 'settings'> { + settings?: TSettings; + shouldApply?: (field: Field) => boolean; } export interface FieldPropertyEditorItem extends RegistryItem { diff --git a/packages/grafana-data/src/types/panel.ts b/packages/grafana-data/src/types/panel.ts index 5be1f8c6f24..0e63bc9232b 100644 --- a/packages/grafana-data/src/types/panel.ts +++ b/packages/grafana-data/src/types/panel.ts @@ -1,11 +1,13 @@ -import { ComponentClass, ComponentType } from 'react'; +import { ComponentType } from 'react'; import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource'; -import { GrafanaPlugin, PluginMeta } from './plugin'; +import { PluginMeta } from './plugin'; import { ScopedVars } from './ScopedVars'; import { LoadingState } from './data'; import { DataFrame } from './dataFrame'; import { AbsoluteTimeRange, TimeRange, TimeZone } from './time'; -import { FieldConfigEditorRegistry, FieldConfigSource } from './fieldOverrides'; +import { FieldConfigSource } from './fieldOverrides'; +import { Registry, RegistryItem } from '../utils'; +import { StandardEditorProps } from '../field'; export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string; @@ -109,87 +111,18 @@ export type PanelTypeChangedHandler = ( prevOptions: any ) => Partial; -export class PanelPlugin extends GrafanaPlugin { - panel: ComponentType>; - editor?: ComponentClass>; - customFieldConfigs?: FieldConfigEditorRegistry; - defaults?: TOptions; - fieldConfigDefaults?: FieldConfigSource = { - defaults: {}, - overrides: [], - }; - onPanelMigration?: PanelMigrationHandler; - onPanelTypeChanged?: PanelTypeChangedHandler; - noPadding?: boolean; +export type PanelOptionEditorsRegistry = Registry; - /** - * Legacy angular ctrl. If this exists it will be used instead of the panel - */ - angularPanelCtrl?: any; - - constructor(panel: ComponentType>) { - super(); - this.panel = panel; - } - - setEditor(editor: ComponentClass>) { - 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; - } +export interface PanelOptionsEditorProps extends StandardEditorProps {} - setCustomFieldConfigs(registry: FieldConfigEditorRegistry) { - this.customFieldConfigs = registry; - return this; - } - - /** - * Enables configuration of panel's default field config - */ - setFieldConfigDefaults(defaultConfig: Partial) { - this.fieldConfigDefaults = { - defaults: {}, - overrides: [], - ...defaultConfig, - }; - - return this; - } +export interface PanelOptionsEditorItem extends RegistryItem { + editor: ComponentType>; + settings?: TSettings; } +export interface PanelOptionsEditorConfig + extends Pick, 'id' | 'description' | 'name' | 'settings'> {} + export interface PanelMenuItem { type?: 'submenu' | 'divider'; text?: string; diff --git a/packages/grafana-data/src/utils/OptionsUIBuilders.ts b/packages/grafana-data/src/utils/OptionsUIBuilders.ts new file mode 100644 index 00000000000..f6c0f5e15bb --- /dev/null +++ b/packages/grafana-data/src/utils/OptionsUIBuilders.ts @@ -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, + FieldPropertyEditorItem +> { + addNumberInput(config: FieldConfigEditorConfig) { + 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(config: FieldConfigEditorConfig) { + 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(config: FieldConfigEditorConfig>) { + 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(config: FieldConfigEditorConfig>) { + 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(config: FieldConfigEditorConfig) { + 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(config: FieldConfigEditorConfig) { + 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(config: FieldConfigEditorConfig) { + 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 { + addNumberInput(config: PanelOptionsEditorConfig) { + return this.addCustomEditor({ + ...config, + editor: standardEditorsRegistry.get('number').editor as any, + }); + } + + addTextInput(config: PanelOptionsEditorConfig) { + return this.addCustomEditor({ + ...config, + editor: standardEditorsRegistry.get('text').editor as any, + }); + } + + addSelect(config: PanelOptionsEditorConfig>) { + return this.addCustomEditor({ + ...config, + editor: standardEditorsRegistry.get('select').editor as any, + }); + } + + addRadio(config: PanelOptionsEditorConfig>) { + return this.addCustomEditor({ + ...config, + editor: standardEditorsRegistry.get('radio').editor as any, + }); + } + + addBooleanSwitch(config: PanelOptionsEditorConfig) { + return this.addCustomEditor({ + ...config, + editor: standardEditorsRegistry.get('boolean').editor as any, + }); + } + + addColorPicker(config: PanelOptionsEditorConfig): this { + return this.addCustomEditor({ + ...config, + editor: standardEditorsRegistry.get('color').editor as any, + settings: config.settings || {}, + }); + } + + addUnitPicker(config: PanelOptionsEditorConfig): this { + return this.addCustomEditor({ + ...config, + editor: standardEditorsRegistry.get('unit').editor as any, + }); + } +} diff --git a/packages/grafana-ui/src/components/FieldConfigs/fieldOverrides.test.ts b/packages/grafana-ui/src/components/FieldConfigs/fieldOverrides.test.ts index 9c31b64fcb4..bbd9bc690aa 100644 --- a/packages/grafana-ui/src/components/FieldConfigs/fieldOverrides.test.ts +++ b/packages/grafana-ui/src/components/FieldConfigs/fieldOverrides.test.ts @@ -1,5 +1,4 @@ import { - applyFieldOverrides, FieldConfig, FieldConfigSource, InterpolateFunction, @@ -7,16 +6,19 @@ import { FieldMatcherID, MutableDataFrame, DataFrame, + FieldType, + applyFieldOverrides, toDataFrame, standardFieldConfigEditorRegistry, - FieldType, + standardEditorsRegistry, } from '@grafana/data'; import { getTheme } from '../../themes'; -import { getStandardFieldConfigs } from './standardFieldConfigEditors'; +import { getStandardFieldConfigs, getStandardOptionEditors } from '../../utils'; describe('FieldOverrides', () => { beforeAll(() => { + standardEditorsRegistry.setInit(getStandardOptionEditors); standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs); }); diff --git a/packages/grafana-ui/src/components/FieldConfigs/links.tsx b/packages/grafana-ui/src/components/FieldConfigs/links.tsx deleted file mode 100644 index 1c923a82533..00000000000 --- a/packages/grafana-ui/src/components/FieldConfigs/links.tsx +++ /dev/null @@ -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> = ({ - value, - onChange, - context, -}) => { - return ( - - ); -}; - -export const DataLinksOverrideEditor: React.FC> = ({ - value, - onChange, - context, -}) => { - return ( - - ); -}; diff --git a/packages/grafana-ui/src/components/FieldConfigs/mappings.tsx b/packages/grafana-ui/src/components/FieldConfigs/mappings.tsx deleted file mode 100644 index b15f45902cf..00000000000 --- a/packages/grafana-ui/src/components/FieldConfigs/mappings.tsx +++ /dev/null @@ -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 -> { - constructor(props: FieldConfigEditorProps) { - super(props); - } - - render() { - const { onChange } = this.props; - let value = this.props.value; - if (!value) { - value = []; - } - - return ; - } -} - -export class ValueMappingsOverrideEditor extends React.PureComponent< - FieldOverrideEditorProps -> { - constructor(props: FieldOverrideEditorProps) { - super(props); - } - - render() { - const { onChange } = this.props; - let value = this.props.value; - if (!value) { - value = []; - } - - return ; - } -} diff --git a/packages/grafana-ui/src/components/FieldConfigs/number.tsx b/packages/grafana-ui/src/components/FieldConfigs/number.tsx deleted file mode 100644 index 4bb75adea24..00000000000 --- a/packages/grafana-ui/src/components/FieldConfigs/number.tsx +++ /dev/null @@ -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> = ({ - value, - onChange, - item, -}) => { - const { settings } = item; - return ( - { - onChange( - settings.integer ? toIntegerOrUndefined(e.currentTarget.value) : toFloatOrUndefined(e.currentTarget.value) - ); - }} - /> - ); -}; - -export const NumberOverrideEditor: React.FC> = ({ - value, - onChange, - item, -}) => { - const { settings } = item; - return ( - { - onChange( - settings.integer ? toIntegerOrUndefined(e.currentTarget.value) : toFloatOrUndefined(e.currentTarget.value) - ); - }} - /> - ); -}; diff --git a/packages/grafana-ui/src/components/FieldConfigs/select.tsx b/packages/grafana-ui/src/components/FieldConfigs/select.tsx deleted file mode 100644 index 3579ef1e31f..00000000000 --- a/packages/grafana-ui/src/components/FieldConfigs/select.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { FC } from 'react'; - -import { FieldOverrideContext, FieldOverrideEditorProps, FieldConfigEditorProps, SelectableValue } from '@grafana/data'; -import Forms from '../Forms'; - -export interface SelectFieldConfigSettings { - options: Array>; -} - -export const selectOverrideProcessor = ( - value: any, - context: FieldOverrideContext, - settings: SelectFieldConfigSettings -) => { - return value; -}; - -export const SelectValueEditor: FC>> = ({ - item, - value, - onChange, -}) => { - return onChange(e.value)} options={item.settings.options} />; -}; - -export const SelectOverrideEditor: FC>> = ({ - item, - value, - onChange, -}) => { - return onChange(e.value)} options={item.settings.options} />; -}; diff --git a/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditors.tsx b/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditors.tsx deleted file mode 100644 index fde35794afa..00000000000 --- a/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditors.tsx +++ /dev/null @@ -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 = { - 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 = { - 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 = { - 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 = { - 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 = { - 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 = { - 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 = { - 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 = { - 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 = { - 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]; -}; diff --git a/packages/grafana-ui/src/components/FieldConfigs/string.tsx b/packages/grafana-ui/src/components/FieldConfigs/string.tsx deleted file mode 100644 index 80dfa9eadb6..00000000000 --- a/packages/grafana-ui/src/components/FieldConfigs/string.tsx +++ /dev/null @@ -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> = ({ - value, - onChange, -}) => { - return onChange(e.currentTarget.value)} />; -}; - -export const StringOverrideEditor: React.FC> = ({ - value, - onChange, -}) => { - return onChange(e.currentTarget.value)} />; -}; diff --git a/packages/grafana-ui/src/components/FieldConfigs/thresholds.tsx b/packages/grafana-ui/src/components/FieldConfigs/thresholds.tsx deleted file mode 100644 index 5b13085dc1d..00000000000 --- a/packages/grafana-ui/src/components/FieldConfigs/thresholds.tsx +++ /dev/null @@ -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 -> { - constructor(props: FieldConfigEditorProps) { - 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 ; - } -} - -export class ThresholdsOverrideEditor extends React.PureComponent< - FieldOverrideEditorProps -> { - constructor(props: FieldOverrideEditorProps) { - super(props); - } - - render() { - return
THRESHOLDS OVERRIDE EDITOR {this.props.item.name}
; - } -} diff --git a/packages/grafana-ui/src/components/FieldConfigs/units.tsx b/packages/grafana-ui/src/components/FieldConfigs/units.tsx deleted file mode 100644 index c5061e675cc..00000000000 --- a/packages/grafana-ui/src/components/FieldConfigs/units.tsx +++ /dev/null @@ -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> = ({ - value, - onChange, -}) => { - return ; -}; - -export const UnitOverrideEditor: React.FC> = ({ - value, - onChange, -}) => { - return ; -}; diff --git a/packages/grafana-ui/src/components/OptionsUI/color.tsx b/packages/grafana-ui/src/components/OptionsUI/color.tsx new file mode 100644 index 00000000000..01f6f7f9480 --- /dev/null +++ b/packages/grafana-ui/src/components/OptionsUI/color.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { FieldConfigEditorProps, ColorFieldConfigSettings } from '@grafana/data'; +import { ColorPicker } from '../ColorPicker/ColorPicker'; + +export const ColorValueEditor: React.FC> = ({ + value, + onChange, + item, +}) => { + return ; +}; diff --git a/packages/grafana-ui/src/components/OptionsUI/links.tsx b/packages/grafana-ui/src/components/OptionsUI/links.tsx new file mode 100644 index 00000000000..09d5e23e59d --- /dev/null +++ b/packages/grafana-ui/src/components/OptionsUI/links.tsx @@ -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> = ({ + value, + onChange, + context, +}) => { + return ( + + ); +}; diff --git a/packages/grafana-ui/src/components/OptionsUI/mappings.tsx b/packages/grafana-ui/src/components/OptionsUI/mappings.tsx new file mode 100644 index 00000000000..55a1a674be8 --- /dev/null +++ b/packages/grafana-ui/src/components/OptionsUI/mappings.tsx @@ -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 +> { + constructor(props: FieldConfigEditorProps) { + super(props); + } + + render() { + const { onChange } = this.props; + let value = this.props.value; + if (!value) { + value = []; + } + + return ; + } +} diff --git a/packages/grafana-ui/src/components/OptionsUI/number.tsx b/packages/grafana-ui/src/components/OptionsUI/number.tsx new file mode 100644 index 00000000000..fe364588997 --- /dev/null +++ b/packages/grafana-ui/src/components/OptionsUI/number.tsx @@ -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> = ({ + value, + onChange, + item, +}) => { + const { settings } = item; + return ( + { + onChange( + settings.integer ? toIntegerOrUndefined(e.currentTarget.value) : toFloatOrUndefined(e.currentTarget.value) + ); + }} + /> + ); +}; diff --git a/packages/grafana-ui/src/components/OptionsUI/select.tsx b/packages/grafana-ui/src/components/OptionsUI/select.tsx new file mode 100644 index 00000000000..f3d9dfc8d7e --- /dev/null +++ b/packages/grafana-ui/src/components/OptionsUI/select.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { FieldConfigEditorProps, SelectFieldConfigSettings } from '@grafana/data'; +import Forms from '../Forms'; + +export function SelectValueEditor({ + value, + onChange, + item, +}: FieldConfigEditorProps>) { + return defaultValue={value} onChange={e => onChange(e.value)} options={item.settings.options} />; +} diff --git a/packages/grafana-ui/src/components/OptionsUI/string.tsx b/packages/grafana-ui/src/components/OptionsUI/string.tsx new file mode 100644 index 00000000000..b0b460b9f7d --- /dev/null +++ b/packages/grafana-ui/src/components/OptionsUI/string.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { FieldConfigEditorProps, StringFieldConfigSettings } from '@grafana/data'; +import Forms from '../Forms'; + +export const StringValueEditor: React.FC> = ({ + value, + onChange, +}) => { + return onChange(e.currentTarget.value)} />; +}; diff --git a/packages/grafana-ui/src/components/OptionsUI/thresholds.tsx b/packages/grafana-ui/src/components/OptionsUI/thresholds.tsx new file mode 100644 index 00000000000..9b89fb8ff42 --- /dev/null +++ b/packages/grafana-ui/src/components/OptionsUI/thresholds.tsx @@ -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 +> { + constructor(props: FieldConfigEditorProps) { + 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 ; + } +} diff --git a/packages/grafana-ui/src/components/OptionsUI/units.tsx b/packages/grafana-ui/src/components/OptionsUI/units.tsx new file mode 100644 index 00000000000..1803d100445 --- /dev/null +++ b/packages/grafana-ui/src/components/OptionsUI/units.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +import { FieldConfigEditorProps, UnitFieldConfigSettings } from '@grafana/data'; +import { UnitPicker } from '../UnitPicker/UnitPicker'; + +export const UnitValueEditor: React.FC> = ({ + value, + onChange, +}) => { + return ; +}; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 18645966f08..fe43908e347 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -129,24 +129,9 @@ export { Drawer } from './Drawer/Drawer'; export { Slider } from './Slider/Slider'; // TODO: namespace!! -export { - StringValueEditor, - StringOverrideEditor, - stringOverrideProcessor, - StringFieldConfigSettings, -} from './FieldConfigs/string'; -export { - NumberValueEditor, - NumberOverrideEditor, - numberOverrideProcessor, - NumberFieldConfigSettings, -} from './FieldConfigs/number'; -export { - selectOverrideProcessor, - SelectValueEditor, - SelectOverrideEditor, - SelectFieldConfigSettings, -} from './FieldConfigs/select'; +export { StringValueEditor } from './OptionsUI/string'; +export { NumberValueEditor } from './OptionsUI/number'; +export { SelectValueEditor } from './OptionsUI/select'; export { FieldConfigItemHeaderTitle } from './FieldConfigs/FieldConfigItemHeaderTitle'; // Next-gen forms @@ -154,5 +139,5 @@ export { default as Forms } from './Forms'; export * from './Button'; export { ValuePicker } from './ValuePicker/ValuePicker'; export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI'; -export { getStandardFieldConfigs } from './FieldConfigs/standardFieldConfigEditors'; + export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout'; diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index 09cdd61d0b2..659ae52851b 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -8,3 +8,6 @@ export { default as ansicolor } from './ansicolor'; import * as DOMUtil from './dom'; // includes Element.closest polyfil export { DOMUtil }; + +// Exposes standard editors for registries of optionsUi config and panel options UI +export { getStandardFieldConfigs, getStandardOptionEditors } from './standardEditors'; diff --git a/packages/grafana-ui/src/utils/standardEditors.tsx b/packages/grafana-ui/src/utils/standardEditors.tsx new file mode 100644 index 00000000000..481b2a4da6a --- /dev/null +++ b/packages/grafana-ui/src/utils/standardEditors.tsx @@ -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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + id: 'number', + name: 'Number', + description: 'Allows numeric values input', + editor: NumberValueEditor as any, + }; + + const text: StandardEditorsRegistryItem = { + id: 'text', + name: 'Text', + description: 'Allows string values input', + editor: StringValueEditor as any, + }; + + const boolean: StandardEditorsRegistryItem = { + id: 'boolean', + name: 'Boolean', + description: 'Allows boolean values input', + editor: props => props.onChange(e.currentTarget.checked)} />, + }; + + const select: StandardEditorsRegistryItem = { + id: 'select', + name: 'Select', + description: 'Allows option selection', + editor: props => ( + props.onChange(e.value)} + options={props.item.settings?.options} + /> + ), + }; + + const radio: StandardEditorsRegistryItem = { + id: 'radio', + name: 'Radio', + description: 'Allows option selection', + editor: props => , + }; + + const unit: StandardEditorsRegistryItem = { + id: 'unit', + name: 'Unit', + description: 'Allows unit input', + editor: UnitValueEditor as any, + }; + + const thresholds: StandardEditorsRegistryItem = { + id: 'thresholds', + name: 'Thresholds', + description: 'Allows defining thresholds', + editor: ThresholdsValueEditor as any, + }; + + const mappings: StandardEditorsRegistryItem = { + id: 'mappings', + name: 'Mappings', + description: 'Allows defining value mappings', + editor: ValueMappingsValueEditor as any, + }; + + const color: StandardEditorsRegistryItem = { + id: 'color', + name: 'Color', + description: 'Allows color selection', + editor: ColorValueEditor as any, + }; + + const links: StandardEditorsRegistryItem = { + id: 'links', + name: 'Links', + description: 'Allows defining data links', + editor: DataLinksValueEditor as any, + }; + + return [text, number, boolean, radio, select, unit, mappings, thresholds, links, color]; +}; diff --git a/public/app/app.ts b/public/app/app.ts index d1ba7518b9f..88d0923775a 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -25,7 +25,13 @@ import angular from 'angular'; import config from 'app/core/config'; // @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move import _ from 'lodash'; -import { AppEvents, setLocale, setMarkdownOptions, standardFieldConfigEditorRegistry } from '@grafana/data'; +import { + AppEvents, + setLocale, + setMarkdownOptions, + standardEditorsRegistry, + standardFieldConfigEditorRegistry, +} from '@grafana/data'; import appEvents from 'app/core/app_events'; import { addClassIfNoOverlayScrollbar } from 'app/core/utils/scrollbar'; import { checkBrowserCompatibility } from 'app/core/utils/browser'; @@ -40,7 +46,7 @@ import { PerformanceBackend } from './core/services/echo/backends/PerformanceBac import 'app/routes/GrafanaCtrl'; import 'app/features/all'; -import { getStandardFieldConfigs } from '@grafana/ui'; +import { getStandardFieldConfigs, getStandardOptionEditors } from '@grafana/ui'; import { getDefaultVariableAdapters, variableAdapters } from './features/variables/adapters'; // add move to lodash for backward compatabiltiy @@ -84,6 +90,8 @@ export class GrafanaApp { setLocale(config.bootData.user.locale); setMarkdownOptions({ sanitize: !config.disableSanitizeHtml }); + + standardEditorsRegistry.setInit(getStandardOptionEditors); standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs); variableAdapters.setInit(getDefaultVariableAdapters); diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx index 6840999ac93..bfa11741fa6 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneContent.tsx @@ -6,6 +6,7 @@ import { DefaultFieldConfigEditor, OverrideFieldConfigEditor } from './FieldConf import { AngularPanelOptions } from './AngularPanelOptions'; import { css } from 'emotion'; import { GeneralPanelOptions } from './GeneralPanelOptions'; +import { PanelOptionsEditor } from './PanelOptionsEditor'; export const OptionsPaneContent: React.FC<{ plugin?: PanelPlugin; @@ -64,8 +65,9 @@ export const OptionsPaneContent: React.FC<{ const renderCustomPanelSettings = useCallback( (plugin: PanelPlugin) => { + const editors = []; if (plugin.editor && panel) { - return ( + editors.push(
+ ); + } + + if (editors.length > 0) { + return <>{editors}; + } + return (
diff --git a/public/app/features/dashboard/components/PanelEditor/PanelOptionsEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelOptionsEditor.tsx new file mode 100644 index 00000000000..037f13d49fb --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/PanelOptionsEditor.tsx @@ -0,0 +1,32 @@ +import React, { useMemo } from 'react'; +import { PanelPlugin } from '@grafana/data'; +import { Forms } from '@grafana/ui'; + +interface PanelOptionsEditorProps { + plugin: PanelPlugin; + options: TOptions; + onChange: (options: TOptions) => void; +} + +export const PanelOptionsEditor: React.FC> = ({ 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 ( + + onOptionChange(e.id, value)} item={e} /> + + ); + })} + + ); +}; diff --git a/public/app/plugins/panel/table2/custom.tsx b/public/app/plugins/panel/table2/custom.tsx deleted file mode 100644 index e8cee0f08c2..00000000000 --- a/public/app/plugins/panel/table2/custom.tsx +++ /dev/null @@ -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(() => { - const columWidth: FieldPropertyEditorItem = { - 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> = { - 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]; -}); diff --git a/public/app/plugins/panel/table2/module.tsx b/public/app/plugins/panel/table2/module.tsx index 9a955e6c47a..5524e3b17a8 100644 --- a/public/app/plugins/panel/table2/module.tsx +++ b/public/app/plugins/panel/table2/module.tsx @@ -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(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", + }); + });