Scenes/VizPanel: Add support for panel repeat options (#81818)

This commit is contained in:
kay delaney
2024-02-26 17:03:36 +00:00
committed by GitHub
parent e295c38a6e
commit c6a16e5520
7 changed files with 239 additions and 75 deletions

View File

@@ -2,14 +2,15 @@ import * as H from 'history';
import { NavIndex } from '@grafana/data'; import { NavIndex } from '@grafana/data';
import { config, locationService } from '@grafana/runtime'; import { config, locationService } from '@grafana/runtime';
import { SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; import { SceneGridItem, SceneGridLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
import { getPanelIdForVizPanel, getDashboardSceneFor } from '../utils/utils';
import { PanelDataPane } from './PanelDataPane/PanelDataPane'; import { PanelDataPane } from './PanelDataPane/PanelDataPane';
import { PanelEditorRenderer } from './PanelEditorRenderer'; import { PanelEditorRenderer } from './PanelEditorRenderer';
import { PanelOptionsPane } from './PanelOptionsPane'; import { PanelOptionsPane } from './PanelOptionsPane';
import { VizPanelManager } from './VizPanelManager'; import { VizPanelManager, VizPanelManagerState } from './VizPanelManager';
export interface PanelEditorState extends SceneObjectState { export interface PanelEditorState extends SceneObjectState {
isDirty?: boolean; isDirty?: boolean;
@@ -20,12 +21,21 @@ export interface PanelEditorState extends SceneObjectState {
} }
export class PanelEditor extends SceneObjectBase<PanelEditorState> { export class PanelEditor extends SceneObjectBase<PanelEditorState> {
private _initialRepeatOptions: Pick<VizPanelManagerState, 'repeat' | 'repeatDirection' | 'maxPerRow'> = {};
static Component = PanelEditorRenderer; static Component = PanelEditorRenderer;
private _discardChanges = false; private _discardChanges = false;
public constructor(state: PanelEditorState) { public constructor(state: PanelEditorState) {
super(state); super(state);
const { repeat, repeatDirection, maxPerRow } = state.vizManager.state;
this._initialRepeatOptions = {
repeat,
repeatDirection,
maxPerRow,
};
this.addActivationHandler(this._activationHandler.bind(this)); this.addActivationHandler(this._activationHandler.bind(this));
} }
@@ -88,7 +98,104 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
dashboard.onEnterEditMode(); dashboard.onEnterEditMode();
} }
this.state.vizManager.commitChanges(); const panelManager = this.state.vizManager;
const sourcePanel = panelManager.state.sourcePanel.resolve();
const sourcePanelParent = sourcePanel!.parent;
const normalToRepeat = !this._initialRepeatOptions.repeat && panelManager.state.repeat;
const repeatToNormal = this._initialRepeatOptions.repeat && !panelManager.state.repeat;
if (sourcePanelParent instanceof SceneGridItem) {
if (normalToRepeat) {
this.replaceSceneGridItemWithPanelRepeater(sourcePanelParent);
} else {
panelManager.commitChanges();
}
} else if (sourcePanelParent instanceof PanelRepeaterGridItem) {
if (repeatToNormal) {
this.replacePanelRepeaterWithGridItem(sourcePanelParent);
} else {
this.handleRepeatOptionChanges(sourcePanelParent);
}
} else {
console.error('Unsupported scene object type');
}
}
private replaceSceneGridItemWithPanelRepeater(gridItem: SceneGridItem) {
const gridLayout = gridItem.parent;
if (!(gridLayout instanceof SceneGridLayout)) {
console.error('Expected grandparent to be SceneGridLayout!');
return;
}
const panelManager = this.state.vizManager;
const repeatDirection = panelManager.state.repeatDirection ?? 'h';
const repeater = new PanelRepeaterGridItem({
key: gridItem.state.key,
x: gridItem.state.x,
y: gridItem.state.y,
width: repeatDirection === 'h' ? 24 : gridItem.state.width,
height: gridItem.state.height,
itemHeight: gridItem.state.height,
source: panelManager.state.panel.clone(),
variableName: panelManager.state.repeat!,
repeatedPanels: [],
repeatDirection: panelManager.state.repeatDirection,
maxPerRow: panelManager.state.maxPerRow,
});
gridLayout.setState({
children: gridLayout.state.children.map((child) => (child.state.key === gridItem.state.key ? repeater : child)),
});
}
private replacePanelRepeaterWithGridItem(panelRepeater: PanelRepeaterGridItem) {
const gridLayout = panelRepeater.parent;
if (!(gridLayout instanceof SceneGridLayout)) {
console.error('Expected grandparent to be SceneGridLayout!');
return;
}
const panelManager = this.state.vizManager;
const panelClone = panelManager.state.panel.clone();
const gridItem = new SceneGridItem({
key: panelRepeater.state.key,
x: panelRepeater.state.x,
y: panelRepeater.state.y,
width: this._initialRepeatOptions.repeatDirection === 'h' ? 8 : panelRepeater.state.width,
height: this._initialRepeatOptions.repeatDirection === 'v' ? 8 : panelRepeater.state.height,
body: panelClone,
});
gridLayout.setState({
children: gridLayout.state.children.map((child) =>
child.state.key === panelRepeater.state.key ? gridItem : child
),
});
}
private handleRepeatOptionChanges(panelRepeater: PanelRepeaterGridItem) {
let width = panelRepeater.state.width ?? 1;
let height = panelRepeater.state.height;
const panelManager = this.state.vizManager;
const horizontalToVertical =
this._initialRepeatOptions.repeatDirection === 'h' && panelManager.state.repeatDirection === 'v';
const verticalToHorizontal =
this._initialRepeatOptions.repeatDirection === 'v' && panelManager.state.repeatDirection === 'h';
if (horizontalToVertical) {
width = Math.floor(width / (panelRepeater.state.maxPerRow ?? 1));
} else if (verticalToHorizontal) {
width = 24;
}
panelRepeater.setState({
source: panelManager.state.panel.clone(),
repeatDirection: panelManager.state.repeatDirection,
variableName: panelManager.state.repeat,
maxPerRow: panelManager.state.maxPerRow,
width,
height,
});
} }
} }

View File

@@ -15,11 +15,12 @@ interface Props {
} }
export const PanelOptions = React.memo<Props>(({ vizManager, searchQuery, listMode }) => { export const PanelOptions = React.memo<Props>(({ vizManager, searchQuery, listMode }) => {
const { panel } = vizManager.state; const { panel } = vizManager.useState();
const { data } = sceneGraph.getData(panel).useState(); const { data } = sceneGraph.getData(panel).useState();
const { options, fieldConfig } = panel.useState(); const { options, fieldConfig } = panel.useState();
const panelFrameOptions = useMemo(() => getPanelFrameCategory2(panel), [panel]); // eslint-disable-next-line react-hooks/exhaustive-deps
const panelFrameOptions = useMemo(() => getPanelFrameCategory2(vizManager), [vizManager, panel]);
const visualizationOptions = useMemo(() => { const visualizationOptions = useMemo(() => {
const plugin = panel.getPlugin(); const plugin = panel.getPlugin();

View File

@@ -35,16 +35,20 @@ import { updateQueries } from 'app/features/query/state/updateQueries';
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
import { QueryGroupOptions } from 'app/types'; import { QueryGroupOptions } from 'app/types';
import { PanelRepeaterGridItem, RepeatDirection } from '../scene/PanelRepeaterGridItem';
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange'; import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; import { gridItemToPanel } from '../serialization/transformSceneToSaveModel';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
interface VizPanelManagerState extends SceneObjectState { export interface VizPanelManagerState extends SceneObjectState {
panel: VizPanel; panel: VizPanel;
sourcePanel: SceneObjectRef<VizPanel>; sourcePanel: SceneObjectRef<VizPanel>;
datasource?: DataSourceApi; datasource?: DataSourceApi;
dsSettings?: DataSourceInstanceSettings; dsSettings?: DataSourceInstanceSettings;
tableView?: VizPanel; tableView?: VizPanel;
repeat?: string;
repeatDirection?: RepeatDirection;
maxPerRow?: number;
} }
export enum DisplayMode { export enum DisplayMode {
@@ -70,10 +74,17 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
* live on the VizPanelManager level instead of the VizPanel level * live on the VizPanelManager level instead of the VizPanel level
*/ */
public static createFor(sourcePanel: VizPanel) { public static createFor(sourcePanel: VizPanel) {
let repeatOptions: Pick<VizPanelManagerState, 'repeat' | 'repeatDirection' | 'maxPerRow'> = {};
if (sourcePanel.parent instanceof PanelRepeaterGridItem) {
const { variableName: repeat, repeatDirection, maxPerRow } = sourcePanel.parent.state;
repeatOptions = { repeat, repeatDirection, maxPerRow };
}
return new VizPanelManager({ return new VizPanelManager({
panel: sourcePanel.clone({ $data: undefined }), panel: sourcePanel.clone({ $data: undefined }),
$data: sourcePanel.state.$data?.clone(), $data: sourcePanel.state.$data?.clone(),
sourcePanel: sourcePanel.getRef(), sourcePanel: sourcePanel.getRef(),
...repeatOptions,
}); });
} }

View File

@@ -26,7 +26,7 @@ interface PanelRepeaterGridItemState extends SceneGridItemStateLike {
repeatedPanels?: VizPanel[]; repeatedPanels?: VizPanel[];
variableName: string; variableName: string;
itemHeight?: number; itemHeight?: number;
repeatDirection?: RepeatDirection | string; repeatDirection?: RepeatDirection;
maxPerRow?: number; maxPerRow?: number;
} }

View File

@@ -462,7 +462,8 @@ export function buildGridItemForPanel(panel: PanelModel): SceneGridItemLike {
} }
if (panel.repeat) { if (panel.repeat) {
const repeatDirection = panel.repeatDirection ?? 'h'; const repeatDirection = panel.repeatDirection === 'h' ? 'h' : 'v';
return new PanelRepeaterGridItem({ return new PanelRepeaterGridItem({
key: `grid-item-${panel.id}`, key: `grid-item-${panel.id}`,
x: panel.gridPos.x, x: panel.gridPos.x,
@@ -473,7 +474,7 @@ export function buildGridItemForPanel(panel: PanelModel): SceneGridItemLike {
source: new VizPanel(vizPanelState), source: new VizPanel(vizPanelState),
variableName: panel.repeat, variableName: panel.repeat,
repeatedPanels: [], repeatedPanels: [],
repeatDirection: panel.repeatDirection, repeatDirection: repeatDirection,
maxPerRow: panel.maxPerRow, maxPerRow: panel.maxPerRow,
}); });
} }

View File

@@ -1,15 +1,16 @@
import React from 'react'; import React from 'react';
import { SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { VizPanel } from '@grafana/scenes';
import { DataLinksInlineEditor, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui'; import { DataLinksInlineEditor, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui';
import { VizPanelManager, VizPanelManagerState } from 'app/features/dashboard-scene/panel-edit/VizPanelManager';
import { VizPanelLinks } from 'app/features/dashboard-scene/scene/PanelLinks'; import { VizPanelLinks } from 'app/features/dashboard-scene/scene/PanelLinks';
import { dashboardSceneGraph } from 'app/features/dashboard-scene/utils/dashboardSceneGraph'; import { dashboardSceneGraph } from 'app/features/dashboard-scene/utils/dashboardSceneGraph';
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv'; import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
import { GenAIPanelDescriptionButton } from '../GenAI/GenAIPanelDescriptionButton'; import { GenAIPanelDescriptionButton } from '../GenAI/GenAIPanelDescriptionButton';
import { GenAIPanelTitleButton } from '../GenAI/GenAIPanelTitleButton'; import { GenAIPanelTitleButton } from '../GenAI/GenAIPanelTitleButton';
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect'; import { RepeatRowSelect, RepeatRowSelect2 } from '../RepeatRowSelect/RepeatRowSelect';
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor'; import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor'; import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
@@ -175,7 +176,8 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
); );
} }
export function getPanelFrameCategory2(panel: VizPanel): OptionsPaneCategoryDescriptor { export function getPanelFrameCategory2(panelManager: VizPanelManager): OptionsPaneCategoryDescriptor {
const { panel } = panelManager.state;
const descriptor = new OptionsPaneCategoryDescriptor({ const descriptor = new OptionsPaneCategoryDescriptor({
title: 'Panel options', title: 'Panel options',
id: 'Panel options', id: 'Panel options',
@@ -252,69 +254,72 @@ export function getPanelFrameCategory2(panel: VizPanel): OptionsPaneCategoryDesc
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject} />, render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject} />,
}) })
) )
); )
// .addCategory(
// .addCategory( new OptionsPaneCategoryDescriptor({
// new OptionsPaneCategoryDescriptor({ title: 'Repeat options',
// title: 'Repeat options', id: 'Repeat options',
// id: 'Repeat options', isOpenDefault: false,
// isOpenDefault: false, })
// }) .addItem(
// .addItem( new OptionsPaneItemDescriptor({
// new OptionsPaneItemDescriptor({ title: 'Repeat by variable',
// title: 'Repeat by variable', description:
// description: 'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.',
// 'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.', render: function renderRepeatOptions() {
// render: function renderRepeatOptions() { return (
// return ( <RepeatRowSelect2
// <RepeatRowSelect id="repeat-by-variable-select"
// id="repeat-by-variable-select" panelManager={panelManager}
// repeat={panel.repeat} onChange={(value?: string) => {
// onChange={(value?: string) => { const stateUpdate: Partial<VizPanelManagerState> = { repeat: value };
// onPanelConfigChange('repeat', value); if (value && !panelManager.state.repeatDirection) {
// }} stateUpdate.repeatDirection = 'h';
// /> }
// ); panelManager.setState(stateUpdate);
// }, }}
// }) />
// ) );
// .addItem( },
// new OptionsPaneItemDescriptor({ })
// title: 'Repeat direction', )
// showIf: () => !!panel.repeat, .addItem(
// render: function renderRepeatOptions() { new OptionsPaneItemDescriptor({
// const directionOptions = [ title: 'Repeat direction',
// { label: 'Horizontal', value: 'h' }, showIf: () => !!panelManager.state.repeat,
// { label: 'Vertical', value: 'v' }, render: function renderRepeatOptions() {
// ]; const directionOptions: Array<SelectableValue<'h' | 'v'>> = [
{ label: 'Horizontal', value: 'h' },
{ label: 'Vertical', value: 'v' },
];
// return ( return (
// <RadioButtonGroup <RadioButtonGroup
// options={directionOptions} options={directionOptions}
// value={panel.repeatDirection || 'h'} value={panelManager.state.repeatDirection ?? 'h'}
// onChange={(value) => onPanelConfigChange('repeatDirection', value)} onChange={(value) => panelManager.setState({ repeatDirection: value })}
// /> />
// ); );
// }, },
// }) })
// ) )
// .addItem( .addItem(
// new OptionsPaneItemDescriptor({ new OptionsPaneItemDescriptor({
// title: 'Max per row', title: 'Max per row',
// showIf: () => Boolean(panel.repeat && panel.repeatDirection === 'h'), showIf: () => Boolean(panelManager.state.repeat && panelManager.state.repeatDirection === 'h'),
// render: function renderOption() { render: function renderOption() {
// const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value })); const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
// return ( return (
// <Select <Select
// options={maxPerRowOptions} options={maxPerRowOptions}
// value={panel.maxPerRow} value={panelManager.state.maxPerRow}
// onChange={(value) => onPanelConfigChange('maxPerRow', value.value)} onChange={(value) => panelManager.setState({ maxPerRow: value.value })}
// /> />
// ); );
// }, },
// }) })
// ) )
// ); );
} }
interface ScenePanelLinksEditorProps { interface ScenePanelLinksEditorProps {

View File

@@ -1,7 +1,9 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { sceneGraph } from '@grafana/scenes';
import { Select } from '@grafana/ui'; import { Select } from '@grafana/ui';
import { VizPanelManager } from 'app/features/dashboard-scene/panel-edit/VizPanelManager';
import { useSelector } from 'app/types'; import { useSelector } from 'app/types';
import { getLastKey, getVariablesByKey } from '../../../variables/state/selectors'; import { getLastKey, getVariablesByKey } from '../../../variables/state/selectors';
@@ -41,3 +43,40 @@ export const RepeatRowSelect = ({ repeat, onChange, id }: Props) => {
return <Select inputId={id} value={repeat} onChange={onSelectChange} options={variableOptions} />; return <Select inputId={id} value={repeat} onChange={onSelectChange} options={variableOptions} />;
}; };
interface Props2 {
panelManager: VizPanelManager;
id?: string;
onChange: (name?: string) => void;
}
export const RepeatRowSelect2 = ({ panelManager, id, onChange }: Props2) => {
const { panel, repeat } = panelManager.useState();
const sceneVars = useMemo(() => sceneGraph.getVariables(panel), [panel]);
const variables = sceneVars.useState().variables;
const variableOptions = useMemo(() => {
const options: Array<SelectableValue<string | null>> = variables.map((item) => ({
label: item.state.name,
value: item.state.name,
}));
if (options.length === 0) {
options.unshift({
label: 'No template variables found',
value: null,
});
}
options.unshift({
label: 'Disable repeating',
value: null,
});
return options;
}, [variables]);
const onSelectChange = useCallback((option: SelectableValue<string | null>) => onChange(option.value!), [onChange]);
return <Select inputId={id} value={repeat} onChange={onSelectChange} options={variableOptions} />;
};