Skip to content

Commit

Permalink
feat: Show loading spinners immediately in ui
Browse files Browse the repository at this point in the history
  • Loading branch information
mattrunyon committed Nov 16, 2024
1 parent f6ca5b9 commit 638fc12
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 42 deletions.
10 changes: 6 additions & 4 deletions plugins/ui/src/js/src/DashboardPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ import {
import PortalPanel from './layout/PortalPanel';
import PortalPanelManager from './layout/PortalPanelManager';
import DashboardWidgetHandler from './widget/DashboardWidgetHandler';
import { getPreservedData } from './widget/WidgetUtils';
import {
getPreservedData,
DASHBOARD_ELEMENT,
WIDGET_ELEMENT,
} from './widget/WidgetUtils';

const NAME_ELEMENT = 'deephaven.ui.Element';
const DASHBOARD_ELEMENT = 'deephaven.ui.Dashboard';
const PLUGIN_NAME = '@deephaven/js-plugin-ui.DashboardPlugin';

const log = Log.module('@deephaven/js-plugin-ui.DashboardPlugin');
Expand Down Expand Up @@ -119,7 +121,7 @@ export function DashboardPlugin(
const { type } = widget;

switch (type) {
case NAME_ELEMENT: {
case WIDGET_ELEMENT: {
handleWidgetOpen({ widgetId, widget });
break;
}
Expand Down
9 changes: 8 additions & 1 deletion plugins/ui/src/js/src/layout/PortalPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useRef } from 'react';
import { DashboardPanelProps } from '@deephaven/dashboard';
import { Panel } from '@deephaven/dashboard-core-plugins';
import { LoadingOverlay } from '@deephaven/components';
import { emitPortalClosed, emitPortalOpened } from './PortalPanelEvent';

/**
Expand All @@ -27,7 +28,13 @@ function PortalPanel({

return (
<Panel glContainer={glContainer} glEventHub={glEventHub}>
<div className="ui-portal-panel" ref={ref} />
<div className="ui-portal-panel" ref={ref}>
{/* This will be hidden by CSS if it has any siblings (i.e., once the panel contents mount) */}
{/* Without this, we show a blank panel while re-hydrating instead of a loading spinner */}
<div className="ui-portal-panel-loader">
<LoadingOverlay />
</div>
</div>
</Panel>
);
}
Expand Down
5 changes: 5 additions & 0 deletions plugins/ui/src/js/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
overflow: hidden;
}

// Hide if the loader has any siblings
.ui-portal-panel-loader:has(+ *) {
display: none;
}

.ui-table-container {
// If all browsers properly supported 'stretch' for height and width
// we could swap to that, but right now only Chrome properly implements it.
Expand Down
77 changes: 40 additions & 37 deletions plugins/ui/src/js/src/widget/WidgetHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,16 @@ import {
METHOD_DOCUMENT_UPDATED,
} from './WidgetTypes';
import DocumentHandler from './DocumentHandler';
import { getComponentForElement, wrapCallable } from './WidgetUtils';
import {
getComponentForElement,
WIDGET_ELEMENT,
wrapCallable,
} from './WidgetUtils';
import WidgetStatusContext, {
WidgetStatus,
} from '../layout/WidgetStatusContext';
import WidgetErrorView from './WidgetErrorView';
import ReactPanel from '../layout/ReactPanel';

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

Expand Down Expand Up @@ -77,7 +82,12 @@ function WidgetHandler({
setIsLoading(true);
}

const [document, setDocument] = useState<ReactNode>();
// Default to a single panel so we can immediately show a loading spinner
// The panel will be replaced with the first actual panel when the document loads
// Dashboards already have a loader and the placeholder panel causes an error
const [document, setDocument] = useState<ReactNode>(
widgetDescriptor.type === WIDGET_ELEMENT ? <ReactPanel /> : null
);

// We want to update the initial data if the widget changes, as we'll need to re-fetch the widget and want to start with a fresh state.
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down Expand Up @@ -261,9 +271,16 @@ function WidgetHandler({
const newError: WidgetError = JSON.parse(params[0]);
newError.action = {
title: 'Reload',
action: () => sendSetState(),
action: () => {
setInternalError(undefined);
setIsLoading(true);
sendSetState();
},
};
setInternalError(newError);
unstable_batchedUpdates(() => {
setIsLoading(false);
setInternalError(newError);
});
});

return () => {
Expand Down Expand Up @@ -345,48 +362,34 @@ function WidgetHandler({
return document;
}
if (error != null) {
// If there's an error and the document hasn't rendered yet, explicitly show an error view
// If there's an error and the document hasn't rendered yet (mostly applies to dashboards), explicitly show an error view
return <WidgetErrorView error={error} />;
}
return null;
return document;
}, [document, error]);

const widgetStatus: WidgetStatus = useMemo(() => {
if (error != null) {
return { status: 'error', descriptor: widgetDescriptor, error };
}
if (isLoading) {
return { status: 'loading', descriptor: widgetDescriptor };
}
if (renderedDocument != null) {
return { status: 'ready', descriptor: widgetDescriptor };
if (error != null) {
return { status: 'error', descriptor: widgetDescriptor, error };
}
return { status: 'loading', descriptor: widgetDescriptor };
}, [error, renderedDocument, widgetDescriptor, isLoading]);

return useMemo(
() =>
renderedDocument ? (
<WidgetStatusContext.Provider value={widgetStatus}>
<DocumentHandler
widget={widgetDescriptor}
initialData={initialData}
onDataChange={onDataChange}
onClose={onClose}
>
{renderedDocument}
</DocumentHandler>
</WidgetStatusContext.Provider>
) : null,
[
widgetDescriptor,
renderedDocument,
initialData,
onClose,
onDataChange,
widgetStatus,
]
);
return { status: 'ready', descriptor: widgetDescriptor };
}, [error, widgetDescriptor, isLoading]);

return renderedDocument != null ? (
<WidgetStatusContext.Provider value={widgetStatus}>
<DocumentHandler
widget={widgetDescriptor}
initialData={initialData}
onDataChange={onDataChange}
onClose={onClose}
>
{renderedDocument}
</DocumentHandler>
</WidgetStatusContext.Provider>
) : null;
}

WidgetHandler.displayName = '@deephaven/js-plugin-ui/WidgetHandler';
Expand Down
3 changes: 3 additions & 0 deletions plugins/ui/src/js/src/widget/WidgetUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ import {
Tabs,
} from '../elements';

export const WIDGET_ELEMENT = 'deephaven.ui.Element';
export const DASHBOARD_ELEMENT = 'deephaven.ui.Dashboard';

/**
* Elements to implicitly wrap primitive children in <Text> components.
*/
Expand Down

0 comments on commit 638fc12

Please sign in to comment.