Commit 4eae1b54 by Dominik Prokop Committed by GitHub

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 tests
parent 0c9e50c7
......@@ -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';
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;
}
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>>();
......@@ -12,3 +12,4 @@ export * from './datetime';
export * from './text';
export * from './valueFormats';
export * from './field';
export { PanelPlugin } from './panel/PanelPlugin';
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);
});
});
});
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;
}
}
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;
});
}
}
......@@ -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<TValue, TSettings> {
item: FieldPropertyEditorItem<TValue, TSettings>; // 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<TValue, TSettings> {
item: FieldPropertyEditorItem<TValue, TSettings>;
export interface FieldConfigEditorProps<TValue, TSettings>
extends Omit<StandardEditorProps<TValue, TSettings>, 'item'> {
item: FieldPropertyEditorItem<TValue, TSettings>; // The property info
value: TValue;
context: FieldOverrideContext;
onChange: (value?: any) => void;
onChange: (value?: TValue) => void;
}
export interface FieldOverrideEditorProps<TValue, TSettings> extends Omit<StandardEditorProps<TValue>, 'item'> {
item: FieldPropertyEditorItem<TValue, TSettings>;
context: FieldOverrideContext;
}
export interface FieldConfigEditorConfig<TSettings = any, TValue = any>
extends Omit<Pick<FieldPropertyEditorItem<TValue, TSettings>, 'id' | 'description' | 'name'>, 'settings'> {
settings?: TSettings;
shouldApply?: (field: Field) => boolean;
}
export interface FieldPropertyEditorItem<TValue = any, TSettings = any> extends RegistryItem {
......
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<TOptions = any> = (
prevOptions: any
) => Partial<TOptions>;
export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta> {
panel: ComponentType<PanelProps<TOptions>>;
editor?: ComponentClass<PanelEditorProps<TOptions>>;
customFieldConfigs?: FieldConfigEditorRegistry;
defaults?: TOptions;
fieldConfigDefaults?: FieldConfigSource = {
defaults: {},
overrides: [],
};
onPanelMigration?: PanelMigrationHandler<TOptions>;
onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
noPadding?: boolean;
export type PanelOptionEditorsRegistry = Registry<PanelOptionsEditorItem>;
/**
* 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;
}
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;
}
export interface PanelOptionsEditorProps<TValue> extends StandardEditorProps<TValue> {}
setCustomFieldConfigs(registry: FieldConfigEditorRegistry) {
this.customFieldConfigs = registry;
return this;
}
/**
* Enables configuration of panel's default field config
*/
setFieldConfigDefaults(defaultConfig: Partial<FieldConfigSource>) {
this.fieldConfigDefaults = {
defaults: {},
overrides: [],
...defaultConfig,
};
return this;
}
export interface PanelOptionsEditorItem<TValue = any, TSettings = any> extends RegistryItem {
editor: ComponentType<PanelOptionsEditorProps<TValue>>;
settings?: TSettings;
}
export interface PanelOptionsEditorConfig<TSettings = any, TValue = any>
extends Pick<PanelOptionsEditorItem<TValue, TSettings>, 'id' | 'description' | 'name' | 'settings'> {}
export interface PanelMenuItem {
type?: 'submenu' | 'divider';
text?: string;
......
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,
});
}
}
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);
});
......
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} />;
};
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];
};
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)} />;
};
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} />;
};
import { FieldOverrideContext, FieldConfigEditorProps, DataLink, FieldOverrideEditorProps } from '@grafana/data';
import React from 'react';
import { FieldConfigEditorProps, DataLink, DataLinksFieldConfigSettings } from '@grafana/data';
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,
......@@ -26,18 +16,3 @@ export const DataLinksValueEditor: React.FC<FieldConfigEditorProps<DataLink[], D
/>
);
};
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() : []}
/>
);
};
import React from 'react';
import { FieldOverrideContext, FieldOverrideEditorProps, FieldConfigEditorProps, ValueMapping } from '@grafana/data';
import { FieldConfigEditorProps, ValueMapping, ValueMappingFieldConfigSettings } 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>
> {
......@@ -30,21 +20,3 @@ export class ValueMappingsValueEditor extends React.PureComponent<
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} />;
}
}
import React from 'react';
import {
FieldOverrideContext,
FieldOverrideEditorProps,
FieldConfigEditorProps,
toIntegerOrUndefined,
toFloatOrUndefined,
NumberFieldConfigSettings,
} 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,
......@@ -51,26 +29,3 @@ export const NumberValueEditor: React.FC<FieldConfigEditorProps<number, NumberFi
/>
);
};
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)
);
}}
/>
);
};
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} />;
}
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)} />;
};
import React from 'react';
import {
FieldOverrideContext,
FieldOverrideEditorProps,
FieldConfigEditorProps,
ThresholdsConfig,
ThresholdsMode,
} from '@grafana/data';
import { FieldConfigEditorProps, ThresholdsConfig, ThresholdsMode, ThresholdsFieldConfigSettings } 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>
> {
......@@ -45,15 +26,3 @@ export class ThresholdsValueEditor extends React.PureComponent<
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>;
}
}
import React from 'react';
import { FieldOverrideEditorProps, FieldConfigEditorProps } from '@grafana/data';
import { FieldConfigEditorProps, UnitFieldConfigSettings } 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 />;
};
......@@ -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';
......@@ -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';
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];
};
......@@ -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);
......
......@@ -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(
<div className={styles.legacyOptions}>
<plugin.editor
data={data}
......@@ -78,6 +80,17 @@ export const OptionsPaneContent: React.FC<{
);
}
// When editor created declaratively
if (plugin.optionEditors && panel) {
editors.push(
<PanelOptionsEditor options={panel.getOptions()} onChange={onPanelOptionsChanged} plugin={plugin} />
);
}
if (editors.length > 0) {
return <>{editors}</>;
}
return (
<div className={styles.legacyOptions}>
<AngularPanelOptions panel={panel} dashboard={dashboard} plugin={plugin} />
......
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>
);
})}
</>
);
};
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];
});
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",
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment