Skip to content

Commit

Permalink
feat: Multiple dashboards (#1714)
Browse files Browse the repository at this point in the history
Fixes #1683

Test with the dh.ui plugin from
deephaven/deephaven-plugins#176

Example dashboard code

```
from deephaven import ui, time_table
from deephaven.ui import use_memo, use_state
from deephaven.plot.figure import Figure

def use_wave_input():
    """
    Demonstrating a custom hook.
    Creates an input panel that controls the amplitude, frequency, and phase for a wave
    """
    amplitude, set_amplitude = use_state(1.0)
    frequency, set_frequency = use_state(1.0)
    phase, set_phase = use_state(1.0)

    input_panel = ui.flex(
        ui.slider(
            label="Amplitude",
            default_value=amplitude,
            min_value=-100.0,
            max_value=100.0,
            on_change=set_amplitude,
            step=0.1,
        ),
        ui.slider(
            label="Frequency",
            default_value=frequency,
            min_value=-100.0,
            max_value=100.0,
            on_change=set_frequency,
            step=0.1,
        ),
        ui.slider(
            label="Phase",
            default_value=phase,
            min_value=-100.0,
            max_value=100.0,
            on_change=set_phase,
            step=0.1,
        ),
        direction="column",
    )

    return amplitude, frequency, phase, input_panel

@ui.component
def multiwave():
    amplitude, frequency, phase, wave_input = use_wave_input()

    tt = use_memo(lambda: time_table("PT1s").update("x=i"), [])
    t = use_memo(
        lambda: tt.update(
            [
                f"y_sin={amplitude}*Math.sin({frequency}*x+{phase})",
                f"y_cos={amplitude}*Math.cos({frequency}*x+{phase})",
                f"y_tan={amplitude}*Math.tan({frequency}*x+{phase})",
            ]
        ),
        [amplitude, frequency, phase],
    )
    p_sin = use_memo(
        lambda: Figure().plot_xy(series_name="Sine", t=t, x="x", y="y_sin").show(), [t]
    )
    p_cos = use_memo(
        lambda: Figure().plot_xy(series_name="Cosine", t=t, x="x", y="y_cos").show(),
        [t],
    )
    p_tan = use_memo(
        lambda: Figure().plot_xy(series_name="Tangent", t=t, x="x", y="y_tan").show(),
        [t],
    )

    return [
        ui.column(
            ui.row(
                ui.stack(
                    ui.panel(wave_input, title="Wave Input"),
                    ui.panel(t, title="Wave Table"),
                    activeItemIndex=0
                ),
                height=25
            ),
            ui.row(
                ui.stack(ui.panel(p_sin, title="Sine"), width=50),
                ui.stack(ui.panel(p_cos, title="Cosine"), width=30),
                ui.stack(ui.panel(p_tan, title="Tangent"))
            )
        )
    ]

mw = ui.dashboard(multiwave())
```
  • Loading branch information
mattrunyon authored Jan 26, 2024
1 parent e1b4562 commit 32dde3c
Show file tree
Hide file tree
Showing 13 changed files with 490 additions and 212 deletions.
95 changes: 95 additions & 0 deletions packages/code-studio/src/main/AppDashboards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import {
DashboardUtils,
DEFAULT_DASHBOARD_ID,
DehydratedDashboardPanelProps,
LazyDashboard,
} from '@deephaven/dashboard';
import { useConnection } from '@deephaven/jsapi-components';
import { VariableDefinition } from '@deephaven/jsapi-types';
import LayoutManager, { ItemConfigType } from '@deephaven/golden-layout';
import { LoadingOverlay } from '@deephaven/components';
import EmptyDashboard from './EmptyDashboard';

interface AppDashboardsProps {
dashboards: {
id: string;
layoutConfig: ItemConfigType[];
}[];
activeDashboard: string;
onGoldenLayoutChange: (goldenLayout: LayoutManager) => void;
plugins: JSX.Element[];
onAutoFillClick: (event: React.MouseEvent) => void;
}

export function AppDashboards({
dashboards,
activeDashboard,
onGoldenLayoutChange,
plugins,
onAutoFillClick,
}: AppDashboardsProps): JSX.Element {
const connection = useConnection();

const hydratePanel = useCallback(
(hydrateProps: DehydratedDashboardPanelProps, id: string) => {
const { metadata } = hydrateProps;
if (
metadata?.type != null &&
(metadata?.id != null || metadata?.name != null)
) {
// Looks like a widget, hydrate it as such
const widget: VariableDefinition =
metadata.id != null
? {
type: metadata.type,
id: metadata.id,
}
: {
type: metadata.type,
name: metadata.name,
title: metadata.name,
};
return {
fetch: async () => connection?.getObject(widget),
...hydrateProps,
localDashboardId: id,
};
}
return DashboardUtils.hydrate(hydrateProps, id);
},
[connection]
);

return (
<div className="tab-content">
{dashboards.map(d => (
<div
key={d.id}
className={classNames('tab-pane', {
active: d.id === activeDashboard,
})}
>
<LazyDashboard
id={d.id}
isActive={d.id === activeDashboard}
emptyDashboard={
d.id === DEFAULT_DASHBOARD_ID ? (
<EmptyDashboard onAutoFillClick={onAutoFillClick} />
) : (
<LoadingOverlay />
)
}
layoutConfig={d.layoutConfig}
onGoldenLayoutChange={onGoldenLayoutChange}
hydrate={hydratePanel}
plugins={plugins}
/>
</div>
))}
</div>
);
}

export default AppDashboards;
4 changes: 4 additions & 0 deletions packages/code-studio/src/main/AppMainContainer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ $nav-space: 4px; // give a gap around some buttons for focus area that are in na
width: 100%;
justify-content: space-between;
align-items: center;
}

.app-main-right-menu-buttons {
.btn-link {
font-size: $tab-font-size;
text-decoration: none;
Expand All @@ -67,7 +69,9 @@ $nav-space: 4px; // give a gap around some buttons for focus area that are in na
}

.tab-pane {
height: 100%;
width: 100%;
flex-grow: 1;
}

.app-main-tabs {
Expand Down
62 changes: 36 additions & 26 deletions packages/code-studio/src/main/AppMainContainer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { Provider } from 'react-redux';
import { render, screen } from '@testing-library/react';
import { ToolType } from '@deephaven/dashboard-core-plugins';
import { ApiContext } from '@deephaven/jsapi-bootstrap';
import { ConnectionContext } from '@deephaven/jsapi-components';
import dh from '@deephaven/jsapi-shim';
import type {
IdeConnection,
IdeSession,
VariableChanges,
} from '@deephaven/jsapi-types';
import { TestUtils } from '@deephaven/utils';
import { Workspace } from '@deephaven/redux';
import { Workspace, createMockStore } from '@deephaven/redux';
import userEvent from '@testing-library/user-event';
import { DEFAULT_DASHBOARD_ID } from '@deephaven/dashboard';
import { AppMainContainer, AppDashboardData } from './AppMainContainer';
import { AppMainContainer } from './AppMainContainer';
import LocalWorkspaceStorage from '../storage/LocalWorkspaceStorage';
import LayoutStorage from '../storage/LayoutStorage';

Expand Down Expand Up @@ -69,38 +71,46 @@ function renderAppMainContainer({
match = makeMatch(),
plugins = new Map(),
} = {}) {
const store = createMockStore();
return render(
<ApiContext.Provider value={dh}>
<AppMainContainer
dashboardData={dashboardData as AppDashboardData}
layoutStorage={layoutStorage as LayoutStorage}
saveWorkspace={saveWorkspace}
updateDashboardData={updateDashboardData}
updateWorkspaceData={updateWorkspaceData}
user={user}
workspace={workspace as Workspace}
workspaceStorage={workspaceStorage}
activeTool={activeTool}
setActiveTool={setActiveTool}
setDashboardIsolatedLinkerPanelId={setDashboardIsolatedLinkerPanelId}
client={client}
serverConfigValues={serverConfigValues}
dashboardOpenedPanelMaps={dashboardOpenedPanelMaps}
connection={connection}
session={session as unknown as IdeSession}
sessionConfig={sessionConfig}
match={match}
plugins={plugins}
/>
</ApiContext.Provider>
<Provider store={store}>
<ApiContext.Provider value={dh}>
<ConnectionContext.Provider value={connection}>
<AppMainContainer
dashboardData={dashboardData}
allDashboardData={dashboardData}
layoutStorage={layoutStorage as LayoutStorage}
saveWorkspace={saveWorkspace}
updateDashboardData={updateDashboardData}
updateWorkspaceData={updateWorkspaceData}
user={user}
workspace={workspace as Workspace}
workspaceStorage={workspaceStorage}
activeTool={activeTool}
setActiveTool={setActiveTool}
setDashboardIsolatedLinkerPanelId={
setDashboardIsolatedLinkerPanelId
}
client={client}
serverConfigValues={serverConfigValues}
dashboardOpenedPanelMaps={dashboardOpenedPanelMaps}
connection={connection}
session={session as unknown as IdeSession}
sessionConfig={sessionConfig}
match={match}
plugins={plugins}
/>
</ConnectionContext.Provider>
</ApiContext.Provider>
</Provider>
);
}
let mockProp = {};
let mockId = DEFAULT_DASHBOARD_ID;
jest.mock('@deephaven/dashboard', () => ({
...jest.requireActual('@deephaven/dashboard'),
__esModule: true,
Dashboard: jest.fn(({ hydrate }) => {
LazyDashboard: jest.fn(({ hydrate }) => {
const result = hydrate(mockProp, mockId);
if (result.fetch != null) {
result.fetch();
Expand Down
Loading

0 comments on commit 32dde3c

Please sign in to comment.