From c6a16e55201fa26f33144df8820d4528ecd93e2d Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:03:36 +0000 Subject: [PATCH] Scenes/VizPanel: Add support for panel repeat options (#81818) --- .../panel-edit/PanelEditor.tsx | 115 ++++++++++++++- .../panel-edit/PanelOptions.tsx | 5 +- .../panel-edit/VizPanelManager.tsx | 13 +- .../scene/PanelRepeaterGridItem.tsx | 2 +- .../transformSaveModelToScene.ts | 5 +- .../PanelEditor/getPanelFrameOptions.tsx | 135 +++++++++--------- .../RepeatRowSelect/RepeatRowSelect.tsx | 39 +++++ 7 files changed, 239 insertions(+), 75 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 3d0655a2d73..c44cace8a18 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.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 { + private _initialRepeatOptions: Pick = {}; 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 { 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, + }); } } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx index 12b07172ddd..c059c325e40 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx @@ -15,11 +15,12 @@ interface Props { } export const PanelOptions = React.memo(({ 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(); diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx index cc2919c4b49..1570468ea27 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx @@ -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; datasource?: DataSourceApi; dsSettings?: DataSourceInstanceSettings; tableView?: VizPanel; + repeat?: string; + repeatDirection?: RepeatDirection; + maxPerRow?: number; } export enum DisplayMode { @@ -70,10 +74,17 @@ export class VizPanelManager extends SceneObjectBase { * live on the VizPanelManager level instead of the VizPanel level */ public static createFor(sourcePanel: VizPanel) { + let repeatOptions: Pick = {}; + 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, }); } diff --git a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx index 328f16058f0..ccaa1573604 100644 --- a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx @@ -26,7 +26,7 @@ interface PanelRepeaterGridItemState extends SceneGridItemStateLike { repeatedPanels?: VizPanel[]; variableName: string; itemHeight?: number; - repeatDirection?: RepeatDirection | string; + repeatDirection?: RepeatDirection; maxPerRow?: number; } diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 47d89943714..b1bf8d691cf 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -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, }); } diff --git a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx index 6b8da9ffa63..eb2fe924106 100644 --- a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx @@ -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: () => , }) ) - ); - // - // .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 ( - // { - // 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 ( + { + const stateUpdate: Partial = { 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> = [ + { label: 'Horizontal', value: 'h' }, + { label: 'Vertical', value: 'v' }, + ]; - // return ( - // 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 ( - // panelManager.setState({ maxPerRow: value.value })} + /> + ); + }, + }) + ) + ); } interface ScenePanelLinksEditorProps { diff --git a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx index 8c7d0de5afd..c0de0c7293d 100644 --- a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx +++ b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx @@ -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 ; +};