Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ioai/rosview",
"version": "1.3.2",
"version": "1.3.3",
"description": "High-performance robotics data visualization for MCAP, ROS bag, ROS2 db3, HDF5 and BVH — embeddable React component and standalone SPA",
"keywords": [
"ros",
Expand Down
4 changes: 3 additions & 1 deletion src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ interface AppShellProps {
onSubmitRemoteUrl: (url: string) => void | Promise<void>;
remoteSubmitLoading?: boolean;
onSelectSample: (sample: SampleDataset) => void | Promise<void>;
onCancelLoading?: () => void;
historyItems: DatasetHistoryListItem[];
onReplayHistory: (id: string) => void | Promise<void>;
onDropRosRecordingFiles: (files: File[], items?: DataTransferItemList) => void | Promise<void>;
Expand Down Expand Up @@ -91,6 +92,7 @@ export const AppShell: React.FC<AppShellProps> = ({
onSubmitRemoteUrl,
remoteSubmitLoading,
onSelectSample,
onCancelLoading,
historyItems,
onReplayHistory,
onDropRosRecordingFiles,
Expand Down Expand Up @@ -175,7 +177,7 @@ export const AppShell: React.FC<AppShellProps> = ({
onSubmitRemoteUrl={onSubmitRemoteUrl}
remoteSubmitLoading={remoteSubmitLoading}
onSelectSample={onSelectSample}
onRequestChangeRemoteUrl={onOpenRemotePrompt}
onCancelLoading={onCancelLoading}
historyItems={historyItems}
onReplayHistory={onReplayHistory}
onDropRosRecordingFiles={onDropRosRecordingFiles}
Expand Down
56 changes: 34 additions & 22 deletions src/features/viewer/RosViewContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
import { openRawMessagesPanel } from '@/features/workspace/sidebar/topic-list/openRawMessagesPanel';
import { useIntl } from 'react-intl';
import { WelcomeScreen } from '@/features/workspace/common/WelcomeScreen';
import { LoadingOverlay } from '@/features/workspace/common/LoadingOverlay';
import { useMessagePipeline } from '@/core/pipeline/useMessagePipeline';
import type { MessagePipelineState } from '@/core/pipeline/store';
import { useMessagePipelineStore } from '@/core/pipeline/store';
import type { SampleDataset } from '@/services/sampleDatasets';
import type { DatasetHistoryListItem } from '@/shared/utils/datasetHistory';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/shared/ui/resizable';
import { Skeleton } from '@/shared/ui/skeleton';
import type { RosViewExtension } from '@/core/extensions/types';
import { buildExtensionContext } from '@/core/extensions/buildContext';
import type { FoxgloveLayoutData } from '@/core/preferences/foxgloveLayout';
Expand Down Expand Up @@ -49,7 +51,7 @@ interface RosViewContentProps {
onSubmitRemoteUrl: (url: string) => void | Promise<void>;
remoteSubmitLoading?: boolean;
onSelectSample: (sample: SampleDataset) => void | Promise<void>;
onRequestChangeRemoteUrl?: () => void;
onCancelLoading?: () => void;
historyItems: DatasetHistoryListItem[];
onReplayHistory: (id: string) => void | Promise<void>;
onDropRosRecordingFiles: (files: File[], items?: DataTransferItemList) => void | Promise<void>;
Expand Down Expand Up @@ -89,7 +91,7 @@ export const RosViewContent: React.FC<RosViewContentProps> = ({
onSubmitRemoteUrl,
remoteSubmitLoading,
onSelectSample,
onRequestChangeRemoteUrl,
onCancelLoading,
historyItems,
onReplayHistory,
onDropRosRecordingFiles,
Expand All @@ -112,8 +114,8 @@ export const RosViewContent: React.FC<RosViewContentProps> = ({
const { formatMessage } = useIntl();
const presence = useMessagePipeline((state: MessagePipelineState) => state.playerState.presence);
const sortedTopics = useMessagePipeline((state: MessagePipelineState) => state.sortedTopics);
const isLoading = presence === 'initializing';
const isReady = presence === 'ready';
const showWelcomeFallback = !isReady && Boolean(manualOpenHint);
const topicDragDepthRef = useRef(0);
const [sidebarPanelPercent, setSidebarPanelPercent] = useState(() =>
getInitialSidebarPanelPercent(preferencePersistence),
Expand Down Expand Up @@ -251,19 +253,16 @@ export const RosViewContent: React.FC<RosViewContentProps> = ({

return (
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
{!isReady ? (
{showWelcomeFallback ? (
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<WelcomeScreen
isLoading={isLoading}
loadingSourceName={loadingSourceName}
manualOpenHint={manualOpenHint}
onOpenFile={onOpenFilePick}
onOpenDirectory={onOpenDirectory}
onOpenTarPicker={onOpenTarPick}
onSubmitRemoteUrl={onSubmitRemoteUrl}
remoteSubmitLoading={remoteSubmitLoading}
onSelectSample={onSelectSample}
onRequestChangeRemoteUrl={onRequestChangeRemoteUrl}
historyItems={historyItems}
onReplayHistory={onReplayHistory}
/>
Expand Down Expand Up @@ -334,10 +333,10 @@ export const RosViewContent: React.FC<RosViewContentProps> = ({
className={`relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-background [contain:strict] ${
isTopicDragOver ? 'ring-1 ring-inset ring-primary/40' : ''
}`}
onDragEnter={handleTopicDragEnter}
onDragOver={handleMainDragOver}
onDragLeave={handleTopicDragLeave}
onDrop={handleMainDrop}
onDragEnter={isReady ? handleTopicDragEnter : undefined}
onDragOver={isReady ? handleMainDragOver : undefined}
onDragLeave={isReady ? handleTopicDragLeave : undefined}
onDrop={isReady ? handleMainDrop : undefined}
>
{isTopicDragOver && (
<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">
Expand All @@ -351,17 +350,30 @@ export const RosViewContent: React.FC<RosViewContentProps> = ({
</div>
</div>
)}
<DockviewLayout
key={activeDatasetId ?? 'dataset'}
player={player}
preferAutoLayout={preferAutoLayout}
initialLayout={initialLayout}
defaultPanel={defaultPanel}
layoutPersistence={layoutPersistence}
layoutStorageKey={layoutStorageKey}
suppressWelcomePanel={suppressWelcomePanel}
onLayoutReady={onLayoutReady}
/>
{isReady ? (
<DockviewLayout
key={activeDatasetId ?? 'dataset'}
player={player}
preferAutoLayout={preferAutoLayout}
initialLayout={initialLayout}
defaultPanel={defaultPanel}
layoutPersistence={layoutPersistence}
layoutStorageKey={layoutStorageKey}
suppressWelcomePanel={suppressWelcomePanel}
onLayoutReady={onLayoutReady}
/>
) : (
<div className="flex min-h-0 flex-1 flex-col gap-3 p-4">
<Skeleton className="h-8 w-40" />
<Skeleton className="min-h-0 flex-1 rounded-lg" />
</div>
)}
{!isReady ? (
<LoadingOverlay
sourceName={loadingSourceName}
onCancel={onCancelLoading}
/>
) : null}
</main>
</ResizablePanel>
</ResizablePanelGroup>
Expand Down
54 changes: 36 additions & 18 deletions src/features/viewer/RosViewerImpl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { SampleDatasetDialog } from '@/features/workspace/common/SampleDatasetDi
import { getArchiveUrl, getSampleDatasetsManifestUrl, loadSampleDatasets } from '@/services/sampleDatasets';
import type { SampleDataset } from '@/services/sampleDatasets';
import { extractRosFilesFromTarArchive } from '@/shared/utils/tarRosRecordings';
import { WorkerSerializedSource } from '@/infra/workers/WorkerSerializedSource';
import { WorkerSerializedSource, isWorkerSourceCancelledError } from '@/infra/workers/WorkerSerializedSource';
import { IterablePlayer } from '@/core/players/IterablePlayer';
import { MinimalPlayer } from '@/core/players/MinimalPlayer';
import type { Player } from '@/core/types/player';
import { useMessagePipelineStore } from '@/core/pipeline/store';
import { Navbar } from '@/features/workspace/navbar/Navbar';
import { WelcomeScreen } from '@/features/workspace/common/WelcomeScreen';
import { LoadingOverlay } from '@/features/workspace/common/LoadingOverlay';
import { Skeleton } from '@/shared/ui/skeleton';
import type { DatasetItem, FileListItem } from '@/shared/utils/datasetSources';
import {
datasetItemsFromListItems,
Expand Down Expand Up @@ -771,6 +773,11 @@ export const RosViewer: React.FC<RosViewerProps> = (props) => {
}
return;
} catch (err) {
if (cancelled || isWorkerSourceCancelledError(err)) {
newPlayer.close();
createdPlayer = null;
return;
}
lastErr = err;
newPlayer.close();
createdPlayer = null;
Expand Down Expand Up @@ -1166,6 +1173,7 @@ export const RosViewer: React.FC<RosViewerProps> = (props) => {
onSubmitRemoteUrl={handleOpenRemoteRecordingUrl}
remoteSubmitLoading={remoteUrlBusy}
onSelectSample={handleSelectSample}
onCancelLoading={handleGoHome}
historyItems={historyItems}
onReplayHistory={(id) => void handleReplayHistory(id)}
onDropRosRecordingFiles={handleDropRosRecordingFiles}
Expand Down Expand Up @@ -1326,23 +1334,33 @@ export const RosViewer: React.FC<RosViewerProps> = (props) => {
recentHistoryItems={historyItems.slice(0, 10)}
onReplayHistory={(id) => void handleReplayHistory(id)}
/>
<WelcomeScreen
isLoading={!lastLoadError && !manualOpenHint}
loadingSourceName={loadingSourceName}
manualOpenHint={manualOpenHint}
onOpenFile={() => {
clearOpenFeedback();
void handleOpenRecordingFiles();
}}
onOpenDirectory={handleOpenDirectory}
onOpenTarPicker={() => document.getElementById('rosview-inline-tar')?.click()}
onSubmitRemoteUrl={handleOpenRemoteRecordingUrl}
remoteSubmitLoading={remoteUrlBusy}
onSelectSample={handleSelectSample}
onRequestChangeRemoteUrl={openRemotePrompt}
historyItems={historyItems}
onReplayHistory={(id) => void handleReplayHistory(id)}
/>
{!lastLoadError && !manualOpenHint ? (
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<div className="flex min-h-0 flex-1 flex-col gap-3 p-4">
<Skeleton className="h-8 w-40" />
<Skeleton className="min-h-0 flex-1 rounded-lg" />
</div>
<LoadingOverlay
sourceName={loadingSourceName}
onCancel={handleGoHome}
/>
</div>
) : (
<WelcomeScreen
manualOpenHint={manualOpenHint}
onOpenFile={() => {
clearOpenFeedback();
void handleOpenRecordingFiles();
}}
onOpenDirectory={handleOpenDirectory}
onOpenTarPicker={() => document.getElementById('rosview-inline-tar')?.click()}
onSubmitRemoteUrl={handleOpenRemoteRecordingUrl}
remoteSubmitLoading={remoteUrlBusy}
onSelectSample={handleSelectSample}
historyItems={historyItems}
onReplayHistory={(id) => void handleReplayHistory(id)}
/>
)}
<SampleDatasetDialog
open={sampleDialogOpen}
onOpenChange={setSampleDialogOpen}
Expand Down
47 changes: 47 additions & 0 deletions src/features/workspace/common/LoadingOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { useIntl } from 'react-intl';
import { Button } from '@/shared/ui/button';
import { Card, CardFooter, CardHeader, CardTitle } from '@/shared/ui/card';
import { Spinner } from '@/shared/ui/spinner';

interface LoadingOverlayProps {
sourceName?: string;
onCancel?: () => void;
}

export const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ sourceName, onCancel }) => {
const { formatMessage } = useIntl();

return (
<div
className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-background/50"
role="status"
aria-live="polite"
aria-busy="true"
>
<Card className="pointer-events-auto w-full max-w-sm border-border shadow-none">
<CardHeader className="gap-2 pb-4 text-center">
<Spinner className="mx-auto size-8 text-primary" aria-hidden />
<CardTitle className="text-base font-semibold tracking-tight">
{formatMessage({ id: 'welcome.loadingTitle' })}
</CardTitle>
{sourceName ? (
<p className="truncate text-xs text-muted-foreground" title={sourceName}>
{sourceName}
</p>
) : null}
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'welcome.loadingPhase.preparing' })}
</p>
</CardHeader>
{onCancel ? (
<CardFooter className="justify-center pt-0">
<Button type="button" variant="outline" size="sm" onClick={onCancel}>
{formatMessage({ id: 'welcome.cancelLoading' })}
</Button>
</CardFooter>
) : null}
</Card>
</div>
);
};
34 changes: 0 additions & 34 deletions src/features/workspace/common/WelcomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,32 @@
import React from 'react';
import { Link2 } from 'lucide-react';
import { useIntl } from 'react-intl';
import type { SampleDataset } from '@/services/sampleDatasets';
import type { DatasetHistoryListItem } from '@/shared/utils/datasetHistory';
import { useSampleDatasets } from '@/hooks/useSampleDatasets';
import { DatasetSourceSelector } from '@/features/workspace/welcome/DatasetSourceSelector';
import { SampleDatasetList } from '@/features/workspace/welcome/SampleDatasetList';
import { Button } from '@/shared/ui/button';
import { Separator } from '@/shared/ui/separator';
import { Spinner } from '@/shared/ui/spinner';

interface WelcomeScreenProps {
isLoading?: boolean;
loadingSourceName?: string;
manualOpenHint?: string | null;
onOpenFile: () => void;
onOpenDirectory: () => void;
onOpenTarPicker: () => void;
onSubmitRemoteUrl: (url: string) => void | Promise<void>;
remoteSubmitLoading?: boolean;
onSelectSample: (sample: SampleDataset) => void | Promise<void>;
onRequestChangeRemoteUrl?: () => void;
historyItems?: DatasetHistoryListItem[];
onReplayHistory?: (id: string) => void | Promise<void>;
}

export const WelcomeScreen: React.FC<WelcomeScreenProps> = ({
isLoading,
loadingSourceName,
manualOpenHint,
onOpenFile,
onOpenDirectory,
onOpenTarPicker,
onSubmitRemoteUrl,
remoteSubmitLoading,
onSelectSample,
onRequestChangeRemoteUrl,
historyItems = [],
onReplayHistory,
}) => {
Expand All @@ -45,31 +36,6 @@ export const WelcomeScreen: React.FC<WelcomeScreenProps> = ({
/** Show samples column while loading or when any samples exist; hide when none configured after load. */
const showSamplesSection = samplesLoading || hasSamples;

if (isLoading) {
return (
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-8 bg-background px-6 py-16">
<div className="w-full max-w-sm rounded-xl border border-border bg-card px-8 py-12 text-center shadow-sm">
<Spinner className="mx-auto mb-5 size-10 text-primary" aria-hidden />
<h2 className="text-lg font-semibold tracking-tight text-foreground">
{formatMessage({ id: 'welcome.loadingTitle' })}
</h2>
{loadingSourceName ? (
<p className="mx-auto mt-3 max-w-full truncate text-xs text-muted-foreground" title={loadingSourceName}>
{loadingSourceName}
</p>
) : null}
<p className="mt-4 text-sm text-muted-foreground">{formatMessage({ id: 'welcome.loadingHint' })}</p>
</div>
{onRequestChangeRemoteUrl ? (
<Button type="button" variant="link" className="text-sm" onClick={onRequestChangeRemoteUrl}>
<Link2 data-icon="inline-start" aria-hidden />
{formatMessage({ id: 'welcome.changeUrl' })}
</Button>
) : null}
</div>
);
}

return (
<div className="relative flex min-h-0 w-full min-w-0 flex-1 flex-col overflow-hidden bg-background">
<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">
Expand Down
Loading
Loading