mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scenes/VizPanel: Add support for panel repeat options (#81818)
This commit is contained in:
@@ -2,14 +2,15 @@ import * as H from 'history';
|
||||
|
||||
import { NavIndex } from '@grafana/data';
|
||||
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 { PanelEditorRenderer } from './PanelEditorRenderer';
|
||||
import { PanelOptionsPane } from './PanelOptionsPane';
|
||||
import { VizPanelManager } from './VizPanelManager';
|
||||
import { VizPanelManager, VizPanelManagerState } from './VizPanelManager';
|
||||
|
||||
export interface PanelEditorState extends SceneObjectState {
|
||||
isDirty?: boolean;
|
||||
@@ -20,12 +21,21 @@ export interface PanelEditorState extends SceneObjectState {
|
||||
}
|
||||
|
||||
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
private _initialRepeatOptions: Pick<VizPanelManagerState, 'repeat' | 'repeatDirection' | 'maxPerRow'> = {};
|
||||
static Component = PanelEditorRenderer;
|
||||
|
||||
private _discardChanges = false;
|
||||
|
||||
public constructor(state: PanelEditorState) {
|
||||
super(state);
|
||||
|
||||
const { repeat, repeatDirection, maxPerRow } = state.vizManager.state;
|
||||
this._initialRepeatOptions = {
|
||||
repeat,
|
||||
repeatDirection,
|
||||
maxPerRow,
|
||||
};
|
||||
|
||||
this.addActivationHandler(this._activationHandler.bind(this));
|
||||
}
|
||||
|
||||
@@ -88,7 +98,104 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,11 +15,12 @@ interface Props {
|
||||
}
|
||||
|
||||
export const PanelOptions = React.memo<Props>(({ vizManager, searchQuery, listMode }) => {
|
||||
const { panel } = vizManager.state;
|
||||
const { panel } = vizManager.useState();
|
||||
const { data } = sceneGraph.getData(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 plugin = panel.getPlugin();
|
||||
|
||||
@@ -35,16 +35,20 @@ import { updateQueries } from 'app/features/query/state/updateQueries';
|
||||
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
|
||||
import { QueryGroupOptions } from 'app/types';
|
||||
|
||||
import { PanelRepeaterGridItem, RepeatDirection } from '../scene/PanelRepeaterGridItem';
|
||||
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
|
||||
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
|
||||
|
||||
interface VizPanelManagerState extends SceneObjectState {
|
||||
export interface VizPanelManagerState extends SceneObjectState {
|
||||
panel: VizPanel;
|
||||
sourcePanel: SceneObjectRef<VizPanel>;
|
||||
datasource?: DataSourceApi;
|
||||
dsSettings?: DataSourceInstanceSettings;
|
||||
tableView?: VizPanel;
|
||||
repeat?: string;
|
||||
repeatDirection?: RepeatDirection;
|
||||
maxPerRow?: number;
|
||||
}
|
||||
|
||||
export enum DisplayMode {
|
||||
@@ -70,10 +74,17 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
* live on the VizPanelManager level instead of the VizPanel level
|
||||
*/
|
||||
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({
|
||||
panel: sourcePanel.clone({ $data: undefined }),
|
||||
$data: sourcePanel.state.$data?.clone(),
|
||||
sourcePanel: sourcePanel.getRef(),
|
||||
...repeatOptions,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ interface PanelRepeaterGridItemState extends SceneGridItemStateLike {
|
||||
repeatedPanels?: VizPanel[];
|
||||
variableName: string;
|
||||
itemHeight?: number;
|
||||
repeatDirection?: RepeatDirection | string;
|
||||
repeatDirection?: RepeatDirection;
|
||||
maxPerRow?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -462,7 +462,8 @@ export function buildGridItemForPanel(panel: PanelModel): SceneGridItemLike {
|
||||
}
|
||||
|
||||
if (panel.repeat) {
|
||||
const repeatDirection = panel.repeatDirection ?? 'h';
|
||||
const repeatDirection = panel.repeatDirection === 'h' ? 'h' : 'v';
|
||||
|
||||
return new PanelRepeaterGridItem({
|
||||
key: `grid-item-${panel.id}`,
|
||||
x: panel.gridPos.x,
|
||||
@@ -473,7 +474,7 @@ export function buildGridItemForPanel(panel: PanelModel): SceneGridItemLike {
|
||||
source: new VizPanel(vizPanelState),
|
||||
variableName: panel.repeat,
|
||||
repeatedPanels: [],
|
||||
repeatDirection: panel.repeatDirection,
|
||||
repeatDirection: repeatDirection,
|
||||
maxPerRow: panel.maxPerRow,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
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 { dashboardSceneGraph } from 'app/features/dashboard-scene/utils/dashboardSceneGraph';
|
||||
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||
|
||||
import { GenAIPanelDescriptionButton } from '../GenAI/GenAIPanelDescriptionButton';
|
||||
import { GenAIPanelTitleButton } from '../GenAI/GenAIPanelTitleButton';
|
||||
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect';
|
||||
import { RepeatRowSelect, RepeatRowSelect2 } from '../RepeatRowSelect/RepeatRowSelect';
|
||||
|
||||
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
|
||||
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({
|
||||
title: 'Panel options',
|
||||
id: 'Panel options',
|
||||
@@ -252,69 +254,72 @@ export function getPanelFrameCategory2(panel: VizPanel): OptionsPaneCategoryDesc
|
||||
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject} />,
|
||||
})
|
||||
)
|
||||
);
|
||||
//
|
||||
// .addCategory(
|
||||
// new OptionsPaneCategoryDescriptor({
|
||||
// title: 'Repeat options',
|
||||
// id: 'Repeat options',
|
||||
// isOpenDefault: false,
|
||||
// })
|
||||
// .addItem(
|
||||
// new OptionsPaneItemDescriptor({
|
||||
// title: 'Repeat by variable',
|
||||
// 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.',
|
||||
// render: function renderRepeatOptions() {
|
||||
// return (
|
||||
// <RepeatRowSelect
|
||||
// id="repeat-by-variable-select"
|
||||
// repeat={panel.repeat}
|
||||
// onChange={(value?: string) => {
|
||||
// onPanelConfigChange('repeat', value);
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
// },
|
||||
// })
|
||||
// )
|
||||
// .addItem(
|
||||
// new OptionsPaneItemDescriptor({
|
||||
// title: 'Repeat direction',
|
||||
// showIf: () => !!panel.repeat,
|
||||
// render: function renderRepeatOptions() {
|
||||
// const directionOptions = [
|
||||
// { label: 'Horizontal', value: 'h' },
|
||||
// { label: 'Vertical', value: 'v' },
|
||||
// ];
|
||||
)
|
||||
.addCategory(
|
||||
new OptionsPaneCategoryDescriptor({
|
||||
title: 'Repeat options',
|
||||
id: 'Repeat options',
|
||||
isOpenDefault: false,
|
||||
})
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Repeat by variable',
|
||||
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.',
|
||||
render: function renderRepeatOptions() {
|
||||
return (
|
||||
<RepeatRowSelect2
|
||||
id="repeat-by-variable-select"
|
||||
panelManager={panelManager}
|
||||
onChange={(value?: string) => {
|
||||
const stateUpdate: Partial<VizPanelManagerState> = { repeat: value };
|
||||
if (value && !panelManager.state.repeatDirection) {
|
||||
stateUpdate.repeatDirection = 'h';
|
||||
}
|
||||
panelManager.setState(stateUpdate);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Repeat direction',
|
||||
showIf: () => !!panelManager.state.repeat,
|
||||
render: function renderRepeatOptions() {
|
||||
const directionOptions: Array<SelectableValue<'h' | 'v'>> = [
|
||||
{ label: 'Horizontal', value: 'h' },
|
||||
{ label: 'Vertical', value: 'v' },
|
||||
];
|
||||
|
||||
// return (
|
||||
// <RadioButtonGroup
|
||||
// options={directionOptions}
|
||||
// value={panel.repeatDirection || 'h'}
|
||||
// onChange={(value) => onPanelConfigChange('repeatDirection', value)}
|
||||
// />
|
||||
// );
|
||||
// },
|
||||
// })
|
||||
// )
|
||||
// .addItem(
|
||||
// new OptionsPaneItemDescriptor({
|
||||
// title: 'Max per row',
|
||||
// showIf: () => Boolean(panel.repeat && panel.repeatDirection === 'h'),
|
||||
// render: function renderOption() {
|
||||
// const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
|
||||
// return (
|
||||
// <Select
|
||||
// options={maxPerRowOptions}
|
||||
// value={panel.maxPerRow}
|
||||
// onChange={(value) => onPanelConfigChange('maxPerRow', value.value)}
|
||||
// />
|
||||
// );
|
||||
// },
|
||||
// })
|
||||
// )
|
||||
// );
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
options={directionOptions}
|
||||
value={panelManager.state.repeatDirection ?? 'h'}
|
||||
onChange={(value) => panelManager.setState({ repeatDirection: value })}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Max per row',
|
||||
showIf: () => Boolean(panelManager.state.repeat && panelManager.state.repeatDirection === 'h'),
|
||||
render: function renderOption() {
|
||||
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
|
||||
return (
|
||||
<Select
|
||||
options={maxPerRowOptions}
|
||||
value={panelManager.state.maxPerRow}
|
||||
onChange={(value) => panelManager.setState({ maxPerRow: value.value })}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface ScenePanelLinksEditorProps {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { sceneGraph } from '@grafana/scenes';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { VizPanelManager } from 'app/features/dashboard-scene/panel-edit/VizPanelManager';
|
||||
import { useSelector } from 'app/types';
|
||||
|
||||
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} />;
|
||||
};
|
||||
|
||||
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} />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user