mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
0b89bdd47d
commit
962745ff21
@ -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>>;
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>({
|
||||
|
@ -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}
|
||||
|
@ -96,6 +96,7 @@ class OptionsPaneOptionsTestScenario {
|
||||
onFieldConfigsChange={this.onFieldConfigsChange}
|
||||
onPanelConfigChange={this.onPanelConfigChange}
|
||||
onPanelOptionsChanged={this.onPanelOptionsChanged}
|
||||
instanceState={undefined}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
|
@ -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[] = [];
|
||||
|
@ -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}
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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));
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
24
public/app/plugins/panel/debug/StateView.tsx
Normal file
24
public/app/plugins/panel/debug/StateView.tsx
Normal 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>;
|
||||
}
|
@ -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',
|
||||
|
@ -12,6 +12,7 @@ export enum DebugMode {
|
||||
Render = 'render',
|
||||
Events = 'events',
|
||||
Cursor = 'cursor',
|
||||
State = 'State',
|
||||
}
|
||||
|
||||
export interface DebugPanelOptions {
|
||||
|
@ -79,6 +79,7 @@ export interface PanelState {
|
||||
pluginId: string;
|
||||
plugin?: PanelPlugin;
|
||||
angularComponent?: AngularComponent | null;
|
||||
instanceState?: any | null;
|
||||
}
|
||||
|
||||
export interface DashboardState {
|
||||
|
Loading…
Reference in New Issue
Block a user