From 762457b52b77aa3319c2565d355d84870af7757e Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Thu, 14 Dec 2023 14:33:52 -0600 Subject: [PATCH] WIP: UI dashboard --- .../src/deephaven/ui/components/__init__.py | 8 + .../ui/src/deephaven/ui/components/column.py | 18 +++ .../src/deephaven/ui/components/dashboard.py | 14 ++ plugins/ui/src/deephaven/ui/components/row.py | 18 +++ .../ui/src/deephaven/ui/components/stack.py | 30 ++++ .../ui/src/js/src/DocumentHandler.test.tsx | 2 +- plugins/ui/src/js/src/DocumentUtils.tsx | 22 +-- plugins/ui/src/js/src/PanelUtils.test.ts | 11 -- plugins/ui/src/js/src/PanelUtils.ts | 29 ---- plugins/ui/src/js/src/ReactPanel.test.tsx | 2 +- plugins/ui/src/js/src/ReactPanel.tsx | 13 +- plugins/ui/src/js/src/WidgetUtils.tsx | 24 ++- plugins/ui/src/js/src/layout/Column.tsx | 38 +++++ plugins/ui/src/js/src/layout/Dashboard.tsx | 27 ++++ .../ui/src/js/src/layout/LayoutUtils.test.ts | 56 +++++++ plugins/ui/src/js/src/layout/LayoutUtils.ts | 139 ++++++++++++++++++ .../ui/src/js/src/layout/ParentItemContext.ts | 15 ++ plugins/ui/src/js/src/layout/Row.tsx | 38 +++++ plugins/ui/src/js/src/layout/Stack.tsx | 55 +++++++ 19 files changed, 499 insertions(+), 60 deletions(-) create mode 100644 plugins/ui/src/deephaven/ui/components/column.py create mode 100644 plugins/ui/src/deephaven/ui/components/dashboard.py create mode 100644 plugins/ui/src/deephaven/ui/components/row.py create mode 100644 plugins/ui/src/deephaven/ui/components/stack.py delete mode 100644 plugins/ui/src/js/src/PanelUtils.test.ts delete mode 100644 plugins/ui/src/js/src/PanelUtils.ts create mode 100644 plugins/ui/src/js/src/layout/Column.tsx create mode 100644 plugins/ui/src/js/src/layout/Dashboard.tsx create mode 100644 plugins/ui/src/js/src/layout/LayoutUtils.test.ts create mode 100644 plugins/ui/src/js/src/layout/LayoutUtils.ts create mode 100644 plugins/ui/src/js/src/layout/ParentItemContext.ts create mode 100644 plugins/ui/src/js/src/layout/Row.tsx create mode 100644 plugins/ui/src/js/src/layout/Stack.tsx diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py index e5db0b60a..a58a23a2e 100644 --- a/plugins/ui/src/deephaven/ui/components/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/__init__.py @@ -4,6 +4,10 @@ from .panel import panel from .spectrum import * from .table import table +from .row import row +from .column import column +from .stack import stack +from .dashboard import dashboard from . import html @@ -12,9 +16,11 @@ "button", "button_group", "checkbox", + "column", "component", "content", "contextual_help", + "dashboard", "flex", "form", "fragment", @@ -28,8 +34,10 @@ "item", "panel", "range_slider", + "row", "slider", "spectrum_element", + "stack", "switch", "table", "tab_list", diff --git a/plugins/ui/src/deephaven/ui/components/column.py b/plugins/ui/src/deephaven/ui/components/column.py new file mode 100644 index 000000000..d976523c2 --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/column.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from typing import Any +from ..elements import BaseElement + + +def column(*children: Any, width: float | None = None, **kwargs: Any): + """ + A column is a container that can be used to group elements. + Each element will be placed below its prior sibling. + + Args: + children: Elements to render in the column. + width: The percent width of the column relative to other children of its parent. If not provided, the column will be sized automatically. + """ + return BaseElement( + "deephaven.ui.components.Column", *children, width=width, **kwargs + ) diff --git a/plugins/ui/src/deephaven/ui/components/dashboard.py b/plugins/ui/src/deephaven/ui/components/dashboard.py new file mode 100644 index 000000000..d97cb9310 --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/dashboard.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import Any +from ..elements import BaseElement + + +def dashboard(*children: Any, **kwargs: Any): + """ + A dashboard is the container for an entire layout. + + Args: + children: Elements to render in the dashboard. Must have only 1 root element. + """ + return BaseElement("deephaven.ui.components.Dashboard", *children, **kwargs) diff --git a/plugins/ui/src/deephaven/ui/components/row.py b/plugins/ui/src/deephaven/ui/components/row.py new file mode 100644 index 000000000..46700867b --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/row.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from typing import Any +from ..elements import BaseElement + + +def row(*children: Any, height: float | None = None, **kwargs: Any): + """ + A row is a container that can be used to group elements. + Each element will be placed to the right of its prior sibling. + + Args: + children: Elements to render in the row. + height: The percent height of the row relative to other children of its parent. If not provided, the row will be sized automatically. + """ + return BaseElement( + "deephaven.ui.components.Row", *children, height=height, **kwargs + ) diff --git a/plugins/ui/src/deephaven/ui/components/stack.py b/plugins/ui/src/deephaven/ui/components/stack.py new file mode 100644 index 000000000..9687145e9 --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/stack.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import Any +from ..elements import BaseElement + + +def stack( + *children: Any, + height: float | None = None, + width: float | None = None, + activeItemIndex: int | None = None, + **kwargs: Any, +): + """ + A stack is a container that can be used to group elements which creates a set of tabs. + Each element will get a tab and only one element can be visible at a time. + + Args: + children: Elements to render in the row. + height: The percent height of the stack relative to other children of its parent. If not provided, the stack will be sized automatically. + width: The percent width of the stack relative to other children of its parent. If not provided, the stack will be sized automatically. + """ + return BaseElement( + "deephaven.ui.components.Stack", + *children, + height=height, + width=width, + activeItemIndex=activeItemIndex, + **kwargs, + ) diff --git a/plugins/ui/src/js/src/DocumentHandler.test.tsx b/plugins/ui/src/js/src/DocumentHandler.test.tsx index 758536030..fc9d1ba2d 100644 --- a/plugins/ui/src/js/src/DocumentHandler.test.tsx +++ b/plugins/ui/src/js/src/DocumentHandler.test.tsx @@ -3,7 +3,7 @@ import { WidgetDefinition } from '@deephaven/dashboard'; import { TestUtils } from '@deephaven/utils'; import { render } from '@testing-library/react'; import DocumentHandler, { DocumentHandlerProps } from './DocumentHandler'; -import { PANEL_ELEMENT_NAME, ReactPanelProps } from './PanelUtils'; +import { PANEL_ELEMENT_NAME, ReactPanelProps } from './layout/LayoutUtils'; import { MixedPanelsError, NoChildrenError } from './errors'; import { getComponentForElement } from './WidgetUtils'; diff --git a/plugins/ui/src/js/src/DocumentUtils.tsx b/plugins/ui/src/js/src/DocumentUtils.tsx index ab8c9c404..ca68262e8 100644 --- a/plugins/ui/src/js/src/DocumentUtils.tsx +++ b/plugins/ui/src/js/src/DocumentUtils.tsx @@ -32,17 +32,17 @@ export function getRootChildren( throw new MixedPanelsError('Cannot mix panel and non-panel elements'); } - if (childPanelCount === 0) { - // Just wrap it in a panel - return ( - - {children} - - ); - } + // if (childPanelCount === 0) { + // // Just wrap it in a panel + // return ( + // + // {children} + // + // ); + // } // It's already got panels defined, just return it return children; diff --git a/plugins/ui/src/js/src/PanelUtils.test.ts b/plugins/ui/src/js/src/PanelUtils.test.ts deleted file mode 100644 index 319434d5b..000000000 --- a/plugins/ui/src/js/src/PanelUtils.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PANEL_ELEMENT_NAME, isPanelElementNode } from './PanelUtils'; - -describe('isPanelElementNode', () => { - test.each([ - [{ props: { title: 'test' } }, false], - [{ props: { title: 'test' }, __dhElemName: 'a different name' }, false], - [{ props: { title: 'test' }, __dhElemName: PANEL_ELEMENT_NAME }, true], - ])(`isPanelElementNode(%s)`, (element, result) => { - expect(isPanelElementNode(element)).toBe(result); - }); -}); diff --git a/plugins/ui/src/js/src/PanelUtils.ts b/plugins/ui/src/js/src/PanelUtils.ts deleted file mode 100644 index 3cfbf17b9..000000000 --- a/plugins/ui/src/js/src/PanelUtils.ts +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils'; - -export const PANEL_ELEMENT_NAME = 'deephaven.ui.components.Panel'; - -export type PanelElementType = typeof PANEL_ELEMENT_NAME; - -export type ReactPanelProps = React.PropsWithChildren<{ - /** Title of the panel */ - title?: string; -}>; - -/** - * Describes a panel element that can be rendered in the UI. - * Will be placed in the current dashboard, or within a user created dashboard if specified. - */ -export type PanelElementNode = ElementNode; - -/** - * Check if an object is a PanelElementNode - * @param obj Object to check - * @returns True if the object is a PanelElementNode - */ -export function isPanelElementNode(obj: unknown): obj is PanelElementNode { - return ( - isElementNode(obj) && - (obj as ElementNode)[ELEMENT_KEY] === PANEL_ELEMENT_NAME - ); -} diff --git a/plugins/ui/src/js/src/ReactPanel.test.tsx b/plugins/ui/src/js/src/ReactPanel.test.tsx index 0ec339558..9402dd6b5 100644 --- a/plugins/ui/src/js/src/ReactPanel.test.tsx +++ b/plugins/ui/src/js/src/ReactPanel.test.tsx @@ -6,7 +6,7 @@ import { ReactPanelManager, ReactPanelManagerContext, } from './ReactPanelManager'; -import { ReactPanelProps } from './PanelUtils'; +import { ReactPanelProps } from './layout/LayoutUtils'; // Mock LayoutUtils, useListener, and PanelEvent from @deephaven/dashboard package const mockLayout = { root: {}, eventHub: {} }; diff --git a/plugins/ui/src/js/src/ReactPanel.tsx b/plugins/ui/src/js/src/ReactPanel.tsx index 25a35d4a6..57a2796c9 100644 --- a/plugins/ui/src/js/src/ReactPanel.tsx +++ b/plugins/ui/src/js/src/ReactPanel.tsx @@ -10,7 +10,8 @@ import { import Log from '@deephaven/log'; import PortalPanel from './PortalPanel'; import { useReactPanelManager } from './ReactPanelManager'; -import { ReactPanelProps } from './PanelUtils'; +import { ReactPanelProps } from './layout/LayoutUtils'; +import { useParentItem } from './layout/ParentItemContext'; const log = Log.module('@deephaven/js-plugin-ui/ReactPanel'); @@ -25,6 +26,7 @@ function ReactPanel({ children, title }: ReactPanelProps) { const [element, setElement] = useState(); const isPanelOpenRef = useRef(false); const openedMetadataRef = useRef>(); + const parent = useParentItem(); log.debug2('Rendering panel', panelId); @@ -32,12 +34,12 @@ function ReactPanel({ children, title }: ReactPanelProps) { () => () => { if (isPanelOpenRef.current) { log.debug('Closing panel', panelId); - LayoutUtils.closeComponent(layoutManager.root, { id: panelId }); + LayoutUtils.closeComponent(parent, { id: panelId }); isPanelOpenRef.current = false; onClose(panelId); } }, - [layoutManager, onClose, panelId] + [parent, onClose, panelId] ); const handlePanelClosed = useCallback( @@ -76,15 +78,14 @@ function ReactPanel({ children, title }: ReactPanelProps) { id: panelId, }; - const { root } = layoutManager; - LayoutUtils.openComponent({ root, config }); + LayoutUtils.openComponent({ root: parent, config }); log.debug('Opened panel', panelId, config); isPanelOpenRef.current = true; openedMetadataRef.current = metadata; onOpen(panelId); } - }, [layoutManager, metadata, onOpen, panelId, title]); + }, [parent, metadata, onOpen, panelId, title]); return element ? ReactDOM.createPortal(children, element) : null; } diff --git a/plugins/ui/src/js/src/WidgetUtils.tsx b/plugins/ui/src/js/src/WidgetUtils.tsx index 17fde478a..d753fd6b9 100644 --- a/plugins/ui/src/js/src/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/WidgetUtils.tsx @@ -14,9 +14,19 @@ import { isIconElementNode } from './IconElementUtils'; import IconElementView from './IconElementView'; import { isUITable } from './UITableUtils'; import UITable from './UITable'; -import { isPanelElementNode } from './PanelUtils'; +import { + isColumnElementNode, + isDashboardElementNode, + isPanelElementNode, + isRowElementNode, + isStackElementNode, +} from './layout/LayoutUtils'; import ReactPanel from './ReactPanel'; import ObjectView from './ObjectView'; +import Row from './layout/Row'; +import Stack from './layout/Stack'; +import Column from './layout/Column'; +import Dashboard from './layout/Dashboard'; export function getComponentForElement(element: ElementNode): React.ReactNode { // Need to convert the children of the element if they are exported objects to an ObjectView @@ -59,6 +69,18 @@ export function getComponentForElement(element: ElementNode): React.ReactNode { // eslint-disable-next-line react/jsx-no-useless-fragment return <>{newElement.props?.children}; } + if (isRowElementNode(newElement)) { + return ; + } + if (isColumnElementNode(newElement)) { + return ; + } + if (isStackElementNode(newElement)) { + return ; + } + if (isDashboardElementNode(newElement)) { + return ; + } return newElement.props?.children; } diff --git a/plugins/ui/src/js/src/layout/Column.tsx b/plugins/ui/src/js/src/layout/Column.tsx new file mode 100644 index 000000000..e67912992 --- /dev/null +++ b/plugins/ui/src/js/src/layout/Column.tsx @@ -0,0 +1,38 @@ +import React, { useEffect, useState } from 'react'; +import { useLayoutManager } from '@deephaven/dashboard'; +import type { ColumnElementProps } from './LayoutUtils'; +import { ParentItemContext, useParentItem } from './ParentItemContext'; + +function Column({ children, width }: ColumnElementProps): JSX.Element | null { + const layoutManager = useLayoutManager(); + const parent = useParentItem(); + const [column] = useState(() => { + const newColumn = layoutManager.createContentItem( + { + type: 'column', + width, + }, + parent + ); + + parent.addChild(newColumn, undefined, true); + + return newColumn; + }); + + useEffect(() => { + column.setSize(); + + return () => { + column.remove(); + }; + }, [column]); + + return ( + + {children} + + ); +} + +export default Column; diff --git a/plugins/ui/src/js/src/layout/Dashboard.tsx b/plugins/ui/src/js/src/layout/Dashboard.tsx new file mode 100644 index 000000000..a26477b15 --- /dev/null +++ b/plugins/ui/src/js/src/layout/Dashboard.tsx @@ -0,0 +1,27 @@ +import React, { useEffect, useState } from 'react'; +import { useLayoutManager } from '@deephaven/dashboard'; +import type { StackElementProps } from './LayoutUtils'; +import { ParentItemContext } from './ParentItemContext'; + +function Dashboard({ children }: StackElementProps): JSX.Element | null { + const layoutManager = useLayoutManager(); + const [dashboard, setDashboard] = useState(false); + + useEffect(() => { + layoutManager.root.callDownwards('_$destroy', [], true, true); + layoutManager.root.contentItems = []; + setDashboard(true); + }, []); + + if (!dashboard) { + return null; + } + + return ( + + {children} + + ); +} + +export default Dashboard; diff --git a/plugins/ui/src/js/src/layout/LayoutUtils.test.ts b/plugins/ui/src/js/src/layout/LayoutUtils.test.ts new file mode 100644 index 000000000..28a10160f --- /dev/null +++ b/plugins/ui/src/js/src/layout/LayoutUtils.test.ts @@ -0,0 +1,56 @@ +import { + PANEL_ELEMENT_NAME, + ROW_ELEMENT_NAME, + COLUMN_ELEMENT_NAME, + isPanelElementNode, + isRowElementNode, + isColumnElementNode, + isStackElementNode, + STACK_ELEMENT_NAME, +} from './LayoutUtils'; + +describe('isPanelElementNode', () => { + test.each([ + [{ props: { title: 'test' } }, false], + [{ props: { title: 'test' }, __dhElemName: 'a different name' }, false], + [{ props: { title: 'test' }, __dhElemName: PANEL_ELEMENT_NAME }, true], + ])(`isPanelElementNode(%s)`, (element, result) => { + expect(isPanelElementNode(element)).toBe(result); + }); +}); + +describe('isRowElementNode', () => { + test.each([ + [{ props: { height: 100 } }, false], + [{ props: { height: 100 }, __dhElemName: 'a different name' }, false], + [{ props: { height: 100 }, __dhElemName: ROW_ELEMENT_NAME }, true], + ])(`isRowElementNode(%s)`, (element, result) => { + expect(isRowElementNode(element)).toBe(result); + }); +}); + +describe('isColumnElementNode', () => { + test.each([ + [{ props: { width: 100 } }, false], + [{ props: { width: 100 }, __dhElemName: 'a different name' }, false], + [{ props: { width: 100 }, __dhElemName: COLUMN_ELEMENT_NAME }, true], + ])(`isColumnElementNode(%s)`, (element, result) => { + expect(isColumnElementNode(element)).toBe(result); + }); +}); + +describe('isStackElementNode', () => { + test.each([ + [{ props: { height: 100, width: 100 } }, false], + [ + { props: { height: 100, width: 100 }, __dhElemName: 'a different name' }, + false, + ], + [ + { props: { height: 100, width: 100 }, __dhElemName: STACK_ELEMENT_NAME }, + true, + ], + ])(`isStackElementNode(%s)`, (element, result) => { + expect(isStackElementNode(element)).toBe(result); + }); +}); diff --git a/plugins/ui/src/js/src/layout/LayoutUtils.ts b/plugins/ui/src/js/src/layout/LayoutUtils.ts new file mode 100644 index 000000000..6eee60d8e --- /dev/null +++ b/plugins/ui/src/js/src/layout/LayoutUtils.ts @@ -0,0 +1,139 @@ +import React from 'react'; +import type { RowOrColumn, Stack, Root } from '@deephaven/golden-layout'; +import { ELEMENT_KEY, ElementNode, isElementNode } from '../ElementUtils'; + +export const PANEL_ELEMENT_NAME = 'deephaven.ui.components.Panel'; +export const ROW_ELEMENT_NAME = 'deephaven.ui.components.Row'; +export const COLUMN_ELEMENT_NAME = 'deephaven.ui.components.Column'; +export const STACK_ELEMENT_NAME = 'deephaven.ui.components.Stack'; +export const DASHBOARD_ELEMENT_NAME = 'deephaven.ui.components.Dashboard'; + +export type PanelElementType = typeof PANEL_ELEMENT_NAME; +export type RowElementType = typeof ROW_ELEMENT_NAME; +export type ColumnElementType = typeof COLUMN_ELEMENT_NAME; +export type StackElementType = typeof STACK_ELEMENT_NAME; +export type DashboardElementType = typeof DASHBOARD_ELEMENT_NAME; + +export type GoldenLayoutParent = RowOrColumn | Stack | Root; + +export type ReactPanelProps = React.PropsWithChildren<{ + /** Title of the panel */ + title?: string; +}>; + +/** + * Describes a panel element that can be rendered in the UI. + * Will be placed in the current dashboard, or within a user created dashboard if specified. + */ +export type PanelElementNode = ElementNode; + +/** + * Check if an object is a PanelElementNode + * @param obj Object to check + * @returns True if the object is a PanelElementNode + */ +export function isPanelElementNode(obj: unknown): obj is PanelElementNode { + return ( + isElementNode(obj) && + (obj as ElementNode)[ELEMENT_KEY] === PANEL_ELEMENT_NAME + ); +} + +export type RowElementProps = React.PropsWithChildren<{ + height?: number; +}>; + +/** + * Describes a row element that can be rendered in the UI. + */ +export type RowElementNode = ElementNode; + +/** + * Check if an object is a RowElementNode + * @param obj Object to check + * @returns True if the object is a RowElementNode + */ +export function isRowElementNode(obj: unknown): obj is RowElementNode { + return ( + isElementNode(obj) && (obj as ElementNode)[ELEMENT_KEY] === ROW_ELEMENT_NAME + ); +} + +export type ColumnElementProps = React.PropsWithChildren<{ + width?: number; +}>; + +/** + * Describes a column element that can be rendered in the UI. + */ +export type ColumnElementNode = ElementNode< + ColumnElementType, + ColumnElementProps +>; + +/** + * Check if an object is a ColumnElementNode + * @param obj Object to check + * @returns True if the object is a ColumnElementNode + */ +export function isColumnElementNode(obj: unknown): obj is ColumnElementNode { + return ( + isElementNode(obj) && + (obj as ElementNode)[ELEMENT_KEY] === COLUMN_ELEMENT_NAME + ); +} + +export type StackElementProps = React.PropsWithChildren<{ + height?: number; + width?: number; + activeItemIndex?: number; +}>; + +/** + * Describes a stack element that can be rendered in the UI. + */ +export type StackElementNode = ElementNode; + +/** + * Check if an object is a StackElementNode + * @param obj Object to check + * @returns True if the object is a StackElementNode + */ +export function isStackElementNode(obj: unknown): obj is StackElementNode { + return ( + isElementNode(obj) && + (obj as ElementNode)[ELEMENT_KEY] === STACK_ELEMENT_NAME + ); +} + +export type DashboardElementProps = React.PropsWithChildren< + Record +>; + +/** + * Describes a dashboard element that can be rendered in the UI. + */ +export type DashboardElementNode = ElementNode< + DashboardElementType, + DashboardElementProps +>; + +/** + * Check if an object is a DashboardElementNode + * @param obj Object to check + * @returns True if the object is a DashboardElementNode + */ +export function isDashboardElementNode( + obj: unknown +): obj is DashboardElementNode { + return ( + isElementNode(obj) && + (obj as ElementNode)[ELEMENT_KEY] === DASHBOARD_ELEMENT_NAME + ); +} + +// export function createLayoutElement( +// type: 'column' | 'row' | 'stack', +// root: RowOrColumn | Stack, +// props: Record = {} +// ) {} diff --git a/plugins/ui/src/js/src/layout/ParentItemContext.ts b/plugins/ui/src/js/src/layout/ParentItemContext.ts new file mode 100644 index 000000000..1afe302c7 --- /dev/null +++ b/plugins/ui/src/js/src/layout/ParentItemContext.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; +import { useLayoutManager } from '@deephaven/dashboard'; +import type { ContentItem } from '@deephaven/golden-layout'; + +export const ParentItemContext = createContext(null); + +export function useParentItem() { + const layoutManager = useLayoutManager(); + const parentContextItem = useContext(ParentItemContext); + return ( + parentContextItem ?? + layoutManager.root.contentItems[0] ?? + layoutManager.root + ); +} diff --git a/plugins/ui/src/js/src/layout/Row.tsx b/plugins/ui/src/js/src/layout/Row.tsx new file mode 100644 index 000000000..1e106a9ce --- /dev/null +++ b/plugins/ui/src/js/src/layout/Row.tsx @@ -0,0 +1,38 @@ +import React, { useEffect, useState } from 'react'; +import { useLayoutManager } from '@deephaven/dashboard'; +import type { RowElementProps } from './LayoutUtils'; +import { ParentItemContext, useParentItem } from './ParentItemContext'; + +function Row({ children, height }: RowElementProps): JSX.Element | null { + const layoutManager = useLayoutManager(); + const parent = useParentItem(); + const [row] = useState(() => { + const newRow = layoutManager.createContentItem( + { + type: 'row', + height, + }, + parent + ); + + parent.addChild(newRow, undefined, true); + + return newRow; + }); + + useEffect(() => { + row.setSize(); + + return () => { + row.remove(); + }; + }, [row]); + + return ( + + {children} + + ); +} + +export default Row; diff --git a/plugins/ui/src/js/src/layout/Stack.tsx b/plugins/ui/src/js/src/layout/Stack.tsx new file mode 100644 index 000000000..8aa24797d --- /dev/null +++ b/plugins/ui/src/js/src/layout/Stack.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from 'react'; +import { useLayoutManager } from '@deephaven/dashboard'; +import type { Stack as StackType } from '@deephaven/golden-layout'; +import type { StackElementProps } from './LayoutUtils'; +import { ParentItemContext, useParentItem } from './ParentItemContext'; + +function Stack({ + children, + height, + width, + activeItemIndex, +}: StackElementProps): JSX.Element | null { + const layoutManager = useLayoutManager(); + const parent = useParentItem(); + const [stack] = useState(() => { + const newStack = layoutManager.createContentItem( + { + type: 'stack', + height, + width, + activeItemIndex, + }, + parent + ); + + parent.addChild(newStack, undefined, true); + + return newStack as StackType; + }); + + useEffect(() => { + stack.setSize(); + + parent.setSize(); + + if (activeItemIndex != null) { + stack.setActiveContentItem(stack.contentItems[activeItemIndex]); + } + }, [activeItemIndex, parent, stack]); + + useEffect( + () => () => { + stack.remove(); + }, + [stack] + ); + + return ( + + {children} + + ); +} + +export default Stack;