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: Multiple dashboards #1714

Merged
merged 14 commits into from
Jan 26, 2024
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(
Copy link
Member

Choose a reason for hiding this comment

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

How will this work from Enterprise? We need to be able to pass in the hydration function and pull the query from the metadata for Enterprise.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think this hydration specifically will affect Enterprise. This is just moving the hydration function from AppMainContainer to AppDashboards since AppMainContainer doesn't need to know anything about this once the dashboards component is split out

Enterprise has its own hydration which would be part of the Dashboard already

As far as Enterprise changes go, it will need to add dashboardPluginData and also listen to the event for creating a dashboard like DHC does. There might be something w/ the plugin and plugin data that we need to modify to work w/ enterprise PQs since right now the plugin is responsible for fetching its widget

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've thought about extending WidgetPlugin in some manner so that it doesn't have to mount a panel. Right now it's kind of hacky IMO because the dashboard widget from dh.ui is loaded as part of a dashboard plugin. Instead it could be a widget plugin that mounts, but doesn't render a panel.

Or we need some sort of context/hook that lets the plugin get its own fetch function or create one with just the widget name and type. In DHE the hook could wrap any other info like PQ data into the fetch function it provides

Copy link
Member

Choose a reason for hiding this comment

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

A plugin that mounts but doesn't have a panel sounds kinda neat. Need to give it more thought.

(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
Loading