Skip to content

Commit dea320e

Browse files
authored
feat: loading ux v1.3.3
feat: loading ux v1.3.3
2 parents fedf518 + 7b79fbb commit dea320e

15 files changed

Lines changed: 349 additions & 104 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ioai/rosview",
3-
"version": "1.3.2",
3+
"version": "1.3.3",
44
"description": "High-performance robotics data visualization for MCAP, ROS bag, ROS2 db3, HDF5 and BVH — embeddable React component and standalone SPA",
55
"keywords": [
66
"ros",

src/app/AppShell.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ interface AppShellProps {
4242
onSubmitRemoteUrl: (url: string) => void | Promise<void>;
4343
remoteSubmitLoading?: boolean;
4444
onSelectSample: (sample: SampleDataset) => void | Promise<void>;
45+
onCancelLoading?: () => void;
4546
historyItems: DatasetHistoryListItem[];
4647
onReplayHistory: (id: string) => void | Promise<void>;
4748
onDropRosRecordingFiles: (files: File[], items?: DataTransferItemList) => void | Promise<void>;
@@ -91,6 +92,7 @@ export const AppShell: React.FC<AppShellProps> = ({
9192
onSubmitRemoteUrl,
9293
remoteSubmitLoading,
9394
onSelectSample,
95+
onCancelLoading,
9496
historyItems,
9597
onReplayHistory,
9698
onDropRosRecordingFiles,
@@ -175,7 +177,7 @@ export const AppShell: React.FC<AppShellProps> = ({
175177
onSubmitRemoteUrl={onSubmitRemoteUrl}
176178
remoteSubmitLoading={remoteSubmitLoading}
177179
onSelectSample={onSelectSample}
178-
onRequestChangeRemoteUrl={onOpenRemotePrompt}
180+
onCancelLoading={onCancelLoading}
179181
historyItems={historyItems}
180182
onReplayHistory={onReplayHistory}
181183
onDropRosRecordingFiles={onDropRosRecordingFiles}

src/features/viewer/RosViewContent.tsx

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import {
1313
import { openRawMessagesPanel } from '@/features/workspace/sidebar/topic-list/openRawMessagesPanel';
1414
import { useIntl } from 'react-intl';
1515
import { WelcomeScreen } from '@/features/workspace/common/WelcomeScreen';
16+
import { LoadingOverlay } from '@/features/workspace/common/LoadingOverlay';
1617
import { useMessagePipeline } from '@/core/pipeline/useMessagePipeline';
1718
import type { MessagePipelineState } from '@/core/pipeline/store';
1819
import { useMessagePipelineStore } from '@/core/pipeline/store';
1920
import type { SampleDataset } from '@/services/sampleDatasets';
2021
import type { DatasetHistoryListItem } from '@/shared/utils/datasetHistory';
2122
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/shared/ui/resizable';
23+
import { Skeleton } from '@/shared/ui/skeleton';
2224
import type { RosViewExtension } from '@/core/extensions/types';
2325
import { buildExtensionContext } from '@/core/extensions/buildContext';
2426
import type { FoxgloveLayoutData } from '@/core/preferences/foxgloveLayout';
@@ -49,7 +51,7 @@ interface RosViewContentProps {
4951
onSubmitRemoteUrl: (url: string) => void | Promise<void>;
5052
remoteSubmitLoading?: boolean;
5153
onSelectSample: (sample: SampleDataset) => void | Promise<void>;
52-
onRequestChangeRemoteUrl?: () => void;
54+
onCancelLoading?: () => void;
5355
historyItems: DatasetHistoryListItem[];
5456
onReplayHistory: (id: string) => void | Promise<void>;
5557
onDropRosRecordingFiles: (files: File[], items?: DataTransferItemList) => void | Promise<void>;
@@ -89,7 +91,7 @@ export const RosViewContent: React.FC<RosViewContentProps> = ({
8991
onSubmitRemoteUrl,
9092
remoteSubmitLoading,
9193
onSelectSample,
92-
onRequestChangeRemoteUrl,
94+
onCancelLoading,
9395
historyItems,
9496
onReplayHistory,
9597
onDropRosRecordingFiles,
@@ -112,8 +114,8 @@ export const RosViewContent: React.FC<RosViewContentProps> = ({
112114
const { formatMessage } = useIntl();
113115
const presence = useMessagePipeline((state: MessagePipelineState) => state.playerState.presence);
114116
const sortedTopics = useMessagePipeline((state: MessagePipelineState) => state.sortedTopics);
115-
const isLoading = presence === 'initializing';
116117
const isReady = presence === 'ready';
118+
const showWelcomeFallback = !isReady && Boolean(manualOpenHint);
117119
const topicDragDepthRef = useRef(0);
118120
const [sidebarPanelPercent, setSidebarPanelPercent] = useState(() =>
119121
getInitialSidebarPanelPercent(preferencePersistence),
@@ -251,19 +253,16 @@ export const RosViewContent: React.FC<RosViewContentProps> = ({
251253

252254
return (
253255
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
254-
{!isReady ? (
256+
{showWelcomeFallback ? (
255257
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
256258
<WelcomeScreen
257-
isLoading={isLoading}
258-
loadingSourceName={loadingSourceName}
259259
manualOpenHint={manualOpenHint}
260260
onOpenFile={onOpenFilePick}
261261
onOpenDirectory={onOpenDirectory}
262262
onOpenTarPicker={onOpenTarPick}
263263
onSubmitRemoteUrl={onSubmitRemoteUrl}
264264
remoteSubmitLoading={remoteSubmitLoading}
265265
onSelectSample={onSelectSample}
266-
onRequestChangeRemoteUrl={onRequestChangeRemoteUrl}
267266
historyItems={historyItems}
268267
onReplayHistory={onReplayHistory}
269268
/>
@@ -334,10 +333,10 @@ export const RosViewContent: React.FC<RosViewContentProps> = ({
334333
className={`relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-background [contain:strict] ${
335334
isTopicDragOver ? 'ring-1 ring-inset ring-primary/40' : ''
336335
}`}
337-
onDragEnter={handleTopicDragEnter}
338-
onDragOver={handleMainDragOver}
339-
onDragLeave={handleTopicDragLeave}
340-
onDrop={handleMainDrop}
336+
onDragEnter={isReady ? handleTopicDragEnter : undefined}
337+
onDragOver={isReady ? handleMainDragOver : undefined}
338+
onDragLeave={isReady ? handleTopicDragLeave : undefined}
339+
onDrop={isReady ? handleMainDrop : undefined}
341340
>
342341
{isTopicDragOver && (
343342
<div className="pointer-events-none absolute inset-4 z-10 flex items-center justify-center rounded-lg border border-dashed border-primary/60 bg-primary/5">
@@ -351,17 +350,30 @@ export const RosViewContent: React.FC<RosViewContentProps> = ({
351350
</div>
352351
</div>
353352
)}
354-
<DockviewLayout
355-
key={activeDatasetId ?? 'dataset'}
356-
player={player}
357-
preferAutoLayout={preferAutoLayout}
358-
initialLayout={initialLayout}
359-
defaultPanel={defaultPanel}
360-
layoutPersistence={layoutPersistence}
361-
layoutStorageKey={layoutStorageKey}
362-
suppressWelcomePanel={suppressWelcomePanel}
363-
onLayoutReady={onLayoutReady}
364-
/>
353+
{isReady ? (
354+
<DockviewLayout
355+
key={activeDatasetId ?? 'dataset'}
356+
player={player}
357+
preferAutoLayout={preferAutoLayout}
358+
initialLayout={initialLayout}
359+
defaultPanel={defaultPanel}
360+
layoutPersistence={layoutPersistence}
361+
layoutStorageKey={layoutStorageKey}
362+
suppressWelcomePanel={suppressWelcomePanel}
363+
onLayoutReady={onLayoutReady}
364+
/>
365+
) : (
366+
<div className="flex min-h-0 flex-1 flex-col gap-3 p-4">
367+
<Skeleton className="h-8 w-40" />
368+
<Skeleton className="min-h-0 flex-1 rounded-lg" />
369+
</div>
370+
)}
371+
{!isReady ? (
372+
<LoadingOverlay
373+
sourceName={loadingSourceName}
374+
onCancel={onCancelLoading}
375+
/>
376+
) : null}
365377
</main>
366378
</ResizablePanel>
367379
</ResizablePanelGroup>

src/features/viewer/RosViewerImpl.tsx

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import { SampleDatasetDialog } from '@/features/workspace/common/SampleDatasetDi
77
import { getArchiveUrl, getSampleDatasetsManifestUrl, loadSampleDatasets } from '@/services/sampleDatasets';
88
import type { SampleDataset } from '@/services/sampleDatasets';
99
import { extractRosFilesFromTarArchive } from '@/shared/utils/tarRosRecordings';
10-
import { WorkerSerializedSource } from '@/infra/workers/WorkerSerializedSource';
10+
import { WorkerSerializedSource, isWorkerSourceCancelledError } from '@/infra/workers/WorkerSerializedSource';
1111
import { IterablePlayer } from '@/core/players/IterablePlayer';
1212
import { MinimalPlayer } from '@/core/players/MinimalPlayer';
1313
import type { Player } from '@/core/types/player';
1414
import { useMessagePipelineStore } from '@/core/pipeline/store';
1515
import { Navbar } from '@/features/workspace/navbar/Navbar';
1616
import { WelcomeScreen } from '@/features/workspace/common/WelcomeScreen';
17+
import { LoadingOverlay } from '@/features/workspace/common/LoadingOverlay';
18+
import { Skeleton } from '@/shared/ui/skeleton';
1719
import type { DatasetItem, FileListItem } from '@/shared/utils/datasetSources';
1820
import {
1921
datasetItemsFromListItems,
@@ -771,6 +773,11 @@ export const RosViewer: React.FC<RosViewerProps> = (props) => {
771773
}
772774
return;
773775
} catch (err) {
776+
if (cancelled || isWorkerSourceCancelledError(err)) {
777+
newPlayer.close();
778+
createdPlayer = null;
779+
return;
780+
}
774781
lastErr = err;
775782
newPlayer.close();
776783
createdPlayer = null;
@@ -1166,6 +1173,7 @@ export const RosViewer: React.FC<RosViewerProps> = (props) => {
11661173
onSubmitRemoteUrl={handleOpenRemoteRecordingUrl}
11671174
remoteSubmitLoading={remoteUrlBusy}
11681175
onSelectSample={handleSelectSample}
1176+
onCancelLoading={handleGoHome}
11691177
historyItems={historyItems}
11701178
onReplayHistory={(id) => void handleReplayHistory(id)}
11711179
onDropRosRecordingFiles={handleDropRosRecordingFiles}
@@ -1326,23 +1334,33 @@ export const RosViewer: React.FC<RosViewerProps> = (props) => {
13261334
recentHistoryItems={historyItems.slice(0, 10)}
13271335
onReplayHistory={(id) => void handleReplayHistory(id)}
13281336
/>
1329-
<WelcomeScreen
1330-
isLoading={!lastLoadError && !manualOpenHint}
1331-
loadingSourceName={loadingSourceName}
1332-
manualOpenHint={manualOpenHint}
1333-
onOpenFile={() => {
1334-
clearOpenFeedback();
1335-
void handleOpenRecordingFiles();
1336-
}}
1337-
onOpenDirectory={handleOpenDirectory}
1338-
onOpenTarPicker={() => document.getElementById('rosview-inline-tar')?.click()}
1339-
onSubmitRemoteUrl={handleOpenRemoteRecordingUrl}
1340-
remoteSubmitLoading={remoteUrlBusy}
1341-
onSelectSample={handleSelectSample}
1342-
onRequestChangeRemoteUrl={openRemotePrompt}
1343-
historyItems={historyItems}
1344-
onReplayHistory={(id) => void handleReplayHistory(id)}
1345-
/>
1337+
{!lastLoadError && !manualOpenHint ? (
1338+
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
1339+
<div className="flex min-h-0 flex-1 flex-col gap-3 p-4">
1340+
<Skeleton className="h-8 w-40" />
1341+
<Skeleton className="min-h-0 flex-1 rounded-lg" />
1342+
</div>
1343+
<LoadingOverlay
1344+
sourceName={loadingSourceName}
1345+
onCancel={handleGoHome}
1346+
/>
1347+
</div>
1348+
) : (
1349+
<WelcomeScreen
1350+
manualOpenHint={manualOpenHint}
1351+
onOpenFile={() => {
1352+
clearOpenFeedback();
1353+
void handleOpenRecordingFiles();
1354+
}}
1355+
onOpenDirectory={handleOpenDirectory}
1356+
onOpenTarPicker={() => document.getElementById('rosview-inline-tar')?.click()}
1357+
onSubmitRemoteUrl={handleOpenRemoteRecordingUrl}
1358+
remoteSubmitLoading={remoteUrlBusy}
1359+
onSelectSample={handleSelectSample}
1360+
historyItems={historyItems}
1361+
onReplayHistory={(id) => void handleReplayHistory(id)}
1362+
/>
1363+
)}
13461364
<SampleDatasetDialog
13471365
open={sampleDialogOpen}
13481366
onOpenChange={setSampleDialogOpen}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
import { useIntl } from 'react-intl';
3+
import { Button } from '@/shared/ui/button';
4+
import { Card, CardFooter, CardHeader, CardTitle } from '@/shared/ui/card';
5+
import { Spinner } from '@/shared/ui/spinner';
6+
7+
interface LoadingOverlayProps {
8+
sourceName?: string;
9+
onCancel?: () => void;
10+
}
11+
12+
export const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ sourceName, onCancel }) => {
13+
const { formatMessage } = useIntl();
14+
15+
return (
16+
<div
17+
className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-background/50"
18+
role="status"
19+
aria-live="polite"
20+
aria-busy="true"
21+
>
22+
<Card className="pointer-events-auto w-full max-w-sm border-border shadow-none">
23+
<CardHeader className="gap-2 pb-4 text-center">
24+
<Spinner className="mx-auto size-8 text-primary" aria-hidden />
25+
<CardTitle className="text-base font-semibold tracking-tight">
26+
{formatMessage({ id: 'welcome.loadingTitle' })}
27+
</CardTitle>
28+
{sourceName ? (
29+
<p className="truncate text-xs text-muted-foreground" title={sourceName}>
30+
{sourceName}
31+
</p>
32+
) : null}
33+
<p className="text-xs text-muted-foreground">
34+
{formatMessage({ id: 'welcome.loadingPhase.preparing' })}
35+
</p>
36+
</CardHeader>
37+
{onCancel ? (
38+
<CardFooter className="justify-center pt-0">
39+
<Button type="button" variant="outline" size="sm" onClick={onCancel}>
40+
{formatMessage({ id: 'welcome.cancelLoading' })}
41+
</Button>
42+
</CardFooter>
43+
) : null}
44+
</Card>
45+
</div>
46+
);
47+
};

src/features/workspace/common/WelcomeScreen.tsx

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,32 @@
11
import React from 'react';
2-
import { Link2 } from 'lucide-react';
32
import { useIntl } from 'react-intl';
43
import type { SampleDataset } from '@/services/sampleDatasets';
54
import type { DatasetHistoryListItem } from '@/shared/utils/datasetHistory';
65
import { useSampleDatasets } from '@/hooks/useSampleDatasets';
76
import { DatasetSourceSelector } from '@/features/workspace/welcome/DatasetSourceSelector';
87
import { SampleDatasetList } from '@/features/workspace/welcome/SampleDatasetList';
9-
import { Button } from '@/shared/ui/button';
108
import { Separator } from '@/shared/ui/separator';
11-
import { Spinner } from '@/shared/ui/spinner';
129

1310
interface WelcomeScreenProps {
14-
isLoading?: boolean;
15-
loadingSourceName?: string;
1611
manualOpenHint?: string | null;
1712
onOpenFile: () => void;
1813
onOpenDirectory: () => void;
1914
onOpenTarPicker: () => void;
2015
onSubmitRemoteUrl: (url: string) => void | Promise<void>;
2116
remoteSubmitLoading?: boolean;
2217
onSelectSample: (sample: SampleDataset) => void | Promise<void>;
23-
onRequestChangeRemoteUrl?: () => void;
2418
historyItems?: DatasetHistoryListItem[];
2519
onReplayHistory?: (id: string) => void | Promise<void>;
2620
}
2721

2822
export const WelcomeScreen: React.FC<WelcomeScreenProps> = ({
29-
isLoading,
30-
loadingSourceName,
3123
manualOpenHint,
3224
onOpenFile,
3325
onOpenDirectory,
3426
onOpenTarPicker,
3527
onSubmitRemoteUrl,
3628
remoteSubmitLoading,
3729
onSelectSample,
38-
onRequestChangeRemoteUrl,
3930
historyItems = [],
4031
onReplayHistory,
4132
}) => {
@@ -45,31 +36,6 @@ export const WelcomeScreen: React.FC<WelcomeScreenProps> = ({
4536
/** Show samples column while loading or when any samples exist; hide when none configured after load. */
4637
const showSamplesSection = samplesLoading || hasSamples;
4738

48-
if (isLoading) {
49-
return (
50-
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-8 bg-background px-6 py-16">
51-
<div className="w-full max-w-sm rounded-xl border border-border bg-card px-8 py-12 text-center shadow-sm">
52-
<Spinner className="mx-auto mb-5 size-10 text-primary" aria-hidden />
53-
<h2 className="text-lg font-semibold tracking-tight text-foreground">
54-
{formatMessage({ id: 'welcome.loadingTitle' })}
55-
</h2>
56-
{loadingSourceName ? (
57-
<p className="mx-auto mt-3 max-w-full truncate text-xs text-muted-foreground" title={loadingSourceName}>
58-
{loadingSourceName}
59-
</p>
60-
) : null}
61-
<p className="mt-4 text-sm text-muted-foreground">{formatMessage({ id: 'welcome.loadingHint' })}</p>
62-
</div>
63-
{onRequestChangeRemoteUrl ? (
64-
<Button type="button" variant="link" className="text-sm" onClick={onRequestChangeRemoteUrl}>
65-
<Link2 data-icon="inline-start" aria-hidden />
66-
{formatMessage({ id: 'welcome.changeUrl' })}
67-
</Button>
68-
) : null}
69-
</div>
70-
);
71-
}
72-
7339
return (
7440
<div className="relative flex min-h-0 w-full min-w-0 flex-1 flex-col overflow-hidden bg-background">
7541
<div className="relative mx-auto flex min-h-[100dvh] w-full min-w-0 max-w-6xl flex-1 flex-col overflow-y-auto px-4 pb-12 pt-8 sm:px-6 sm:pb-16 sm:pt-12 lg:pb-20 lg:pt-14">

0 commit comments

Comments
 (0)