Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: UI dashboard #176

Merged
merged 13 commits into from
Jan 26, 2024
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions plugins/ui/src/deephaven/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
class UIRegistration(Registration):
@classmethod
def register_into(cls, callback: Callback) -> None:
callback.register(DashboardType)
callback.register(ElementType)
9 changes: 9 additions & 0 deletions plugins/ui/src/deephaven/ui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
from .panel import panel
from .spectrum import *
from .table import table
from .dashboard import dashboard
from .row import row
from .column import column
from .stack import stack

from . import html


Expand All @@ -12,9 +17,11 @@
"button",
"button_group",
"checkbox",
"column",
"component",
"content",
"contextual_help",
"dashboard",
"flex",
"form",
"fragment",
Expand All @@ -28,8 +35,10 @@
"item",
"panel",
"range_slider",
"row",
"slider",
"spectrum_element",
"stack",
"switch",
"table",
"tab_list",
Expand Down
18 changes: 18 additions & 0 deletions plugins/ui/src/deephaven/ui/components/column.py
Original file line number Diff line number Diff line change
@@ -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
)
15 changes: 15 additions & 0 deletions plugins/ui/src/deephaven/ui/components/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import annotations

from typing import Any
from ..elements import DashboardElement, FunctionElement


def dashboard(element: FunctionElement):
"""
A dashboard is the container for an entire layout.

Args:
element: Element to render as the dashboard.
The element should render a layout that contains 1 root column or row.
"""
return DashboardElement(element)
18 changes: 18 additions & 0 deletions plugins/ui/src/deephaven/ui/components/row.py
Original file line number Diff line number Diff line change
@@ -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
)
30 changes: 30 additions & 0 deletions plugins/ui/src/deephaven/ui/components/stack.py
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A question I"m sure that will come up is how do we give a name to the tabs. I'm guessing you need a ui.panel to specify a title. Do we even allow non-ui.panel things here? Should we enforce only passing in PanelElements as the children?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you'd need to use a panel w/ the title param

In this first pass we don't allow non panels here. This would be something we consider auto-wrapping in an update to make it nicer to use. I think it's reasonable to allow ui.stack(some_table, some_other_table) if you're fine with the default panel titles


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,
)
12 changes: 12 additions & 0 deletions plugins/ui/src/deephaven/ui/elements/DashboardElement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from __future__ import annotations

import logging
from .BaseElement import BaseElement
from .FunctionElement import FunctionElement

logger = logging.getLogger(__name__)


class DashboardElement(BaseElement):
def __init__(self, element: FunctionElement):
super().__init__("deephaven.ui.components.Dashboard", element)
10 changes: 9 additions & 1 deletion plugins/ui/src/deephaven/ui/elements/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from .Element import Element, PropsType
from .BaseElement import BaseElement
from .DashboardElement import DashboardElement
from .FunctionElement import FunctionElement
from .UITable import UITable

__all__ = ["BaseElement", "Element", "FunctionElement", "PropsType", "UITable"]
__all__ = [
"BaseElement",
"DashboardElement",
"Element",
"FunctionElement",
"PropsType",
"UITable",
]
17 changes: 17 additions & 0 deletions plugins/ui/src/deephaven/ui/object_types/DashboardType.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from ..elements import DashboardElement
from .._internal import get_component_name
from .ElementMessageStream import ElementMessageStream
from .ElementType import ElementType


class DashboardType(ElementType):
"""
Defines the Dashboard type for the Deephaven plugin system.
"""

@property
def name(self) -> str:
return "deephaven.ui.Dashboard"

def is_type(self, obj: any) -> bool:
return isinstance(obj, DashboardElement)
1 change: 1 addition & 0 deletions plugins/ui/src/deephaven/ui/object_types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .DashboardType import DashboardType
from .ElementMessageStream import ElementMessageStream
from .ElementType import ElementType
16 changes: 16 additions & 0 deletions plugins/ui/src/js/__mocks__/@deephaven/dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Mock LayoutUtils, useListener, and PanelEvent from @deephaven/dashboard package
const mockLayout = { root: { contentItems: [] }, eventHub: {} };

const DashboardActual = jest.requireActual('@deephaven/dashboard');
module.exports = {
...DashboardActual,
LayoutUtils: {
getComponentName: jest.fn(),
openComponent: jest.fn(),
closeComponent: jest.fn(),
},
useLayoutManager: jest.fn(() => mockLayout),
useListener: jest.fn(),
__esModule: true,
default: jest.fn(),
};
5 changes: 3 additions & 2 deletions plugins/ui/src/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@
"@deephaven/icons": "^0.60.0",
"@deephaven/iris-grid": "^0.60.0",
"@deephaven/jsapi-bootstrap": "^0.60.0",
"@deephaven/jsapi-components": "^0.60.0",
"@deephaven/jsapi-types": "^0.60.0",
"@deephaven/log": "^0.60.0",
"@deephaven/plugin": "^0.60.0",
"@deephaven/react-hooks": "^0.60.0",
"@deephaven/utils": "^0.60.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"shortid": "^2.2.16",
"json-rpc-2.0": "^1.6.0"
"json-rpc-2.0": "^1.6.0",
"shortid": "^2.2.16"
},
"publishConfig": {
"access": "public"
Expand Down
119 changes: 109 additions & 10 deletions plugins/ui/src/js/src/DashboardPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import {
LayoutManagerContext,
PanelEvent,
useListener,
useDashboardPluginData,
emitCreateDashboard,
} from '@deephaven/dashboard';
import Log from '@deephaven/log';
import { useConnection } from '@deephaven/jsapi-components';
import { DeferredApiBootstrap } from '@deephaven/jsapi-bootstrap';
import { Widget } from '@deephaven/jsapi-types';
import type { VariableDefinition } from '@deephaven/jsapi-types';
Expand All @@ -16,37 +19,48 @@ import PortalPanel from './PortalPanel';
import WidgetHandler from './WidgetHandler';

const NAME_ELEMENT = 'deephaven.ui.Element';
const DASHBOARD_ELEMENT = 'deephaven.ui.Dashboard';

const log = Log.module('@deephaven/js-plugin-ui.DashboardPlugin');

/**
* The data stored in redux when the user creates a ui.dashboard.
*/
interface DashboardPluginData {
type: string;
title: string;
id: string;
metadata: Record<string, unknown>;
}

export function DashboardPlugin({
id,
layout,
registerComponent,
}: DashboardPluginComponentProps): JSX.Element | null {
const connection = useConnection();
const [pluginData] = useDashboardPluginData(
id,
DASHBOARD_ELEMENT
) as unknown as [DashboardPluginData];

// Keep track of the widgets we've got opened.
const [widgetMap, setWidgetMap] = useState<
ReadonlyMap<string, WidgetWrapper>
>(new Map());
const handlePanelOpen = useCallback(

const handleWidgetOpen = useCallback(
({
dragEvent,
fetch,
metadata = {},
metadata,
panelId: widgetId = shortid.generate(),
widget,
}: {
dragEvent?: React.DragEvent;
fetch: () => Promise<Widget>;
metadata?: Record<string, unknown>;
metadata: Record<string, unknown>;
panelId?: string;
widget: VariableDefinition;
}) => {
const { type } = widget;
if ((type as string) !== NAME_ELEMENT) {
// Only want to listen for Element panels trying to be opened
return;
}
log.info('Opening widget with ID', widgetId, metadata);
setWidgetMap(prevWidgetMap => {
const newWidgetMap = new Map<string, WidgetWrapper>(prevWidgetMap);
Expand All @@ -70,6 +84,91 @@ export function DashboardPlugin({
[]
);

const handleDashboardOpen = useCallback(
({
widget,
metadata,
}: {
widget: VariableDefinition;
metadata: Record<string, unknown>;
}) => {
const { id: dashboardId, type, title = 'Untitled' } = widget;
if (dashboardId == null) {
log.error("Can't open dashboard without an ID", widget);
return;
}
log.debug('Emitting create dashboard event for', widget);
emitCreateDashboard(layout.eventHub, {
pluginId: DASHBOARD_ELEMENT,
title,
data: {
type,
title,
id: dashboardId,
metadata,
} satisfies DashboardPluginData,
});
},
[layout.eventHub]
);

const handlePanelOpen = useCallback(
({
fetch,
panelId: widgetId = shortid.generate(),
widget,
metadata = {},
}: {
fetch: () => Promise<Widget>;
panelId?: string;
widget: VariableDefinition;
metadata: Record<string, unknown>;
}) => {
const { type } = widget;

switch (type) {
case NAME_ELEMENT: {
handleWidgetOpen({ fetch, panelId: widgetId, widget, metadata });
break;
}
case DASHBOARD_ELEMENT: {
handleDashboardOpen({ widget, metadata });
break;
}
default: {
log.error('Unknown widget type', type);
}
}
},
[handleDashboardOpen, handleWidgetOpen]
);

useEffect(
function loadDashboard() {
if (pluginData == null) {
return;
}

log.info('Loading dashboard', pluginData);

setWidgetMap(prevWidgetMap => {
const newWidgetMap = new Map<string, WidgetWrapper>(prevWidgetMap);
// We need to create a new definition object, otherwise the layout will think it's already open
// Can't use a spread operator because the widget definition uses property accessors

newWidgetMap.set(id, {
definition: pluginData,
fetch: () =>
connection.getObject(pluginData) as unknown as Promise<Widget>,
id,
metadata: {},
});
return newWidgetMap;
});
},
[connection, pluginData, id]
);

const handlePanelClose = useCallback((panelId: string) => {
setWidgetMap(prevWidgetMap => {
if (!prevWidgetMap.has(panelId)) {
Expand Down
Loading
Loading