Skip to content

Commit 0a45777

Browse files
authored
Add swimlane view to vortex-web that visualizes physical layout of columns in files (#8634)
This code mostly existed before. Swimlane is a new top level tab alongside current view as it replicates the schema of the table to render the lanes
1 parent d9ded20 commit 0a45777

20 files changed

Lines changed: 683 additions & 238 deletions

vortex-web/.storybook/preview.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { createElement } from 'react';
55
import type { Preview } from '@storybook/react-vite';
6-
import { ThemeContext } from '../src/contexts/ThemeContextCore';
6+
import { ThemeProvider } from '../src/contexts/ThemeContext';
77
import '../src/index.css';
88

99
const preview: Preview = {
@@ -26,16 +26,13 @@ const preview: Preview = {
2626
},
2727
decorators: [
2828
(Story, context) => {
29-
const theme = context.globals.theme || 'light';
30-
document.documentElement.classList.toggle('dark', theme === 'dark');
31-
document.documentElement.classList.toggle('light', theme === 'light');
32-
// Supply the theme context so components using useTheme render in stories,
33-
// following the Storybook theme toolbar rather than persisted preferences.
34-
return createElement(
35-
ThemeContext.Provider,
36-
{ value: { theme, setTheme: () => {} } },
37-
Story(),
38-
);
29+
// Wrap every story in the real ThemeProvider so theme-aware components
30+
// (e.g. FileHeader/ThemePicker, which call useTheme) have their context.
31+
// Seed it from the toolbar's theme global; `key` remounts the provider
32+
// when the toolbar theme changes so the switch takes effect.
33+
const theme = (context.globals.theme as string) || 'light';
34+
localStorage.setItem('vortex-theme', theme);
35+
return createElement(ThemeProvider, { key: theme }, Story());
3936
},
4037
],
4138
parameters: {

vortex-web/src/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { LayoutTreeNode } from './components/swimlane/types';
99
import { arrayTreeToLayoutChildren, findNodeById } from './components/swimlane/utils';
1010
import { FileDropScreen } from './components/explorer/FileDropScreen';
1111
import { FileHeader } from './components/explorer/FileHeader';
12-
import { MainArea } from './components/explorer/MainArea';
12+
import { MainArea, type MainView } from './components/explorer/MainArea';
1313
import { StatusBar } from './components/explorer/StatusBar';
1414
import { VortexWorker } from './workers/VortexWorker';
1515

@@ -18,6 +18,7 @@ function App() {
1818
const [error, setError] = useState<string | null>(null);
1919
const [loading, setLoading] = useState(false);
2020
const [isDragging, setIsDragging] = useState(false);
21+
const [view, setView] = useState<MainView>('details');
2122
const dragCounter = useRef(0);
2223
const workerRef = useRef<VortexWorker | null>(null);
2324

@@ -177,8 +178,8 @@ function App() {
177178
onDragLeave={handleDragLeave}
178179
onDrop={handleDrop}
179180
>
180-
<FileHeader onClose={closeFile} />
181-
<MainArea />
181+
<FileHeader onClose={closeFile} view={view} onViewChange={setView} />
182+
<MainArea view={view} />
182183
<StatusBar />
183184
{isDragging && (
184185
<div className="absolute inset-0 z-50 flex items-center justify-center bg-vortex-black/50 dark:bg-black/50 backdrop-blur-sm pointer-events-none">

vortex-web/src/components/detail/DetailPanel.stories.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,16 @@ const mockFileState: VortexFileState = {
2626

2727
const meta: Meta<typeof DetailPanel> = {
2828
component: DetailPanel,
29-
decorators: [withMockFileContext(mockFileState), withMockSelection(layout)],
29+
decorators: [
30+
withMockFileContext(mockFileState),
31+
withMockSelection(layout),
32+
// DetailPanel fills its parent's height; give it a bounded flex container.
33+
(Story) => (
34+
<div className="flex" style={{ height: 520 }}>
35+
<Story />
36+
</div>
37+
),
38+
],
3039
parameters: {
3140
layout: 'padded',
3241
},
@@ -35,4 +44,5 @@ export default meta;
3544

3645
type Story = StoryObj<typeof DetailPanel>;
3746

47+
/** No node selected — the panel shows its empty state. */
3848
export const NoSelection: Story = {};

vortex-web/src/components/detail/DetailPanel.tsx

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import {
1111
shortEncoding,
1212
DTYPE_COLORS,
1313
} from '../swimlane/utils';
14-
import { SummaryPane } from './SummaryPane';
15-
import { ArraySummaryPane } from './ArraySummaryPane';
14+
import { SummarySidebar } from './SummarySidebar';
1615
import { EncodingPane } from './EncodingPane';
1716
import { SegmentsPane } from './SegmentsPane';
1817
import { BlockTreemap } from '../explorer/BlockTreemap';
@@ -66,7 +65,8 @@ export function DetailPanel() {
6665
const selectedIdSet = useMemo(() => new Set(selectedPath.map((n) => n.id)), [selectedPath]);
6766
const isHoverBreadcrumb = hoveredPath.length > 0;
6867

69-
const currentTab = tabs.find((t) => t.id === activeTab) ? activeTab : tabs[0]?.id;
68+
// Prefer the active tab; otherwise fall back to the first available tab.
69+
const currentTab = tabs.find((t) => t.id === activeTab)?.id ?? tabs[0]?.id;
7070

7171
return (
7272
<div className="flex flex-col flex-1 min-h-0 h-full bg-vortex-white dark:bg-vortex-black">
@@ -144,7 +144,6 @@ export function DetailPanel() {
144144

145145
{/* Main content: tab content (left) + summary sidebar (right) */}
146146
<div className="flex flex-1 min-h-0 overflow-hidden">
147-
{/* Tab content */}
148147
<div className="flex-1 overflow-auto min-w-0">
149148
{currentTab === 'encoding' && selection.selectedNode && (
150149
<div className="p-2.5">
@@ -169,21 +168,15 @@ export function DetailPanel() {
169168
{currentTab === 'buffers' && selection.selectedNode && (
170169
<BuffersPane node={selection.selectedNode} />
171170
)}
172-
{!currentTab && !selection.selectedNode && (
171+
{!selection.selectedNode && (
173172
<div className="p-2.5 text-xs text-vortex-grey-dark">
174173
Select a node to view details.
175174
</div>
176175
)}
177176
</div>
178177

179178
{/* Summary sidebar — always visible */}
180-
<div className="w-[180px] flex-shrink-0 overflow-y-auto border-l border-vortex-grey-light/40 dark:border-white/[0.06] p-2.5">
181-
{isArrayNode && selection.selectedNode ? (
182-
<ArraySummaryPane node={selection.selectedNode} />
183-
) : (
184-
<SummaryPane node={selection.selectedNode} file={file} />
185-
)}
186-
</div>
179+
<SummarySidebar />
187180
</div>
188181
</div>
189182
);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
import { useVortexFile } from '../../contexts/VortexFileContextCore';
5+
import { useSelection } from '../../contexts/SelectionContextCore';
6+
import { SummaryPane } from './SummaryPane';
7+
import { ArraySummaryPane } from './ArraySummaryPane';
8+
9+
/**
10+
* Right-hand summary sidebar shared by the details and swimlane views. Shows the
11+
* selected node's summary (array-aware), falling back to the whole-file summary
12+
* when nothing is selected.
13+
*/
14+
export function SummarySidebar() {
15+
const file = useVortexFile();
16+
const { state: selection } = useSelection();
17+
const isArrayNode = selection.selectedNode?.isArrayNode ?? false;
18+
19+
return (
20+
<div className="w-[180px] flex-shrink-0 overflow-y-auto border-l border-vortex-grey-light/40 dark:border-white/[0.06] p-2.5">
21+
{isArrayNode && selection.selectedNode ? (
22+
<ArraySummaryPane node={selection.selectedNode} />
23+
) : (
24+
<SummaryPane node={selection.selectedNode} file={file} />
25+
)}
26+
</div>
27+
);
28+
}

vortex-web/src/components/explorer/ExplorerShell.stories.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4+
import { useState } from 'react';
45
import type { Meta, StoryObj } from '@storybook/react-vite';
56
import { FileHeader } from './FileHeader';
6-
import { MainArea } from './MainArea';
7+
import { MainArea, type MainView } from './MainArea';
78
import { StatusBar } from './StatusBar';
89
import { withMockFileContext, withMockSelection } from '../../storybook/decorators';
910
import { ordersMock } from '../../mocks/layouts';
@@ -29,10 +30,11 @@ const mockFileState: VortexFileState = {
2930

3031
/** Full page layout — mirrors App.tsx when a file is loaded */
3132
function ExplorerPage() {
33+
const [view, setView] = useState<MainView>('details');
3234
return (
3335
<div className="flex flex-col h-screen bg-vortex-white dark:bg-vortex-black">
34-
<FileHeader onClose={() => {}} />
35-
<MainArea />
36+
<FileHeader onClose={() => {}} view={view} onViewChange={setView} />
37+
<MainArea view={view} />
3638
<StatusBar />
3739
</div>
3840
);

vortex-web/src/components/explorer/FileHeader.stories.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,6 @@ export default meta;
3232

3333
type Story = StoryObj<typeof FileHeader>;
3434

35-
export const Default: Story = {};
35+
export const Default: Story = {
36+
args: { view: 'details', onViewChange: () => {} },
37+
};

vortex-web/src/components/explorer/FileHeader.tsx

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33

44
import { useVortexFile } from '../../contexts/VortexFileContextCore';
55
import { ThemePicker } from '../ThemePicker';
6+
import type { MainView } from './MainArea';
67

78
interface FileHeaderProps {
89
onClose: () => void;
10+
view: MainView;
11+
onViewChange: (view: MainView) => void;
912
}
1013

11-
export function FileHeader({ onClose }: FileHeaderProps) {
14+
export function FileHeader({ onClose, view, onViewChange }: FileHeaderProps) {
1215
const file = useVortexFile();
1316

1417
return (
@@ -22,28 +25,47 @@ export function FileHeader({ onClose }: FileHeaderProps) {
2225
>
2326
v{file.version}
2427
</span>
25-
<div className="ml-auto flex items-center gap-1">
26-
<ThemePicker />
27-
<button
28-
onClick={onClose}
29-
className="p-1.5 rounded-md text-vortex-grey-dark hover:text-vortex-fg-light dark:hover:text-vortex-fg hover:bg-vortex-grey-lightest dark:hover:bg-white/[0.06] transition-colors cursor-pointer"
30-
title="Close file"
31-
aria-label="Close file"
32-
>
33-
<svg
34-
width="14"
35-
height="14"
36-
viewBox="0 0 24 24"
37-
fill="none"
38-
stroke="currentColor"
39-
strokeWidth="2"
40-
strokeLinecap="round"
41-
strokeLinejoin="round"
28+
<div className="ml-auto flex items-center gap-2">
29+
{/* Primary view switch — sits with the global controls in the header. */}
30+
<div className="flex rounded-md bg-vortex-grey-lightest dark:bg-white/[0.06] p-0.5">
31+
{(['details', 'swimlane'] as const).map((v) => (
32+
<button
33+
key={v}
34+
className={`px-3 py-0.5 text-[11px] rounded-[3px] transition-colors ${
35+
view === v
36+
? 'bg-white dark:bg-white/[0.1] text-vortex-fg-light dark:text-vortex-fg shadow-sm font-medium'
37+
: 'text-vortex-grey-dark hover:text-vortex-fg-light dark:hover:text-vortex-fg'
38+
}`}
39+
onClick={() => onViewChange(v)}
40+
>
41+
{v === 'details' ? 'Details' : 'Swimlane'}
42+
</button>
43+
))}
44+
</div>
45+
46+
<div className="flex items-center gap-1">
47+
<ThemePicker />
48+
<button
49+
onClick={onClose}
50+
className="p-1.5 rounded-md text-vortex-grey-dark hover:text-vortex-fg-light dark:hover:text-vortex-fg hover:bg-vortex-grey-lightest dark:hover:bg-white/[0.06] transition-colors cursor-pointer"
51+
title="Close file"
52+
aria-label="Close file"
4253
>
43-
<line x1="18" y1="6" x2="6" y2="18" />
44-
<line x1="6" y1="6" x2="18" y2="18" />
45-
</svg>
46-
</button>
54+
<svg
55+
width="14"
56+
height="14"
57+
viewBox="0 0 24 24"
58+
fill="none"
59+
stroke="currentColor"
60+
strokeWidth="2"
61+
strokeLinecap="round"
62+
strokeLinejoin="round"
63+
>
64+
<line x1="18" y1="6" x2="6" y2="18" />
65+
<line x1="6" y1="6" x2="18" y2="18" />
66+
</svg>
67+
</button>
68+
</div>
4769
</div>
4870
</div>
4971
);

vortex-web/src/components/explorer/MainArea.tsx

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@
33

44
import { useCallback, useRef, useState } from 'react';
55
import { TreePanel } from './TreePanel';
6-
import { FileMap } from './FileMap';
6+
import { SwimlaneOverview } from '../swimlane/SwimlaneOverview';
77
import { DataPreview } from './DataPreview';
88
import { DetailPanel } from '../detail/DetailPanel';
9+
import { SummarySidebar } from '../detail/SummarySidebar';
910

1011
const MIN_PANEL_HEIGHT = 120;
1112
const DEFAULT_PREVIEW_HEIGHT = 200;
1213

14+
export type MainView = 'details' | 'swimlane';
15+
1316
/**
14-
* Main explorer area: tree panel (left) | detail + filemap + preview (right).
17+
* Main explorer area: tree panel (left) | the active main view (details or
18+
* swimlane, chosen in the top header) over a resizable data preview (right). The
19+
* tree stays put as column navigation while the header tabs switch the view.
1520
*
1621
* The preview panel at the bottom is vertically resizable via a drag handle.
1722
*/
18-
export function MainArea() {
23+
export function MainArea({ view }: { view: MainView }) {
1924
const [previewHeight, setPreviewHeight] = useState(DEFAULT_PREVIEW_HEIGHT);
2025
const dragging = useRef(false);
2126
const startY = useRef(0);
@@ -48,19 +53,29 @@ export function MainArea() {
4853

4954
return (
5055
<div className="flex flex-1 min-h-0 overflow-hidden">
51-
{/* Left: tree panel — full height, fixed width */}
52-
<div className="w-[260px] flex-shrink-0 h-full overflow-hidden">
53-
<TreePanel />
54-
</div>
56+
{/* Left: tree panel — column navigation paired with the details view. The
57+
swimlane view hides it so the byte-bars get the full width. */}
58+
{view === 'details' && (
59+
<div className="w-[260px] flex-shrink-0 h-full overflow-hidden">
60+
<TreePanel />
61+
</div>
62+
)}
5563

56-
{/* Right: detail pane, file map, data preview — stacked vertically */}
64+
{/* Right: active main view over the resizable data preview */}
5765
<div ref={containerRef} className="flex-1 flex flex-col min-w-0 h-full overflow-hidden">
58-
{/* Detail pane — fills available vertical space, scrolls internally */}
59-
<DetailPanel />
60-
61-
{/* File map strip */}
62-
<div className="flex-shrink-0">
63-
<FileMap />
66+
{/* Active view — fills available vertical space, scrolls internally. The
67+
swimlane keeps the summary sidebar alongside it, like the details view. */}
68+
<div className="flex-1 min-h-0 flex overflow-hidden">
69+
{view === 'details' ? (
70+
<DetailPanel />
71+
) : (
72+
<>
73+
<div className="flex-1 min-w-0 flex flex-col">
74+
<SwimlaneOverview />
75+
</div>
76+
<SummarySidebar />
77+
</>
78+
)}
6479
</div>
6580

6681
{/* Resize handle */}

0 commit comments

Comments
 (0)