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

ivana/vertical-panel-wip
kay delaney 1 year ago committed by GitHub
parent e295c38a6e
commit c6a16e5520
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 115
      public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx
  2. 5
      public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx
  3. 13
      public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx
  4. 2
      public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx
  5. 5
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
  6. 135
      public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx
  7. 39
      public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx

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

Loading…
Cancel
Save