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
This commit is contained in:
Torkel Ödegaard 2021-10-01 11:08:11 +02:00 committed by GitHub
parent 0b89bdd47d
commit 962745ff21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 104 additions and 47 deletions

View File

@ -4,20 +4,21 @@ import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
import { DataFrame, InterpolateFunction, VariableSuggestionsScope, VariableSuggestion } from '../types';
import { EventBus } from '../events';
export interface StandardEditorContext<TOptions> {
export interface StandardEditorContext<TOptions, TState = any> {
data: DataFrame[]; // All results
replaceVariables?: InterpolateFunction;
eventBus?: EventBus;
getSuggestions?: (scope?: VariableSuggestionsScope) => VariableSuggestion[];
options?: TOptions;
instanceState?: TState;
isOverride?: boolean;
}
export interface StandardEditorProps<TValue = any, TSettings = any, TOptions = any> {
export interface StandardEditorProps<TValue = any, TSettings = any, TOptions = any, TState = any> {
value: TValue;
onChange: (value?: TValue) => void;
item: StandardEditorsRegistryItem<TValue, TSettings>;
context: StandardEditorContext<TOptions>;
context: StandardEditorContext<TOptions, TState>;
}
export interface StandardEditorsRegistryItem<TValue = any, TSettings = any> extends RegistryItem {
editor: ComponentType<StandardEditorProps<TValue, TSettings>>;

View File

@ -57,7 +57,7 @@ export interface FieldConfigSource<TOptions extends object = any> {
overrides: ConfigOverrideRule[];
}
export interface FieldOverrideContext extends StandardEditorContext<any> {
export interface FieldOverrideContext extends StandardEditorContext<any, any> {
field?: Field;
dataFrameIndex?: number; // The index for the selected field frame
}

View File

@ -59,7 +59,7 @@ export interface PanelData {
timeRange: TimeRange;
}
export interface PanelProps<T = any> {
export interface PanelProps<T = any, S = any> {
/** ID of the panel within the current dashboard */
id: number;

View File

@ -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<PanelContext>({

View File

@ -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<Props> = ({
export const OptionsPane: React.FC<OptionPaneRenderProps> = ({
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<Props> = ({
panel={panel}
dashboard={dashboard}
plugin={plugin}
instanceState={instanceState}
data={data}
onFieldConfigsChange={onFieldConfigsChange}
onPanelOptionsChanged={onPanelOptionsChanged}

View File

@ -96,6 +96,7 @@ class OptionsPaneOptionsTestScenario {
onFieldConfigsChange={this.onFieldConfigsChange}
onPanelConfigChange={this.onPanelConfigChange}
onPanelOptionsChanged={this.onPanelOptionsChanged}
instanceState={undefined}
/>
</Provider>
);

View File

@ -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> = (props) => {
export const OptionsPaneOptions: React.FC<OptionPaneRenderProps> = (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> = (props) => {
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[panel.configRev, props.data]
[panel.configRev, props.data, props.instanceState]
);
const mainBoxElements: React.ReactNode[] = [];

View File

@ -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<Props> {
}
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 <div />;
@ -411,7 +407,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
plugin={plugin}
dashboard={dashboard}
panel={panel}
width={rightPaneSize}
instanceState={instanceState}
onFieldConfigsChange={this.onFieldConfigChange}
onPanelOptionsChanged={this.onPanelOptionsChanged}
onPanelConfigChange={this.onPanelConfigChanged}

View File

@ -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<string, OptionsPaneCategoryDescriptor> = {};
const context: StandardEditorContext<any> = {
const context: StandardEditorContext<any, any> = {
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<any>
context: StandardEditorContext<any, any>
) {
for (const pluginOption of optionEditors) {
if (pluginOption.showIf && !pluginOption.showIf(context.options, context.data)) {

View File

@ -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;

View File

@ -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);

View File

@ -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<Props, State> {
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));
};

View File

@ -78,6 +78,9 @@ const dashbardSlice = createSlice({
cleanUpEditPanel: (state) => {
delete state.panels[EDIT_PANEL_ID];
},
setPanelInstanceState: (state, action: PayloadAction<SetPanelInstanceStatePayload>) => {
updatePanelState(state, action.payload.panelId, { instanceState: action.payload.value });
},
setPanelAngularComponent: (state, action: PayloadAction<SetPanelAngularComponentPayload>) => {
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;

View File

@ -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<DebugPanelOptions>;
export class DebugPanel extends Component<Props> {
render() {
const { options } = this.props;
if (options.mode === DebugMode.Events) {
return <EventBusLoggerPanel eventBus={this.props.eventBus} />;
}
if (options.mode === DebugMode.Cursor) {
return <CursorView eventBus={this.props.eventBus} />;
}
if (options.mode === DebugMode.State) {
return <StateView {...this.props} />;
}
return <RenderInfoViewer {...this.props} />;
}
}

View File

@ -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<DebugPanelOptions>) {
const context = usePanelContext();
const onChangeName = (e: FormEvent<HTMLInputElement>) => {
context.onInstanceStateChange!({
name: e.currentTarget.value,
});
};
return (
<Field label="State name">
<Input value={context.instanceState?.name ?? ''} onChange={onChangeName} />
</Field>
);
}
export function StateViewEditor({ value, context, onChange, item }: PanelOptionsEditorProps<string>) {
return <div>Current value: {context.instanceState?.name} </div>;
}

View File

@ -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<DebugPanelOptions>(DebugPanel).useFieldConfig().setPanelOptions((builder) => {
@ -13,9 +14,18 @@ export const plugin = new PanelPlugin<DebugPanelOptions>(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',

View File

@ -12,6 +12,7 @@ export enum DebugMode {
Render = 'render',
Events = 'events',
Cursor = 'cursor',
State = 'State',
}
export interface DebugPanelOptions {

View File

@ -79,6 +79,7 @@ export interface PanelState {
pluginId: string;
plugin?: PanelPlugin;
angularComponent?: AngularComponent | null;
instanceState?: any | null;
}
export interface DashboardState {