mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
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 {
|
||||
|
@@ -0,0 +1,49 @@
|
||||
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);
|
||||
},
|
||||
});
|
||||
});
|
@@ -0,0 +1,93 @@
|
||||
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;
|
58
packages/grafana-ui/src/components/FieldConfigs/number.tsx
Normal file
58
packages/grafana-ui/src/components/FieldConfigs/number.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
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>;
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,116 @@
|
||||
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];
|
||||
}
|
||||
);
|
36
packages/grafana-ui/src/components/FieldConfigs/string.tsx
Normal file
36
packages/grafana-ui/src/components/FieldConfigs/string.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
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>;
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
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>;
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
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',
|
||||
};
|
@@ -0,0 +1,7 @@
|
||||
import { Registry } from '@grafana/data';
|
||||
import { fieldNameMatcherItem } from './FieldNameMatcherEditor';
|
||||
import { FieldMatcherUIRegistryItem } from './types';
|
||||
|
||||
export const fieldMatchersUI = new Registry<FieldMatcherUIRegistryItem<any>>(() => {
|
||||
return [fieldNameMatcherItem];
|
||||
});
|
14
packages/grafana-ui/src/components/MatchersUI/types.ts
Normal file
14
packages/grafana-ui/src/components/MatchersUI/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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';
|
||||
|
@@ -1,7 +1,7 @@
|
||||
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,47 +61,46 @@ 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 = {
|
||||
pluginLoadedCounter: 0,
|
||||
data: {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
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, () => {
|
||||
const panel = props.sourcePanel.getEditClone();
|
||||
this.state = {
|
||||
panel,
|
||||
pluginLoadedCounter: 0,
|
||||
data: {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
timeRange: DefaultTimeRange,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
27
public/app/plugins/panel/table2/custom.tsx
Normal file
27
public/app/plugins/panel/table2/custom.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
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);
|
||||
|
Reference in New Issue
Block a user