From 962745ff216917be7115d11e37a642cdea681178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 Oct 2021 11:08:11 +0200 Subject: [PATCH] PanelEditor: Adds panel instance state and a way to share this between panel & option editors (#39637) * PanelEditor: Adds panel instance state and a way to share this between panel & option editors * Refactoring to use PanelContext --- .../standardFieldConfigEditorRegistry.ts | 7 +++--- .../grafana-data/src/types/fieldOverrides.ts | 2 +- packages/grafana-data/src/types/panel.ts | 2 +- .../components/PanelChrome/PanelContext.ts | 6 +++++ .../components/PanelEditor/OptionsPane.tsx | 21 +++++----------- .../PanelEditor/OptionsPaneOptions.test.tsx | 1 + .../PanelEditor/OptionsPaneOptions.tsx | 20 ++++------------ .../components/PanelEditor/PanelEditor.tsx | 12 ++++------ .../PanelEditor/getVizualizationOptions.tsx | 7 +++--- .../dashboard/components/PanelEditor/types.ts | 1 + .../dashboard/dashgrid/DashboardPanel.tsx | 5 +++- .../dashboard/dashgrid/PanelChrome.tsx | 15 ++++++++++++ .../app/features/dashboard/state/reducers.ts | 9 +++++++ public/app/plugins/panel/debug/DebugPanel.tsx | 7 ++++++ public/app/plugins/panel/debug/StateView.tsx | 24 +++++++++++++++++++ public/app/plugins/panel/debug/module.tsx | 10 ++++++++ public/app/plugins/panel/debug/types.ts | 1 + public/app/types/dashboard.ts | 1 + 18 files changed, 104 insertions(+), 47 deletions(-) create mode 100644 public/app/plugins/panel/debug/StateView.tsx diff --git a/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts b/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts index 31a5c2480c6..ca66a5523c1 100644 --- a/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts +++ b/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts @@ -4,20 +4,21 @@ import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; import { DataFrame, InterpolateFunction, VariableSuggestionsScope, VariableSuggestion } from '../types'; import { EventBus } from '../events'; -export interface StandardEditorContext { +export interface StandardEditorContext { data: DataFrame[]; // All results replaceVariables?: InterpolateFunction; eventBus?: EventBus; getSuggestions?: (scope?: VariableSuggestionsScope) => VariableSuggestion[]; options?: TOptions; + instanceState?: TState; isOverride?: boolean; } -export interface StandardEditorProps { +export interface StandardEditorProps { value: TValue; onChange: (value?: TValue) => void; item: StandardEditorsRegistryItem; - context: StandardEditorContext; + context: StandardEditorContext; } export interface StandardEditorsRegistryItem extends RegistryItem { editor: ComponentType>; diff --git a/packages/grafana-data/src/types/fieldOverrides.ts b/packages/grafana-data/src/types/fieldOverrides.ts index 08c16412418..7a162599ab3 100644 --- a/packages/grafana-data/src/types/fieldOverrides.ts +++ b/packages/grafana-data/src/types/fieldOverrides.ts @@ -57,7 +57,7 @@ export interface FieldConfigSource { overrides: ConfigOverrideRule[]; } -export interface FieldOverrideContext extends StandardEditorContext { +export interface FieldOverrideContext extends StandardEditorContext { field?: Field; dataFrameIndex?: number; // The index for the selected field frame } diff --git a/packages/grafana-data/src/types/panel.ts b/packages/grafana-data/src/types/panel.ts index e6fd6e1f729..82f824fc245 100644 --- a/packages/grafana-data/src/types/panel.ts +++ b/packages/grafana-data/src/types/panel.ts @@ -59,7 +59,7 @@ export interface PanelData { timeRange: TimeRange; } -export interface PanelProps { +export interface PanelProps { /** ID of the panel within the current dashboard */ id: number; diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts b/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts index b444a9e2955..4bb689f6782 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts +++ b/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts @@ -48,6 +48,12 @@ export interface PanelContext { * For example TimeSeries panel. */ onSplitOpen?: SplitOpen; + + /** For instance state that can be shared between panel & options UI */ + instanceState?: any; + + /** Update instance state, this is only supported in dashboard panel context currently */ + onInstanceStateChange?: (state: any) => void; } export const PanelContextRoot = React.createContext({ diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx index 5230e6c0708..16c57345bbb 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { FieldConfigSource, GrafanaTheme, PanelPlugin } from '@grafana/data'; -import { DashboardModel, PanelModel } from '../../state'; +import { GrafanaTheme } from '@grafana/data'; import { useStyles } from '@grafana/ui'; import { css } from '@emotion/css'; import { selectors } from '@grafana/e2e-selectors'; @@ -10,26 +9,17 @@ import { useSelector } from 'react-redux'; import { StoreState } from 'app/types'; import { VisualizationSelectPane } from './VisualizationSelectPane'; import { usePanelLatestData } from './usePanelLatestData'; +import { OptionPaneRenderProps } from './types'; -interface Props { - plugin: PanelPlugin; - panel: PanelModel; - width: number; - dashboard: DashboardModel; - onFieldConfigsChange: (config: FieldConfigSource) => void; - onPanelOptionsChanged: (options: any) => void; - onPanelConfigChange: (configKey: keyof PanelModel, value: any) => void; -} - -export const OptionsPane: React.FC = ({ +export const OptionsPane: React.FC = ({ plugin, panel, - width, onFieldConfigsChange, onPanelOptionsChanged, onPanelConfigChange, dashboard, -}: Props) => { + instanceState, +}) => { const styles = useStyles(getStyles); const isVizPickerOpen = useSelector((state: StoreState) => state.panelEditor.isVizPickerOpen); const { data } = usePanelLatestData(panel, { withTransforms: true, withFieldConfig: false }, true); @@ -46,6 +36,7 @@ export const OptionsPane: React.FC = ({ panel={panel} dashboard={dashboard} plugin={plugin} + instanceState={instanceState} data={data} onFieldConfigsChange={onFieldConfigsChange} onPanelOptionsChanged={onPanelOptionsChanged} diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx index 710659c8aa7..67a5e4a2190 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx @@ -96,6 +96,7 @@ class OptionsPaneOptionsTestScenario { onFieldConfigsChange={this.onFieldConfigsChange} onPanelConfigChange={this.onPanelConfigChange} onPanelOptionsChanged={this.onPanelOptionsChanged} + instanceState={undefined} /> ); diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx index 28f45d38ddc..6efd8451e70 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.tsx @@ -1,7 +1,6 @@ import React, { useMemo, useState } from 'react'; -import { FieldConfigSource, GrafanaTheme2, PanelData, PanelPlugin, SelectableValue } from '@grafana/data'; -import { DashboardModel, PanelModel } from '../../state'; -import { CustomScrollbar, RadioButtonGroup, useStyles2, FilterInput } from '@grafana/ui'; +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { CustomScrollbar, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { getPanelFrameCategory } from './getPanelFrameOptions'; import { getVizualizationOptions } from './getVizualizationOptions'; import { css } from '@emotion/css'; @@ -13,18 +12,9 @@ import { AngularPanelOptions } from './AngularPanelOptions'; import { getRecentOptions } from './state/getRecentOptions'; import { isPanelModelLibraryPanel } from '../../../library-panels/guard'; import { getLibraryPanelOptionsCategory } from './getLibraryPanelOptions'; +import { OptionPaneRenderProps } from './types'; -interface Props { - plugin: PanelPlugin; - panel: PanelModel; - dashboard: DashboardModel; - data?: PanelData; - onFieldConfigsChange: (config: FieldConfigSource) => void; - onPanelOptionsChanged: (options: any) => void; - onPanelConfigChange: (configKey: keyof PanelModel, value: any) => void; -} - -export const OptionsPaneOptions: React.FC = (props) => { +export const OptionsPaneOptions: React.FC = (props) => { const { plugin, dashboard, panel } = props; const [searchQuery, setSearchQuery] = useState(''); const [listMode, setListMode] = useState(OptionFilter.All); @@ -39,7 +29,7 @@ export const OptionsPaneOptions: React.FC = (props) => { ], // eslint-disable-next-line react-hooks/exhaustive-deps - [panel.configRev, props.data] + [panel.configRev, props.data, props.instanceState] ); const mainBoxElements: React.ReactNode[] = []; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 036d910d34d..85d3d2dcba5 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -64,11 +64,12 @@ interface OwnProps { const mapStateToProps = (state: StoreState) => { const panel = state.panelEditor.getPanel(); - const { plugin } = getPanelStateById(state.dashboard, panel.id); + const { plugin, instanceState } = getPanelStateById(state.dashboard, panel.id); return { plugin: plugin, panel, + instanceState, initDone: state.panelEditor.initDone, uiState: state.panelEditor.ui, tableViewEnabled: state.panelEditor.tableViewEnabled, @@ -395,12 +396,7 @@ export class PanelEditorUnconnected extends PureComponent { } renderOptionsPane() { - const { plugin, dashboard, panel, uiState } = this.props; - - const rightPaneSize = - uiState.rightPaneSize <= 1 - ? (uiState.rightPaneSize as number) * window.innerWidth - : (uiState.rightPaneSize as number); + const { plugin, dashboard, panel, instanceState } = this.props; if (!plugin) { return
; @@ -411,7 +407,7 @@ export class PanelEditorUnconnected extends PureComponent { plugin={plugin} dashboard={dashboard} panel={panel} - width={rightPaneSize} + instanceState={instanceState} onFieldConfigsChange={this.onFieldConfigChange} onPanelOptionsChanged={this.onPanelOptionsChanged} onPanelConfigChange={this.onPanelConfigChanged} diff --git a/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx index 66030566bb7..fa5dc566546 100644 --- a/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx @@ -10,12 +10,12 @@ import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor'; type categoryGetter = (categoryNames?: string[]) => OptionsPaneCategoryDescriptor; export function getVizualizationOptions(props: OptionPaneRenderProps): OptionsPaneCategoryDescriptor[] { - const { plugin, panel, onPanelOptionsChanged, onFieldConfigsChange, data, dashboard } = props; + const { plugin, panel, onPanelOptionsChanged, onFieldConfigsChange, data, dashboard, instanceState } = props; const currentOptions = panel.getOptions(); const currentFieldConfig = panel.fieldConfig; const categoryIndex: Record = {}; - const context: StandardEditorContext = { + const context: StandardEditorContext = { data: data?.series || [], replaceVariables: panel.replaceVariables, options: currentOptions, @@ -23,6 +23,7 @@ export function getVizualizationOptions(props: OptionPaneRenderProps): OptionsPa getSuggestions: (scope?: VariableSuggestionsScope) => { return data ? getDataLinksVariableSuggestions(data.series, scope) : []; }, + instanceState, }; const getOptionsPaneCategory = (categoryNames?: string[]): OptionsPaneCategoryDescriptor => { @@ -109,7 +110,7 @@ export function fillOptionsPaneItems( optionEditors: PanelOptionsEditorItem[], getOptionsPaneCategory: categoryGetter, onValueChanged: (path: string, value: any) => void, - context: StandardEditorContext + context: StandardEditorContext ) { for (const pluginOption of optionEditors) { if (pluginOption.showIf && !pluginOption.showIf(context.options, context.data)) { diff --git a/public/app/features/dashboard/components/PanelEditor/types.ts b/public/app/features/dashboard/components/PanelEditor/types.ts index 477143e3389..7420de836dc 100644 --- a/public/app/features/dashboard/components/PanelEditor/types.ts +++ b/public/app/features/dashboard/components/PanelEditor/types.ts @@ -54,6 +54,7 @@ export interface OptionPaneRenderProps { plugin: PanelPlugin; data?: PanelData; dashboard: DashboardModel; + instanceState: any; onPanelConfigChange: (configKey: keyof PanelModel, value: any) => void; onPanelOptionsChanged: (options: any) => void; onFieldConfigsChange: (config: FieldConfigSource) => void; diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 9cda9be168a..2a3c3140be4 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -36,10 +36,13 @@ const mapStateToProps = (state: StoreState, props: OwnProps) => { return { plugin: panelState.plugin, + instanceState: panelState.instanceState, }; }; -const mapDispatchToProps = { initDashboardPanel }; +const mapDispatchToProps = { + initDashboardPanel, +}; const connector = connect(mapStateToProps, mapDispatchToProps); diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 64cdf22800f..54709a3d3f9 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -36,6 +36,8 @@ import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../annota import { getDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner'; import { liveTimer } from './liveTimer'; import { isSoloRoute } from '../../../routes/utils'; +import { setPanelInstanceState } from '../state/reducers'; +import { store } from 'app/store/store'; const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; @@ -84,11 +86,24 @@ export class PanelChrome extends Component { onAnnotationUpdate: this.onAnnotationUpdate, onAnnotationDelete: this.onAnnotationDelete, canAddAnnotations: () => Boolean(props.dashboard.meta.canEdit || props.dashboard.meta.canMakeEditable), + onInstanceStateChange: this.onInstanceStateChange, }, data: this.getInitialPanelDataState(), }; } + onInstanceStateChange = (value: any) => { + this.setState({ + context: { + ...this.state.context, + instanceState: value, + }, + }); + + // Set redux panel state so panel options can get notified + store.dispatch(setPanelInstanceState({ panelId: this.props.panel.id, value })); + }; + onSeriesColorChange = (label: string, color: string) => { this.onFieldConfigChange(changeSeriesColorConfigFactory(label, color, this.props.panel.fieldConfig)); }; diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index 50ae8de9bc8..13482b58eba 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -78,6 +78,9 @@ const dashbardSlice = createSlice({ cleanUpEditPanel: (state) => { delete state.panels[EDIT_PANEL_ID]; }, + setPanelInstanceState: (state, action: PayloadAction) => { + updatePanelState(state, action.payload.panelId, { instanceState: action.payload.value }); + }, setPanelAngularComponent: (state, action: PayloadAction) => { updatePanelState(state, action.payload.panelId, { angularComponent: action.payload.angularComponent }); }, @@ -105,6 +108,11 @@ export interface SetPanelAngularComponentPayload { angularComponent: AngularComponent | null; } +export interface SetPanelInstanceStatePayload { + panelId: number; + value: any; +} + export const { loadDashboardPermissions, dashboardInitFetching, @@ -119,6 +127,7 @@ export const { addPanel, cleanUpEditPanel, setPanelAngularComponent, + setPanelInstanceState, } = dashbardSlice.actions; export const dashboardReducer = dashbardSlice.reducer; diff --git a/public/app/plugins/panel/debug/DebugPanel.tsx b/public/app/plugins/panel/debug/DebugPanel.tsx index b0afe334c1c..aabd71c4f00 100644 --- a/public/app/plugins/panel/debug/DebugPanel.tsx +++ b/public/app/plugins/panel/debug/DebugPanel.tsx @@ -5,19 +5,26 @@ import { DebugPanelOptions, DebugMode } from './types'; import { EventBusLoggerPanel } from './EventBusLogger'; import { RenderInfoViewer } from './RenderInfoViewer'; import { CursorView } from './CursorView'; +import { StateView } from './StateView'; type Props = PanelProps; export class DebugPanel extends Component { render() { const { options } = this.props; + if (options.mode === DebugMode.Events) { return ; } + if (options.mode === DebugMode.Cursor) { return ; } + if (options.mode === DebugMode.State) { + return ; + } + return ; } } diff --git a/public/app/plugins/panel/debug/StateView.tsx b/public/app/plugins/panel/debug/StateView.tsx new file mode 100644 index 00000000000..bc199be3b10 --- /dev/null +++ b/public/app/plugins/panel/debug/StateView.tsx @@ -0,0 +1,24 @@ +import React, { FormEvent } from 'react'; +import { PanelOptionsEditorProps, PanelProps } from '@grafana/data'; +import { Field, Input, usePanelContext } from '@grafana/ui'; +import { DebugPanelOptions } from './types'; + +export function StateView(props: PanelProps) { + const context = usePanelContext(); + + const onChangeName = (e: FormEvent) => { + context.onInstanceStateChange!({ + name: e.currentTarget.value, + }); + }; + + return ( + + + + ); +} + +export function StateViewEditor({ value, context, onChange, item }: PanelOptionsEditorProps) { + return
Current value: {context.instanceState?.name}
; +} diff --git a/public/app/plugins/panel/debug/module.tsx b/public/app/plugins/panel/debug/module.tsx index 0dcc3a9832e..a5169af95fe 100644 --- a/public/app/plugins/panel/debug/module.tsx +++ b/public/app/plugins/panel/debug/module.tsx @@ -1,5 +1,6 @@ import { PanelPlugin } from '@grafana/data'; import { DebugPanel } from './DebugPanel'; +import { StateViewEditor } from './StateView'; import { DebugMode, DebugPanelOptions } from './types'; export const plugin = new PanelPlugin(DebugPanel).useFieldConfig().setPanelOptions((builder) => { @@ -13,9 +14,18 @@ export const plugin = new PanelPlugin(DebugPanel).useFieldCon { label: 'Render', value: DebugMode.Render }, { label: 'Events', value: DebugMode.Events }, { label: 'Cursor', value: DebugMode.Cursor }, + { label: 'Share state', value: DebugMode.State }, ], }, }) + .addCustomEditor({ + id: 'stateView', + path: 'stateView', + name: 'State view', + defaultValue: '', + showIf: ({ mode }) => mode === DebugMode.State, + editor: StateViewEditor, + }) .addBooleanSwitch({ path: 'counters.render', name: 'Render Count', diff --git a/public/app/plugins/panel/debug/types.ts b/public/app/plugins/panel/debug/types.ts index 2fa3b54c74b..0db4d56f237 100644 --- a/public/app/plugins/panel/debug/types.ts +++ b/public/app/plugins/panel/debug/types.ts @@ -12,6 +12,7 @@ export enum DebugMode { Render = 'render', Events = 'events', Cursor = 'cursor', + State = 'State', } export interface DebugPanelOptions { diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index d99bde26ff6..f1f9304a265 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -79,6 +79,7 @@ export interface PanelState { pluginId: string; plugin?: PanelPlugin; angularComponent?: AngularComponent | null; + instanceState?: any | null; } export interface DashboardState {