Commit da395729 by Ryan McKinley Committed by GitHub

FieldEditor: extendable FieldConfig UI (#21882)

* initial POC

* fix import

* field config editor in the sidebar

* field config editor in the sidebar

* field config editor in the sidebar

* sidebar

* include threshold in sidebar

* include threshold in sidebar

* include threshold in sidebar

* init to empty threshold

* merge

* Make sure editor is fully rendered when page is refreshed

* use scrollbars

* add matcher UI folder

* remove

* Field options basic editors

* Removed deebugger

* Make number field editor controlled

* Update public/app/features/dashboard/state/PanelModel.ts

* Update public/app/plugins/panel/gauge/GaugePanel.tsx

* Ready for production

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
parent 20c4d00d
import { MatcherConfig, FieldConfig } from '../types';
import { MatcherConfig, FieldConfig, Field } from '../types';
import { Registry, RegistryItem } from '../utils';
import { ComponentType } from 'react';
import { InterpolateFunction } from './panel';
import { DataFrame } from 'apache-arrow';
export interface DynamicConfigValue {
path: string;
......@@ -17,3 +21,38 @@ export interface FieldConfigSource {
// Rules to override individual values
overrides: ConfigOverrideRule[];
}
export interface FieldConfigEditorProps<TValue, TSettings> {
item: FieldPropertyEditorItem<TValue, TSettings>; // The property info
value: TValue;
onChange: (value: TValue) => void;
}
export interface FieldOverrideContext {
field: Field;
data: DataFrame;
replaceVariables: InterpolateFunction;
}
export interface FieldOverrideEditorProps<TValue, TSettings> {
item: FieldPropertyEditorItem<TValue, TSettings>;
value: any;
context: FieldOverrideContext;
onChange: (value: any) => void;
}
export interface FieldPropertyEditorItem<TValue = any, TSettings = any> extends RegistryItem {
// An editor the creates the well typed value
editor: ComponentType<FieldConfigEditorProps<TValue, TSettings>>;
// An editor that can be filled in with context info (template variables etc)
override: ComponentType<FieldOverrideEditorProps<TValue, TSettings>>;
// Convert the override value to a well typed value
process: (value: any, context: FieldOverrideContext, settings: TSettings) => TValue;
// Configuration options for the particular property
settings: TSettings;
}
export type FieldConfigEditorRegistry = Registry<FieldPropertyEditorItem>;
......@@ -5,6 +5,7 @@ import { ScopedVars } from './ScopedVars';
import { LoadingState } from './data';
import { DataFrame } from './dataFrame';
import { AbsoluteTimeRange, TimeRange, TimeZone } from './time';
import { FieldConfigEditorRegistry } from './fieldOverrides';
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
......@@ -72,6 +73,7 @@ export type PanelTypeChangedHandler<TOptions = any> = (
export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta> {
panel: ComponentType<PanelProps<TOptions>>;
editor?: ComponentClass<PanelEditorProps<TOptions>>;
customFieldConfigs?: FieldConfigEditorRegistry;
defaults?: TOptions;
onPanelMigration?: PanelMigrationHandler<TOptions>;
onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
......@@ -121,6 +123,11 @@ export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta>
this.onPanelTypeChanged = handler;
return this;
}
setCustomFieldConfigs(registry: FieldConfigEditorRegistry) {
this.customFieldConfigs = registry;
return this;
}
}
export interface PanelMenuItem {
......
import { storiesOf } from '@storybook/react';
import FieldConfigEditor from './FieldConfigEditor';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { FieldConfigSource, FieldConfigEditorRegistry, FieldPropertyEditorItem, Registry } from '@grafana/data';
import { NumberFieldConfigSettings, NumberValueEditor, NumberOverrideEditor, numberOverrideProcessor } from './number';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
const FieldConfigStories = storiesOf('UI/FieldConfig', module);
FieldConfigStories.addDecorator(withCenteredStory);
const cfg: FieldConfigSource = {
defaults: {
title: 'Hello',
decimals: 3,
},
overrides: [],
};
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,
},
};
export const customEditorRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(() => {
return [columWidth];
});
FieldConfigStories.add('default', () => {
return renderComponentWithTheme(FieldConfigEditor, {
config: cfg,
data: [],
custom: customEditorRegistry,
onChange: (config: FieldConfigSource) => {
console.log('Data', config);
},
});
});
import React from 'react';
import { FieldConfigEditorRegistry, FieldConfigSource, DataFrame, FieldPropertyEditorItem } from '@grafana/data';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
import Forms from '../Forms';
interface Props {
config: FieldConfigSource;
custom?: FieldConfigEditorRegistry; // custom fields
include?: string[]; // Ordered list of which fields should be shown/included
onChange: (config: FieldConfigSource) => void;
// Helpful for IntelliSense
data: DataFrame[];
}
/**
* Expects the container div to have size set and will fill it 100%
*/
export class FieldConfigEditor extends React.PureComponent<Props> {
private setDefaultValue = (name: string, value: any, custom: boolean) => {
const defaults = { ...this.props.config.defaults };
const remove = value === undefined || value === null || '';
if (custom) {
if (defaults.custom) {
if (remove) {
defaults.custom = { ...defaults.custom };
delete defaults.custom[name];
} else {
defaults.custom = { ...defaults.custom, [name]: value };
}
} else if (!remove) {
defaults.custom = { [name]: value };
}
} else if (remove) {
delete (defaults as any)[name];
} else {
(defaults as any)[name] = value;
}
this.props.onChange({
...this.props.config,
defaults,
});
};
renderEditor(item: FieldPropertyEditorItem, custom: boolean) {
const config = this.props.config.defaults;
const value = custom ? (config.custom ? config.custom[item.id] : undefined) : (config as any)[item.id];
return (
<Forms.Field label={item.name} description={item.description} key={`${item.id}/${custom}`}>
<item.editor item={item} value={value} onChange={v => this.setDefaultValue(item.id, v, custom)} />
</Forms.Field>
);
}
renderStandardConfigs() {
const { include } = this.props;
if (include) {
return include.map(f => this.renderEditor(standardFieldConfigEditorRegistry.get(f), false));
}
return standardFieldConfigEditorRegistry.list().map(f => this.renderEditor(f, false));
}
renderCustomConfigs() {
const { custom } = this.props;
if (!custom) {
return null;
}
return custom.list().map(f => this.renderEditor(f, true));
}
renderOverrides() {
return <div>Override rules</div>;
}
renderAddOverride() {
return <div>Override rules</div>;
}
render() {
return (
<div>
{this.renderStandardConfigs()}
{this.renderCustomConfigs()}
{this.renderOverrides()}
{this.renderAddOverride()}
</div>
);
}
}
export default FieldConfigEditor;
import React from 'react';
import { FieldOverrideContext, FieldOverrideEditorProps, FieldConfigEditorProps } 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}
type="number"
step={settings.step}
onChange={e => {
onChange(
item.settings.integer
? parseInt(e.currentTarget.value, settings.step || 10)
: parseFloat(e.currentTarget.value)
);
}}
/>
);
};
export class NumberOverrideEditor extends React.PureComponent<
FieldOverrideEditorProps<number, NumberFieldConfigSettings>
> {
constructor(props: FieldOverrideEditorProps<number, NumberFieldConfigSettings>) {
super(props);
}
render() {
return <div>SHOW OVERRIDE EDITOR {this.props.item.name}</div>;
}
}
import { FieldConfig } from '@grafana/data';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
describe('standardFieldConfigEditorRegistry', () => {
const dummyConfig: FieldConfig = {
title: 'Hello',
min: 10,
max: 10,
decimals: 10,
thresholds: {} as any,
noValue: 'no value',
unit: 'km/s',
};
it('make sure all fields have a valid name', () => {
standardFieldConfigEditorRegistry.list().forEach(v => {
if (!dummyConfig.hasOwnProperty(v.id)) {
fail(`Registry uses unknown property: ${v.id}`);
}
});
});
});
import { FieldConfigEditorRegistry, Registry, FieldPropertyEditorItem, ThresholdsConfig } from '@grafana/data';
import { StringValueEditor, StringOverrideEditor, stringOverrideProcessor, StringFieldConfigSettings } from './string';
import { NumberValueEditor, NumberOverrideEditor, numberOverrideProcessor, NumberFieldConfigSettings } from './number';
import {
ThresholdsValueEditor,
ThresholdsOverrideEditor,
thresholdsOverrideProcessor,
ThresholdsFieldConfigSettings,
} from './thresholds';
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',
},
};
const unit: FieldPropertyEditorItem<string, StringFieldConfigSettings> = {
id: 'unit', // Match field properties
name: 'Unit',
description: 'value units',
editor: StringValueEditor,
override: StringOverrideEditor,
process: stringOverrideProcessor,
settings: {
placeholder: 'none',
},
};
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',
},
};
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',
},
};
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,
},
};
const thresholds: FieldPropertyEditorItem<ThresholdsConfig, ThresholdsFieldConfigSettings> = {
id: 'thresholds', // Match field properties
name: 'Thresholds',
description: 'Manage Thresholds',
editor: ThresholdsValueEditor,
override: ThresholdsOverrideEditor,
process: thresholdsOverrideProcessor,
settings: {
// ??
},
};
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: '-',
},
};
export const standardFieldConfigEditorRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(
() => {
return [title, unit, min, max, decimals, thresholds, noValue];
}
);
import React from 'react';
import { FieldOverrideContext, FieldOverrideEditorProps, FieldConfigEditorProps } from '@grafana/data';
import Forms from '../Forms';
export interface StringFieldConfigSettings {
placeholder?: string;
maxLength?: number;
}
export const stringOverrideProcessor = (
value: any,
context: FieldOverrideContext,
settings: StringFieldConfigSettings
) => {
return `${value}`;
};
export const StringValueEditor: React.FC<FieldConfigEditorProps<string, StringFieldConfigSettings>> = ({
value,
onChange,
}) => {
return <Forms.Input value={value || ''} onChange={e => onChange(e.currentTarget.value)} />;
};
export class StringOverrideEditor extends React.PureComponent<
FieldOverrideEditorProps<string, StringFieldConfigSettings>
> {
constructor(props: FieldOverrideEditorProps<string, StringFieldConfigSettings>) {
super(props);
}
render() {
return <div>SHOW OVERRIDE EDITOR {this.props.item.name}</div>;
}
}
import React from 'react';
import {
FieldOverrideContext,
FieldOverrideEditorProps,
FieldConfigEditorProps,
ThresholdsConfig,
ThresholdsMode,
} from '@grafana/data';
import { ThresholdsEditor } from '../ThresholdsEditor/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 showAlphaUI={true} 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 { MatcherUIProps, FieldMatcherUIRegistryItem } from './types';
import { FieldMatcherID, fieldMatchers } from '@grafana/data';
export class FieldNameMatcherEditor extends React.PureComponent<MatcherUIProps<string>> {
render() {
const { matcher } = this.props;
return <div>TODO: MATCH STRING for: {matcher.id}</div>;
}
}
export const fieldNameMatcherItem: FieldMatcherUIRegistryItem<string> = {
id: FieldMatcherID.byName,
component: FieldNameMatcherEditor,
matcher: fieldMatchers.get(FieldMatcherID.byName),
name: 'Filter by name',
description: 'Set properties for fields matching the name',
};
import { Registry } from '@grafana/data';
import { fieldNameMatcherItem } from './FieldNameMatcherEditor';
import { FieldMatcherUIRegistryItem } from './types';
export const fieldMatchersUI = new Registry<FieldMatcherUIRegistryItem<any>>(() => {
return [fieldNameMatcherItem];
});
import React from 'react';
import { DataFrame, RegistryItem, FieldMatcherInfo } from '@grafana/data';
export interface FieldMatcherUIRegistryItem<TOptions> extends RegistryItem {
component: React.ComponentType<MatcherUIProps<TOptions>>;
matcher: FieldMatcherInfo<TOptions>;
}
export interface MatcherUIProps<T> {
matcher: FieldMatcherInfo<T>;
data: DataFrame[];
options: T;
onChange: (options: T) => void;
}
......@@ -117,5 +117,20 @@ export { default as Chart } from './Chart';
export { Icon } from './Icon/Icon';
export { Drawer } from './Drawer/Drawer';
// TODO: namespace!!
export { FieldConfigEditor } from './FieldConfigs/FieldConfigEditor';
export {
StringValueEditor,
StringOverrideEditor,
stringOverrideProcessor,
StringFieldConfigSettings,
} from './FieldConfigs/string';
export {
NumberValueEditor,
NumberOverrideEditor,
numberOverrideProcessor,
NumberFieldConfigSettings,
} from './FieldConfigs/number';
// Next-gen forms
export { default as Forms } from './Forms';
import React, { PureComponent } from 'react';
import { GrafanaTheme, FieldConfigSource, PanelData, LoadingState, DefaultTimeRange, PanelEvents } from '@grafana/data';
import { stylesFactory, Forms, FieldConfigEditor, CustomScrollbar } from '@grafana/ui';
import { css, cx } from 'emotion';
import { GrafanaTheme, PanelData, LoadingState, DefaultTimeRange, PanelEvents } from '@grafana/data';
import { stylesFactory, Forms, CustomScrollbar } from '@grafana/ui';
import config from 'app/core/config';
import { PanelModel } from '../../state/PanelModel';
......@@ -61,20 +61,27 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
interface Props {
dashboard: DashboardModel;
panel: PanelModel;
sourcePanel: PanelModel;
updateLocation: typeof updateLocation;
}
interface State {
pluginLoadedCounter: number;
dirtyPanel?: PanelModel;
panel: PanelModel;
data: PanelData;
}
export class PanelEditor extends PureComponent<Props, State> {
querySubscription: Unsubscribable;
state: State = {
constructor(props: Props) {
super(props);
// To ensure visualisation settings are re-rendered when plugin has loaded
// panelInitialised event is emmited from PanelChrome
const panel = props.sourcePanel.getEditClone();
this.state = {
panel,
pluginLoadedCounter: 0,
data: {
state: LoadingState.NotStarted,
......@@ -82,26 +89,18 @@ export class PanelEditor extends PureComponent<Props, State> {
timeRange: DefaultTimeRange,
},
};
}
constructor(props: Props) {
super(props);
// To ensure visualisation settings are re-rendered when plugin has loaded
// panelInitialised event is emmited from PanelChrome
props.panel.events.on(PanelEvents.panelInitialized, () => {
componentDidMount() {
const { sourcePanel } = this.props;
const { panel } = this.state;
panel.events.on(PanelEvents.panelInitialized, () => {
this.setState(state => ({
pluginLoadedCounter: state.pluginLoadedCounter + 1,
}));
});
}
componentDidMount() {
const { panel } = this.props;
const dirtyPanel = panel.getEditClone();
this.setState({ dirtyPanel });
// Get data from any pending
panel
sourcePanel
.getQueryRunner()
.getData()
.subscribe({
......@@ -112,7 +111,7 @@ export class PanelEditor extends PureComponent<Props, State> {
});
// Listen for queries on the new panel
const queryRunner = dirtyPanel.getQueryRunner();
const queryRunner = panel.getQueryRunner();
this.querySubscription = queryRunner.getData().subscribe({
next: (data: PanelData) => this.setState({ data }),
});
......@@ -126,9 +125,9 @@ export class PanelEditor extends PureComponent<Props, State> {
}
onPanelUpdate = () => {
const { dirtyPanel } = this.state;
const { panel } = this.state;
const { dashboard } = this.props;
dashboard.updatePanel(dirtyPanel);
dashboard.updatePanel(panel);
};
onPanelExit = () => {
......@@ -147,6 +146,59 @@ export class PanelEditor extends PureComponent<Props, State> {
});
};
onFieldConfigsChange = (fieldOptions: FieldConfigSource) => {
// NOTE: for now, assume this is from 'fieldOptions' -- TODO? put on panel model directly?
const { panel } = this.state;
const options = panel.getOptions();
panel.updateOptions({
...options,
fieldOptions, // Assume it is from shared singlestat -- TODO own property?
});
this.forceUpdate();
};
renderFieldOptions() {
const { panel, data } = this.state;
const { plugin } = panel;
const fieldOptions = panel.options['fieldOptions'] as FieldConfigSource;
if (!fieldOptions || !plugin) {
return null;
}
return (
<div>
<FieldConfigEditor
config={fieldOptions}
custom={plugin.customFieldConfigs}
onChange={this.onFieldConfigsChange}
data={data.series}
/>
</div>
);
}
onPanelOptionsChanged = (options: any) => {
this.state.panel.updateOptions(options);
this.forceUpdate();
};
/**
* The existing visualization tab
*/
renderVisSettings() {
const { data, panel } = this.state;
const { plugin } = panel;
if (!plugin) {
return null; // not yet ready
}
if (plugin.editor && panel) {
return <plugin.editor data={data} options={panel.getOptions()} onOptionsChange={this.onPanelOptionsChanged} />;
}
return <div>No editor (angular?)</div>;
}
onDragFinished = () => {
document.body.style.cursor = 'auto';
console.log('TODO, save splitter settings');
......@@ -154,11 +206,10 @@ export class PanelEditor extends PureComponent<Props, State> {
render() {
const { dashboard } = this.props;
const { dirtyPanel } = this.state;
const { panel } = this.state;
const styles = getStyles(config.theme);
if (!dirtyPanel) {
if (!panel) {
return null;
}
......@@ -168,7 +219,7 @@ export class PanelEditor extends PureComponent<Props, State> {
<button className="navbar-edit__back-btn" onClick={this.onPanelExit}>
<i className="fa fa-arrow-left"></i>
</button>
{this.props.panel.title}
{panel.title}
<Forms.Button variant="destructive" onClick={this.onDiscard}>
Discard
</Forms.Button>
......@@ -194,7 +245,7 @@ export class PanelEditor extends PureComponent<Props, State> {
<div className={styles.fill}>
<DashboardPanel
dashboard={dashboard}
panel={dirtyPanel}
panel={panel}
isEditing={false}
isInEditMode
isFullscreen={false}
......@@ -202,12 +253,15 @@ export class PanelEditor extends PureComponent<Props, State> {
/>
</div>
<div className={styles.noScrollPaneContent}>
<QueriesTab panel={dirtyPanel} dashboard={dashboard} />
<QueriesTab panel={panel} dashboard={dashboard} />
</div>
</SplitPane>
<div className={styles.noScrollPaneContent}>
<CustomScrollbar>
<div>Viz settings</div>
<div style={{ padding: '10px' }}>
{this.renderFieldOptions()}
{this.renderVisSettings()}
</div>
</CustomScrollbar>
</div>
</SplitPane>
......
......@@ -327,7 +327,7 @@ export class DashboardPage extends PureComponent<Props, State> {
{inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} selectedTab={inspectTab} />}
{editPanel && (
<Portal>
<PanelEditor dashboard={dashboard} panel={editPanel} />
<PanelEditor dashboard={dashboard} sourcePanel={editPanel} />
</Portal>
)}
</div>
......
......@@ -172,7 +172,6 @@ export class PanelChrome extends PureComponent<Props, State> {
}
onRefresh = () => {
// debugger
const { panel, isInView, width } = this.props;
if (!isInView) {
console.log('Refresh when panel is visible', panel.id);
......
......@@ -175,6 +175,7 @@ export class PanelModel {
updateOptions(options: object) {
this.options = options;
this.render();
}
......
......@@ -56,11 +56,8 @@ export class PanelQueryRunner {
private transformations?: DataTransformerConfig[];
private lastResult?: PanelData;
constructor(data?: PanelData) {
constructor() {
this.subject = new ReplaySubject(1);
if (data) {
this.pipeDataToSubject(data);
}
}
/**
......
import { FieldPropertyEditorItem, Registry, FieldConfigEditorRegistry } from '@grafana/data';
import {
NumberValueEditor,
NumberOverrideEditor,
numberOverrideProcessor,
NumberFieldConfigSettings,
} from '@grafana/ui';
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,
},
};
export const tableFieldRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(() => {
return [columWidth];
});
......@@ -2,6 +2,10 @@ 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).setEditor(TablePanelEditor);
export const plugin = new PanelPlugin<Options>(TablePanel)
.setDefaults(defaults)
.setCustomFieldConfigs(tableFieldRegistry)
.setEditor(TablePanelEditor);
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