From da395729c3ac46d6ce2ea6f196261bbcc470f6d4 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Sat, 8 Feb 2020 18:29:09 +0100 Subject: [PATCH] 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 --- .../grafana-data/src/types/fieldOverrides.ts | 41 +++++- packages/grafana-data/src/types/panel.ts | 7 ++ .../FieldConfigs/FieldConfigEditor.story.tsx | 49 ++++++++ .../FieldConfigs/FieldConfigEditor.tsx | 93 ++++++++++++++ .../src/components/FieldConfigs/number.tsx | 58 +++++++++ .../standardFieldConfigEditorRegistry.test.ts | 22 ++++ .../standardFieldConfigEditorRegistry.tsx | 116 +++++++++++++++++ .../src/components/FieldConfigs/string.tsx | 36 ++++++ .../components/FieldConfigs/thresholds.tsx | 59 +++++++++ .../MatchersUI/FieldNameMatcherEditor.tsx | 19 +++ .../components/MatchersUI/fieldMatchersUI.ts | 7 ++ .../src/components/MatchersUI/types.ts | 14 +++ packages/grafana-ui/src/components/index.ts | 15 +++ .../components/PanelEditor/PanelEditor.tsx | 118 +++++++++++++----- .../dashboard/containers/DashboardPage.tsx | 2 +- .../dashboard/dashgrid/PanelChrome.tsx | 1 - .../features/dashboard/state/PanelModel.ts | 1 + .../dashboard/state/PanelQueryRunner.ts | 5 +- public/app/plugins/panel/table2/custom.tsx | 27 ++++ public/app/plugins/panel/table2/module.tsx | 6 +- 20 files changed, 656 insertions(+), 40 deletions(-) create mode 100644 packages/grafana-ui/src/components/FieldConfigs/FieldConfigEditor.story.tsx create mode 100644 packages/grafana-ui/src/components/FieldConfigs/FieldConfigEditor.tsx create mode 100644 packages/grafana-ui/src/components/FieldConfigs/number.tsx create mode 100644 packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.test.ts create mode 100644 packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.tsx create mode 100644 packages/grafana-ui/src/components/FieldConfigs/string.tsx create mode 100644 packages/grafana-ui/src/components/FieldConfigs/thresholds.tsx create mode 100644 packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx create mode 100644 packages/grafana-ui/src/components/MatchersUI/fieldMatchersUI.ts create mode 100644 packages/grafana-ui/src/components/MatchersUI/types.ts create mode 100644 public/app/plugins/panel/table2/custom.tsx diff --git a/packages/grafana-data/src/types/fieldOverrides.ts b/packages/grafana-data/src/types/fieldOverrides.ts index c2b2c670f85..8f9d1c156dc 100644 --- a/packages/grafana-data/src/types/fieldOverrides.ts +++ b/packages/grafana-data/src/types/fieldOverrides.ts @@ -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 { + item: FieldPropertyEditorItem; // The property info + value: TValue; + onChange: (value: TValue) => void; +} + +export interface FieldOverrideContext { + field: Field; + data: DataFrame; + replaceVariables: InterpolateFunction; +} + +export interface FieldOverrideEditorProps { + item: FieldPropertyEditorItem; + value: any; + context: FieldOverrideContext; + onChange: (value: any) => void; +} + +export interface FieldPropertyEditorItem extends RegistryItem { + // An editor the creates the well typed value + editor: ComponentType>; + + // An editor that can be filled in with context info (template variables etc) + override: ComponentType>; + + // 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; diff --git a/packages/grafana-data/src/types/panel.ts b/packages/grafana-data/src/types/panel.ts index 8db1f73e2c3..ad2627488e7 100644 --- a/packages/grafana-data/src/types/panel.ts +++ b/packages/grafana-data/src/types/panel.ts @@ -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 = ( export class PanelPlugin extends GrafanaPlugin { panel: ComponentType>; editor?: ComponentClass>; + customFieldConfigs?: FieldConfigEditorRegistry; defaults?: TOptions; onPanelMigration?: PanelMigrationHandler; onPanelTypeChanged?: PanelTypeChangedHandler; @@ -121,6 +123,11 @@ export class PanelPlugin extends GrafanaPlugin this.onPanelTypeChanged = handler; return this; } + + setCustomFieldConfigs(registry: FieldConfigEditorRegistry) { + this.customFieldConfigs = registry; + return this; + } } export interface PanelMenuItem { diff --git a/packages/grafana-ui/src/components/FieldConfigs/FieldConfigEditor.story.tsx b/packages/grafana-ui/src/components/FieldConfigs/FieldConfigEditor.story.tsx new file mode 100644 index 00000000000..c0b35bd9d23 --- /dev/null +++ b/packages/grafana-ui/src/components/FieldConfigs/FieldConfigEditor.story.tsx @@ -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 = { + 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(() => { + return [columWidth]; +}); + +FieldConfigStories.add('default', () => { + return renderComponentWithTheme(FieldConfigEditor, { + config: cfg, + data: [], + custom: customEditorRegistry, + onChange: (config: FieldConfigSource) => { + console.log('Data', config); + }, + }); +}); diff --git a/packages/grafana-ui/src/components/FieldConfigs/FieldConfigEditor.tsx b/packages/grafana-ui/src/components/FieldConfigs/FieldConfigEditor.tsx new file mode 100644 index 00000000000..30fc75bff99 --- /dev/null +++ b/packages/grafana-ui/src/components/FieldConfigs/FieldConfigEditor.tsx @@ -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 { + 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 ( + + this.setDefaultValue(item.id, v, custom)} /> + + ); + } + + 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
Override rules
; + } + + renderAddOverride() { + return
Override rules
; + } + + render() { + return ( +
+ {this.renderStandardConfigs()} + {this.renderCustomConfigs()} + {this.renderOverrides()} + {this.renderAddOverride()} +
+ ); + } +} + +export default FieldConfigEditor; diff --git a/packages/grafana-ui/src/components/FieldConfigs/number.tsx b/packages/grafana-ui/src/components/FieldConfigs/number.tsx new file mode 100644 index 00000000000..5cd2e46cf14 --- /dev/null +++ b/packages/grafana-ui/src/components/FieldConfigs/number.tsx @@ -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> = ({ + value, + onChange, + item, +}) => { + const { settings } = item; + return ( + { + onChange( + item.settings.integer + ? parseInt(e.currentTarget.value, settings.step || 10) + : parseFloat(e.currentTarget.value) + ); + }} + /> + ); +}; + +export class NumberOverrideEditor extends React.PureComponent< + FieldOverrideEditorProps +> { + constructor(props: FieldOverrideEditorProps) { + super(props); + } + + render() { + return
SHOW OVERRIDE EDITOR {this.props.item.name}
; + } +} diff --git a/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.test.ts b/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.test.ts new file mode 100644 index 00000000000..5589ba29c7d --- /dev/null +++ b/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.test.ts @@ -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}`); + } + }); + }); +}); diff --git a/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.tsx b/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.tsx new file mode 100644 index 00000000000..6f446e00148 --- /dev/null +++ b/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.tsx @@ -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 = { + id: 'title', // Match field properties + name: 'Title', + description: 'The field title', + + editor: StringValueEditor, + override: StringOverrideEditor, + process: stringOverrideProcessor, + + settings: { + placeholder: 'auto', + }, +}; + +const unit: FieldPropertyEditorItem = { + id: 'unit', // Match field properties + name: 'Unit', + description: 'value units', + + editor: StringValueEditor, + override: StringOverrideEditor, + process: stringOverrideProcessor, + + settings: { + placeholder: 'none', + }, +}; + +const min: FieldPropertyEditorItem = { + id: 'min', // Match field properties + name: 'Min', + description: 'Minimum expected value', + + editor: NumberValueEditor, + override: NumberOverrideEditor, + process: numberOverrideProcessor, + + settings: { + placeholder: 'auto', + }, +}; + +const max: FieldPropertyEditorItem = { + id: 'max', // Match field properties + name: 'Max', + description: 'Maximum expected value', + + editor: NumberValueEditor, + override: NumberOverrideEditor, + process: numberOverrideProcessor, + + settings: { + placeholder: 'auto', + }, +}; + +const decimals: FieldPropertyEditorItem = { + id: 'decimals', // Match field properties + name: 'Decimals', + description: 'How many decimal places should be shown on a number', + + editor: NumberValueEditor, + override: NumberOverrideEditor, + process: numberOverrideProcessor, + + settings: { + placeholder: 'auto', + min: 0, + max: 15, + integer: true, + }, +}; + +const thresholds: FieldPropertyEditorItem = { + id: 'thresholds', // Match field properties + name: 'Thresholds', + description: 'Manage Thresholds', + + editor: ThresholdsValueEditor, + override: ThresholdsOverrideEditor, + process: thresholdsOverrideProcessor, + + settings: { + // ?? + }, +}; + +const noValue: FieldPropertyEditorItem = { + id: 'noValue', // Match field properties + name: 'No Value', + description: 'What to show when there is no value', + + editor: StringValueEditor, + override: StringOverrideEditor, + process: stringOverrideProcessor, + + settings: { + placeholder: '-', + }, +}; + +export const standardFieldConfigEditorRegistry: FieldConfigEditorRegistry = new Registry( + () => { + return [title, unit, min, max, decimals, thresholds, noValue]; + } +); diff --git a/packages/grafana-ui/src/components/FieldConfigs/string.tsx b/packages/grafana-ui/src/components/FieldConfigs/string.tsx new file mode 100644 index 00000000000..6d5bebf1925 --- /dev/null +++ b/packages/grafana-ui/src/components/FieldConfigs/string.tsx @@ -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> = ({ + value, + onChange, +}) => { + return onChange(e.currentTarget.value)} />; +}; + +export class StringOverrideEditor extends React.PureComponent< + FieldOverrideEditorProps +> { + constructor(props: FieldOverrideEditorProps) { + super(props); + } + + render() { + return
SHOW OVERRIDE EDITOR {this.props.item.name}
; + } +} diff --git a/packages/grafana-ui/src/components/FieldConfigs/thresholds.tsx b/packages/grafana-ui/src/components/FieldConfigs/thresholds.tsx new file mode 100644 index 00000000000..a62d10b186a --- /dev/null +++ b/packages/grafana-ui/src/components/FieldConfigs/thresholds.tsx @@ -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 +> { + constructor(props: FieldConfigEditorProps) { + super(props); + } + + render() { + const { onChange } = this.props; + let value = this.props.value; + if (!value) { + value = { + mode: ThresholdsMode.Percentage, + + // Must be sorted by 'value', first value is always -Infinity + steps: [ + // anything? + ], + }; + } + + return ; + } +} + +export class ThresholdsOverrideEditor extends React.PureComponent< + FieldOverrideEditorProps +> { + constructor(props: FieldOverrideEditorProps) { + super(props); + } + + render() { + return
THRESHOLDS OVERRIDE EDITOR {this.props.item.name}
; + } +} diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx new file mode 100644 index 00000000000..4e16ecf0bd4 --- /dev/null +++ b/packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx @@ -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> { + render() { + const { matcher } = this.props; + + return
TODO: MATCH STRING for: {matcher.id}
; + } +} + +export const fieldNameMatcherItem: FieldMatcherUIRegistryItem = { + id: FieldMatcherID.byName, + component: FieldNameMatcherEditor, + matcher: fieldMatchers.get(FieldMatcherID.byName), + name: 'Filter by name', + description: 'Set properties for fields matching the name', +}; diff --git a/packages/grafana-ui/src/components/MatchersUI/fieldMatchersUI.ts b/packages/grafana-ui/src/components/MatchersUI/fieldMatchersUI.ts new file mode 100644 index 00000000000..868409ebb0e --- /dev/null +++ b/packages/grafana-ui/src/components/MatchersUI/fieldMatchersUI.ts @@ -0,0 +1,7 @@ +import { Registry } from '@grafana/data'; +import { fieldNameMatcherItem } from './FieldNameMatcherEditor'; +import { FieldMatcherUIRegistryItem } from './types'; + +export const fieldMatchersUI = new Registry>(() => { + return [fieldNameMatcherItem]; +}); diff --git a/packages/grafana-ui/src/components/MatchersUI/types.ts b/packages/grafana-ui/src/components/MatchersUI/types.ts new file mode 100644 index 00000000000..22b4d691f33 --- /dev/null +++ b/packages/grafana-ui/src/components/MatchersUI/types.ts @@ -0,0 +1,14 @@ +import React from 'react'; +import { DataFrame, RegistryItem, FieldMatcherInfo } from '@grafana/data'; + +export interface FieldMatcherUIRegistryItem extends RegistryItem { + component: React.ComponentType>; + matcher: FieldMatcherInfo; +} + +export interface MatcherUIProps { + matcher: FieldMatcherInfo; + data: DataFrame[]; + options: T; + onChange: (options: T) => void; +} diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 995fb5d3ce2..4d7c6da2639 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index a14a92dbaad..bcdcbe71842 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -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 { 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 { }); // 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 { } 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 { }); }; + 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 ( +
+ +
+ ); + } + + 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 ; + } + + return
No editor (angular?)
; + } + onDragFinished = () => { document.body.style.cursor = 'auto'; console.log('TODO, save splitter settings'); @@ -154,11 +206,10 @@ export class PanelEditor extends PureComponent { 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 { - {this.props.panel.title} + {panel.title} Discard @@ -194,7 +245,7 @@ export class PanelEditor extends PureComponent {
{ />
- +
-
Viz settings
+
+ {this.renderFieldOptions()} + {this.renderVisSettings()} +
diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 714875a99ff..e473b69a825 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -327,7 +327,7 @@ export class DashboardPage extends PureComponent { {inspectPanel && } {editPanel && ( - + )} diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 32ed6dabb69..2a833067cc6 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -172,7 +172,6 @@ export class PanelChrome extends PureComponent { } onRefresh = () => { - // debugger const { panel, isInView, width } = this.props; if (!isInView) { console.log('Refresh when panel is visible', panel.id); diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index ecc4fb5a33b..95f23fb05d8 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -175,6 +175,7 @@ export class PanelModel { updateOptions(options: object) { this.options = options; + this.render(); } diff --git a/public/app/features/dashboard/state/PanelQueryRunner.ts b/public/app/features/dashboard/state/PanelQueryRunner.ts index ccf39b9bb2e..6e20d650e67 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.ts @@ -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); - } } /** diff --git a/public/app/plugins/panel/table2/custom.tsx b/public/app/plugins/panel/table2/custom.tsx new file mode 100644 index 00000000000..e7f4189c661 --- /dev/null +++ b/public/app/plugins/panel/table2/custom.tsx @@ -0,0 +1,27 @@ +import { FieldPropertyEditorItem, Registry, FieldConfigEditorRegistry } from '@grafana/data'; +import { + NumberValueEditor, + NumberOverrideEditor, + numberOverrideProcessor, + NumberFieldConfigSettings, +} from '@grafana/ui'; + +const columWidth: FieldPropertyEditorItem = { + id: 'width', // Match field properties + name: 'Column Width', + description: 'column width (for table)', + + editor: NumberValueEditor, + override: NumberOverrideEditor, + process: numberOverrideProcessor, + + settings: { + placeholder: 'auto', + min: 20, + max: 300, + }, +}; + +export const tableFieldRegistry: FieldConfigEditorRegistry = new Registry(() => { + return [columWidth]; +}); diff --git a/public/app/plugins/panel/table2/module.tsx b/public/app/plugins/panel/table2/module.tsx index 5373cb486d4..9a955e6c47a 100644 --- a/public/app/plugins/panel/table2/module.tsx +++ b/public/app/plugins/panel/table2/module.tsx @@ -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(TablePanel).setDefaults(defaults).setEditor(TablePanelEditor); +export const plugin = new PanelPlugin(TablePanel) + .setDefaults(defaults) + .setCustomFieldConfigs(tableFieldRegistry) + .setEditor(TablePanelEditor);