From 9c22a6144e323340cc3ce14acc826953dd113fd0 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:34:40 +0200 Subject: [PATCH] Scenes: Row controls (#83607) * wip row controls * wip row repeats * wip * wip * row repeat functional * refactor * refactor to reuse RepeatRowSelect2 * refactor + tests * remove comment * refactor --- .../panel-edit/PanelOptions.tsx | 7 +- .../scene/DashboardScene.test.tsx | 41 ++++ .../dashboard-scene/scene/DashboardScene.tsx | 19 ++ .../scene/RowRepeaterBehavior.test.tsx | 33 +++ .../scene/RowRepeaterBehavior.ts | 8 +- .../scene/row-actions/RowActions.tsx | 208 ++++++++++++++++++ .../scene/row-actions/RowOptionsButton.tsx | 50 +++++ .../scene/row-actions/RowOptionsForm.test.tsx | 51 +++++ .../scene/row-actions/RowOptionsForm.tsx | 61 +++++ .../scene/row-actions/RowOptionsModal.tsx | 40 ++++ .../transformSaveModelToScene.ts | 2 + .../features/dashboard-scene/utils/utils.ts | 2 + .../PanelEditor/getPanelFrameOptions.tsx | 11 +- .../RepeatRowSelect/RepeatRowSelect.tsx | 11 +- 14 files changed, 531 insertions(+), 13 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx create mode 100644 public/app/features/dashboard-scene/scene/row-actions/RowOptionsButton.tsx create mode 100644 public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.tsx create mode 100644 public/app/features/dashboard-scene/scene/row-actions/RowOptionsModal.tsx diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx index c059c325e40..988a7db4c9d 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx @@ -15,12 +15,15 @@ interface Props { } export const PanelOptions = React.memo(({ vizManager, searchQuery, listMode }) => { - const { panel } = vizManager.useState(); + const { panel, repeat } = vizManager.useState(); const { data } = sceneGraph.getData(panel).useState(); const { options, fieldConfig } = panel.useState(); // eslint-disable-next-line react-hooks/exhaustive-deps - const panelFrameOptions = useMemo(() => getPanelFrameCategory2(vizManager), [vizManager, panel]); + const panelFrameOptions = useMemo( + () => getPanelFrameCategory2(vizManager, panel, repeat), + [vizManager, panel, repeat] + ); const visualizationOptions = useMemo(() => { const plugin = panel.getPlugin(); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 69b1e38091d..7dc9c6339fd 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -338,6 +338,47 @@ describe('DashboardScene', () => { expect(gridRow.state.children.length).toBe(0); }); + it('Should remove a row and move its children to the grid layout', () => { + const body = scene.state.body as SceneGridLayout; + const row = body.state.children[2] as SceneGridRow; + + scene.removeRow(row); + + const vizPanel = (body.state.children[2] as SceneGridItem).state.body as VizPanel; + + expect(body.state.children.length).toBe(6); + expect(vizPanel.state.key).toBe('panel-4'); + }); + + it('Should remove a row and its children', () => { + const body = scene.state.body as SceneGridLayout; + const row = body.state.children[2] as SceneGridRow; + + scene.removeRow(row, true); + + expect(body.state.children.length).toBe(4); + }); + + it('Should remove an empty row from the layout', () => { + const row = new SceneGridRow({ + key: 'panel-1', + }); + + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [row], + }), + }); + + const body = scene.state.body as SceneGridLayout; + + expect(body.state.children.length).toBe(1); + + scene.removeRow(row); + + expect(body.state.children.length).toBe(0); + }); + it('Should fail to copy a panel if it does not have a grid item parent', () => { const vizPanel = new VizPanel({ title: 'Panel Title', diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index be27cb411a5..41bdb767c81 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -412,6 +412,25 @@ export class DashboardScene extends SceneObjectBase { }); } + public removeRow(row: SceneGridRow, removePanels = false) { + if (!(this.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); + } + + const sceneGridLayout = this.state.body; + + const children = sceneGridLayout.state.children.filter((child) => child.state.key !== row.state.key); + + if (!removePanels) { + const rowChildren = row.state.children.map((child) => child.clone()); + const indexOfRow = sceneGridLayout.state.children.findIndex((child) => child.state.key === row.state.key); + + children.splice(indexOfRow, 0, ...rowChildren); + } + + sceneGridLayout.setState({ children }); + } + public addPanel(vizPanel: VizPanel): void { if (!(this.state.body instanceof SceneGridLayout)) { throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx index 204394c372f..e6d18e149b7 100644 --- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx @@ -50,6 +50,19 @@ describe('RowRepeaterBehavior', () => { expect(rowAtTheBottom.state.y).toBe(40); }); + it('Should push row at the bottom down and also offset its children', () => { + const rowAtTheBottom = grid.state.children[6] as SceneGridRow; + const rowChildOne = rowAtTheBottom.state.children[0] as SceneGridItem; + const rowChildTwo = rowAtTheBottom.state.children[1] as SceneGridItem; + + expect(rowAtTheBottom.state.title).toBe('Row at the bottom'); + + // Panel at the top is 10, each row is (1+5)*5 = 30, so the grid item below it should be 40 + expect(rowAtTheBottom.state.y).toBe(40); + expect(rowChildOne.state.y).toBe(41); + expect(rowChildTwo.state.y).toBe(49); + }); + it('Should handle second repeat cycle and update remove old repeats', async () => { // trigger another repeat cycle by changing the variable const variable = scene.state.$variables!.state.variables[0] as TestVariable; @@ -111,6 +124,26 @@ function buildScene(options: SceneOptions) { width: 24, height: 5, title: 'Row at the bottom', + children: [ + new SceneGridItem({ + key: 'griditem-2', + x: 0, + y: 17, + body: new SceneCanvasText({ + key: 'canvas-2', + text: 'Panel inside row, server = $server', + }), + }), + new SceneGridItem({ + key: 'griditem-3', + x: 0, + y: 25, + body: new SceneCanvasText({ + key: 'canvas-3', + text: 'Panel inside row, server = $server', + }), + }), + ], }), ], }); diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts index 75202b3367f..476fc31d637 100644 --- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts +++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts @@ -180,8 +180,12 @@ function updateLayout(layout: SceneGridLayout, rows: SceneGridRow[], maxYOfRows: const diff = maxYOfRows - firstChildAfterY; for (const child of childrenAfter) { - if (child.state.y! < maxYOfRows) { - child.setState({ y: child.state.y! + diff }); + child.setState({ y: child.state.y! + diff }); + + if (child instanceof SceneGridRow) { + for (const rowChild of child.state.children) { + rowChild.setState({ y: rowChild.state.y! + diff }); + } } } } diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx new file mode 100644 index 00000000000..ec9b123be47 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx @@ -0,0 +1,208 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { + SceneComponentProps, + SceneGridItem, + SceneGridLayout, + SceneGridRow, + SceneObjectBase, + SceneObjectState, + SceneQueryRunner, + VizPanel, +} from '@grafana/scenes'; +import { Icon, TextLink, useStyles2 } from '@grafana/ui'; +import appEvents from 'app/core/app_events'; +import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; +import { ShowConfirmModalEvent } from 'app/types/events'; + +import { getDashboardSceneFor } from '../../utils/utils'; +import { DashboardScene } from '../DashboardScene'; +import { RowRepeaterBehavior } from '../RowRepeaterBehavior'; + +import { RowOptionsButton } from './RowOptionsButton'; + +export interface RowActionsState extends SceneObjectState {} + +export class RowActions extends SceneObjectBase { + private updateLayout(rowClone: SceneGridRow): void { + const row = this.getParent(); + + const layout = this.getDashboard().state.body; + + if (!(layout instanceof SceneGridLayout)) { + throw new Error('Layout is not a SceneGridLayout'); + } + + // remove the repeated rows + const children = layout.state.children.filter((child) => !child.state.key?.startsWith(`${row.state.key}-clone-`)); + + // get the index to replace later + const index = children.indexOf(row); + + if (index === -1) { + throw new Error('Parent row not found in layout children'); + } + + // replace the row with the clone + layout.setState({ + children: [...children.slice(0, index), rowClone, ...children.slice(index + 1)], + }); + } + + public getParent(): SceneGridRow { + if (!(this.parent instanceof SceneGridRow)) { + throw new Error('RowActions must have a SceneGridRow parent'); + } + + return this.parent; + } + + public getDashboard(): DashboardScene { + return getDashboardSceneFor(this); + } + + public onUpdate = (title: string, repeat?: string | null): void => { + const row = this.getParent(); + + // return early if there is no repeat + if (!repeat) { + const clone = row.clone(); + + // remove the row repeater behaviour, leave the rest + clone.setState({ + title, + $behaviors: row.state.$behaviors?.filter((b) => !(b instanceof RowRepeaterBehavior)) ?? [], + }); + + this.updateLayout(clone); + + return; + } + + const children = row.state.children.map((child) => child.clone()); + + const newBehaviour = new RowRepeaterBehavior({ + variableName: repeat, + sources: children, + }); + + // get rest of behaviors except the old row repeater, if any, and push new one + const behaviors = row.state.$behaviors?.filter((b) => !(b instanceof RowRepeaterBehavior)) ?? []; + behaviors.push(newBehaviour); + + row.setState({ + title, + $behaviors: behaviors, + }); + + newBehaviour.activate(); + }; + + public onDelete = () => { + appEvents.publish( + new ShowConfirmModalEvent({ + title: 'Delete row', + text: 'Are you sure you want to remove this row and all its panels?', + altActionText: 'Delete row only', + icon: 'trash-alt', + onConfirm: () => { + this.getDashboard().removeRow(this.getParent(), true); + }, + onAltAction: () => { + this.getDashboard().removeRow(this.getParent()); + }, + }) + ); + }; + + public getWarning = () => { + const row = this.getParent(); + const gridItems = row.state.children; + + const isAnyPanelUsingDashboardDS = gridItems.some((gridItem) => { + if (!(gridItem instanceof SceneGridItem)) { + return false; + } + + if (gridItem.state.body instanceof VizPanel && gridItem.state.body.state.$data instanceof SceneQueryRunner) { + return gridItem.state.body.state.$data?.state.datasource?.uid === SHARED_DASHBOARD_QUERY; + } + + return false; + }); + + if (isAnyPanelUsingDashboardDS) { + return ( +
+

+ Panels in this row use the {SHARED_DASHBOARD_QUERY} data source. These panels will reference the panel in + the original row, not the ones in the repeated rows. +

+ + Learn more + +
+ ); + } + + return undefined; + }; + + static Component = ({ model }: SceneComponentProps) => { + const dashboard = model.getDashboard(); + const row = model.getParent(); + const { title } = row.useState(); + const { meta, isEditing } = dashboard.useState(); + const styles = useStyles2(getStyles); + + const behaviour = row.state.$behaviors?.find((b) => b instanceof RowRepeaterBehavior); + + return ( + <> + {meta.canEdit && isEditing && ( + <> +
+ + +
+ + )} + + ); + }; +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + rowActions: css({ + color: theme.colors.text.secondary, + lineHeight: '27px', + + button: { + color: theme.colors.text.secondary, + paddingLeft: theme.spacing(2), + background: 'transparent', + border: 'none', + + '&:hover': { + color: theme.colors.text.maxContrast, + }, + }, + }), + }; +}; diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowOptionsButton.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsButton.tsx new file mode 100644 index 00000000000..ef10894cf36 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsButton.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import { SceneObject } from '@grafana/scenes'; +import { Icon, ModalsController } from '@grafana/ui'; + +import { OnRowOptionsUpdate } from './RowOptionsForm'; +import { RowOptionsModal } from './RowOptionsModal'; + +export interface RowOptionsButtonProps { + title: string; + repeat?: string; + parent: SceneObject; + onUpdate: OnRowOptionsUpdate; + warning?: React.ReactNode; +} + +export const RowOptionsButton = ({ repeat, title, parent, onUpdate, warning }: RowOptionsButtonProps) => { + const onUpdateChange = (hideModal: () => void) => (title: string, repeat?: string | null) => { + onUpdate(title, repeat); + hideModal(); + }; + + return ( + + {({ showModal, hideModal }) => { + return ( + + ); + }} + + ); +}; + +RowOptionsButton.displayName = 'RowOptionsButton'; diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.test.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.test.tsx new file mode 100644 index 00000000000..c6bcf17fb93 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.test.tsx @@ -0,0 +1,51 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { TestProvider } from 'test/helpers/TestProvider'; + +import { selectors } from '@grafana/e2e-selectors'; + +import { DashboardScene } from '../DashboardScene'; + +import { RowOptionsForm } from './RowOptionsForm'; + +jest.mock('app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect', () => ({ + RepeatRowSelect2: () =>
, +})); +describe('DashboardRow', () => { + const scene = new DashboardScene({ + title: 'hello', + uid: 'dash-1', + meta: { + canEdit: true, + }, + }); + + it('Should show warning component when has warningMessage prop', () => { + render( + + + + ); + expect( + screen.getByTestId(selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage) + ).toBeInTheDocument(); + }); + + it('Should not show warning component when does not have warningMessage prop', () => { + render( + + + + ); + expect( + screen.queryByTestId(selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage) + ).not.toBeInTheDocument(); + }); +}); diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.tsx new file mode 100644 index 00000000000..538a35382e7 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.tsx @@ -0,0 +1,61 @@ +import React, { useCallback, useState } from 'react'; +import { useForm } from 'react-hook-form'; + +import { selectors } from '@grafana/e2e-selectors'; +import { SceneObject } from '@grafana/scenes'; +import { Button, Field, Modal, Input, Alert } from '@grafana/ui'; +import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect'; + +export type OnRowOptionsUpdate = (title: string, repeat?: string | null) => void; + +export interface Props { + title: string; + repeat?: string; + parent: SceneObject; + onUpdate: OnRowOptionsUpdate; + onCancel: () => void; + warning?: React.ReactNode; +} + +export const RowOptionsForm = ({ repeat, title, parent, warning, onUpdate, onCancel }: Props) => { + const [newRepeat, setNewRepeat] = useState(repeat); + const onChangeRepeat = useCallback((name?: string) => setNewRepeat(name), [setNewRepeat]); + + const { handleSubmit, register } = useForm({ + defaultValues: { + title, + }, + }); + + const submit = (formData: { title: string }) => { + onUpdate(formData.title, newRepeat); + }; + + return ( +
+ + + + + + + {warning && ( + + {warning} + + )} + + + + +
+ ); +}; diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowOptionsModal.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsModal.tsx new file mode 100644 index 00000000000..2f23db9add7 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsModal.tsx @@ -0,0 +1,40 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { SceneObject } from '@grafana/scenes'; +import { Modal, useStyles2 } from '@grafana/ui'; + +import { OnRowOptionsUpdate, RowOptionsForm } from './RowOptionsForm'; + +export interface RowOptionsModalProps { + title: string; + repeat?: string; + parent: SceneObject; + warning?: React.ReactNode; + onDismiss: () => void; + onUpdate: OnRowOptionsUpdate; +} + +export const RowOptionsModal = ({ repeat, title, parent, onDismiss, onUpdate, warning }: RowOptionsModalProps) => { + const styles = useStyles2(getStyles); + + return ( + + + + ); +}; + +const getStyles = () => ({ + modal: css({ + label: 'RowOptionsModal', + width: '500px', + }), +}); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 4cb91f0c17f..8609ff8f98f 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -47,6 +47,7 @@ import { PanelNotices } from '../scene/PanelNotices'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { RowActions } from '../scene/row-actions/RowActions'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; import { DashboardInteractions } from '../utils/interactions'; @@ -211,6 +212,7 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]): isCollapsed: row.collapsed, children: children, $behaviors: behaviors, + actions: new RowActions({}), }); } diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index d82cf30125b..00d50ee41d7 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -17,6 +17,7 @@ import { DashboardScene } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; +import { RowActions } from '../scene/row-actions/RowActions'; import { dashboardSceneGraph } from './dashboardSceneGraph'; @@ -233,6 +234,7 @@ export function getDefaultRow(dashboard: DashboardScene): SceneGridRow { return new SceneGridRow({ key: getVizPanelKeyForPanelId(id), title: 'Row title', + actions: new RowActions({}), y: 0, }); } diff --git a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx index 6b3adccb4e4..88574094060 100644 --- a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx @@ -2,6 +2,7 @@ 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'; @@ -176,8 +177,11 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane ); } -export function getPanelFrameCategory2(panelManager: VizPanelManager): OptionsPaneCategoryDescriptor { - const { panel } = panelManager.state; +export function getPanelFrameCategory2( + panelManager: VizPanelManager, + panel: VizPanel, + repeat?: string +): OptionsPaneCategoryDescriptor { const descriptor = new OptionsPaneCategoryDescriptor({ title: 'Panel options', id: 'Panel options', @@ -270,7 +274,8 @@ export function getPanelFrameCategory2(panelManager: VizPanelManager): OptionsPa return ( { const stateUpdate: Partial = { repeat: value }; if (value && !panelManager.state.repeatDirection) { diff --git a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx index c0de0c7293d..de9ce1c1977 100644 --- a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx +++ b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx @@ -1,9 +1,8 @@ import React, { useCallback, useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { sceneGraph } from '@grafana/scenes'; +import { SceneObject, 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'; @@ -45,14 +44,14 @@ export const RepeatRowSelect = ({ repeat, onChange, id }: Props) => { }; interface Props2 { - panelManager: VizPanelManager; + parent: SceneObject; + repeat: string | undefined; 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]); +export const RepeatRowSelect2 = ({ parent, repeat, id, onChange }: Props2) => { + const sceneVars = useMemo(() => sceneGraph.getVariables(parent), [parent]); const variables = sceneVars.useState().variables; const variableOptions = useMemo(() => {