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