Skip to content

Commit c1f2d45

Browse files
committed
virtual cluster support
1 parent 27409f8 commit c1f2d45

File tree

7 files changed

+410
-92
lines changed

7 files changed

+410
-92
lines changed

src/components/DropZone.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ interface TableData {
1919
loaded?: boolean;
2020
loading?: boolean;
2121
sourceFile?: string;
22+
clusterName?: string;
23+
nodeFiles?: Array<{
24+
path: string;
25+
size: number;
26+
nodeId: number;
27+
isError: boolean;
28+
}>;
2229
}
2330

2431
interface FileStatus {
@@ -1132,10 +1139,13 @@ function DropZone() {
11321139
});
11331140

11341141
// Extract stack files from the file list (both stacks.txt and stacks_with_labels.txt)
1142+
// Exclude files in cluster/ directory
11351143
const stackFiles = entries
11361144
.filter(
11371145
(entry: ZipEntryMeta) =>
1138-
!entry.isDir && (entry.path.endsWith("stacks.txt") || entry.path.endsWith("stacks_with_labels.txt")),
1146+
!entry.isDir &&
1147+
(entry.path.endsWith("stacks.txt") || entry.path.endsWith("stacks_with_labels.txt")) &&
1148+
!entry.path.includes("/cluster/"),
11391149
)
11401150
.map((entry: ZipEntryMeta) => ({
11411151
path: entry.path,
@@ -1144,9 +1154,6 @@ function DropZone() {
11441154
}));
11451155

11461156
if (stackFiles.length > 0) {
1147-
console.log(
1148-
`🎯 Stack files found in file list: ${stackFiles.length} files`,
1149-
);
11501157
dispatch({ type: "SET_STACK_DATA", stackData: {} });
11511158
dispatch({ type: "SET_STACK_FILES", stackFiles });
11521159
dispatch({ type: "SET_STACKGAZER_READY", ready: false });
@@ -1480,9 +1487,6 @@ function DropZone() {
14801487
onStackProcessingComplete: (_stackFilesCount: number) => {
14811488
// Stack files have been loaded and sent to iframe
14821489
// stackData should already be populated via ADD_STACK_FILE actions
1483-
console.log(
1484-
`🎯 Stack processing complete: ${_stackFilesCount} files`,
1485-
);
14861490
dispatch({ type: "SET_STACKGAZER_READY", ready: true });
14871491
},
14881492
onFileList: (entries: ZipEntryMeta[]) => {
@@ -1500,10 +1504,13 @@ function DropZone() {
15001504
});
15011505

15021506
// Extract stack files from the file list (both stacks.txt and stacks_with_labels.txt)
1507+
// Exclude files in cluster/ directory
15031508
const stackFiles = entries
15041509
.filter(
15051510
(entry: ZipEntryMeta) =>
1506-
!entry.isDir && (entry.path.endsWith("stacks.txt") || entry.path.endsWith("stacks_with_labels.txt")),
1511+
!entry.isDir &&
1512+
(entry.path.endsWith("stacks.txt") || entry.path.endsWith("stacks_with_labels.txt")) &&
1513+
!entry.path.includes("/cluster/"),
15071514
)
15081515
.map((entry: ZipEntryMeta) => ({
15091516
path: entry.path,
@@ -1512,9 +1519,6 @@ function DropZone() {
15121519
}));
15131520

15141521
if (stackFiles.length > 0) {
1515-
console.log(
1516-
`🎯 Stack files found in file list: ${stackFiles.length} files`,
1517-
);
15181522
dispatch({ type: "SET_STACK_DATA", stackData: {} });
15191523
dispatch({ type: "SET_STACK_FILES", stackFiles });
15201524
dispatch({ type: "SET_STACKGAZER_READY", ready: false });

src/components/sidebar/StackgazerView.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ interface StackgazerViewProps {
1414
onCollapse?: () => void;
1515
}
1616

17-
type StackMode = "per-goroutine" | "labeled";
18-
1917
function StackgazerView({ onCollapse }: StackgazerViewProps) {
2018
const { state, dispatch } = useApp();
2119
const mode = state.stackgazerMode || "per-goroutine";

src/components/sidebar/TablesView.tsx

Lines changed: 206 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,31 @@ function formatFileSize(bytes: number): string {
1010
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
1111
}
1212

13-
function parseTableName(name: string): { prefix?: string; mainName: string } {
14-
// Check for system. or crdb_internal. prefix
13+
function parseTableName(name: string): {
14+
cluster?: string;
15+
prefix?: string;
16+
mainName: string;
17+
} {
18+
// Check for cluster.system. or cluster.crdb_internal. pattern
19+
// Examples: tenant1.system.jobs, mixed-version-tenant-ikhut.crdb_internal.active_range_feeds_by_node
20+
// Allow alphanumeric, underscores, and hyphens in cluster names
21+
const clusterMatch = name.match(/^([a-zA-Z0-9_-]+)\.(system|crdb_internal)\.(.+)$/);
22+
if (clusterMatch) {
23+
return {
24+
cluster: clusterMatch[1],
25+
prefix: clusterMatch[2],
26+
mainName: clusterMatch[3],
27+
};
28+
}
29+
30+
// Check for system. or crdb_internal. prefix (root cluster)
1531
if (name.startsWith("system.")) {
1632
return { prefix: "system", mainName: name.substring(7) };
1733
}
1834
if (name.startsWith("crdb_internal.")) {
1935
return { prefix: "crdb_internal", mainName: name.substring(14) };
2036
}
37+
2138
// Check for per-node schemas like n1_system. or n1_crdb_internal.
2239
if (name.match(/^n\d+_system\./)) {
2340
const dotIndex = name.indexOf(".");
@@ -33,6 +50,7 @@ function parseTableName(name: string): { prefix?: string; mainName: string } {
3350
mainName: name.substring(dotIndex + 1),
3451
};
3552
}
53+
3654
return { mainName: name };
3755
}
3856

@@ -153,13 +171,57 @@ function TablesView() {
153171
// Sort all tables by name
154172
const allTables = filteredTables.sort((a, b) => a.name.localeCompare(b.name));
155173

156-
// Separate zero-row tables from regular tables
157-
const regularTables = allTables.filter(
158-
(t) => !t.loaded || t.rowCount === undefined || t.rowCount > 0,
159-
);
160-
const emptyTables = allTables.filter(
161-
(t) => t.loaded && t.rowCount === 0,
162-
);
174+
// Group tables by cluster
175+
const tablesByCluster = useMemo(() => {
176+
const groups = new Map<string, typeof allTables>();
177+
178+
for (const table of allTables) {
179+
const { cluster } = parseTableName(table.name);
180+
const clusterKey = cluster || "_root"; // Use _root for root cluster
181+
182+
if (!groups.has(clusterKey)) {
183+
groups.set(clusterKey, []);
184+
}
185+
groups.get(clusterKey)!.push(table);
186+
}
187+
188+
return groups;
189+
}, [allTables]);
190+
191+
// Separate zero-row tables from regular tables (for each cluster)
192+
const clusterGroups = useMemo(() => {
193+
const groups: Array<{
194+
clusterKey: string;
195+
clusterName: string;
196+
regularTables: typeof allTables;
197+
emptyTables: typeof allTables;
198+
}> = [];
199+
200+
for (const [clusterKey, tables] of tablesByCluster.entries()) {
201+
const regularTables = tables.filter(
202+
(t) => !t.loaded || t.rowCount === undefined || t.rowCount > 0,
203+
);
204+
const emptyTables = tables.filter(
205+
(t) => t.loaded && t.rowCount === 0,
206+
);
207+
208+
groups.push({
209+
clusterKey,
210+
clusterName: clusterKey === "_root" ? "System Cluster" : clusterKey,
211+
regularTables,
212+
emptyTables,
213+
});
214+
}
215+
216+
// Sort: root cluster first, then others alphabetically
217+
groups.sort((a, b) => {
218+
if (a.clusterKey === "_root") return -1;
219+
if (b.clusterKey === "_root") return 1;
220+
return a.clusterName.localeCompare(b.clusterName);
221+
});
222+
223+
return groups;
224+
}, [tablesByCluster]);
163225

164226
// Auto-expand sections when filtering
165227
useEffect(() => {
@@ -446,8 +508,135 @@ function TablesView() {
446508
</div>
447509
)}
448510

449-
{/* Tables Section */}
450-
{(regularTables.length > 0 || emptyTables.length > 0) && (
511+
{/* Cluster Sections - each cluster is a top-level section when there are multiple clusters */}
512+
{clusterGroups.length > 1 ? (
513+
<>
514+
{clusterGroups.map((group) => (
515+
<div key={group.clusterKey} className="table-section">
516+
{/* Cluster Header */}
517+
<div
518+
className="section-header sub-header clickable"
519+
onClick={() => toggleSection(`cluster-${group.clusterKey}`)}
520+
>
521+
<span className="section-chevron">
522+
{collapsedSections.has(`cluster-${group.clusterKey}`) ? "▶" : "▼"}
523+
</span>
524+
{group.clusterName} ({group.regularTables.length + group.emptyTables.length})
525+
</div>
526+
527+
{/* Tables in this cluster */}
528+
{!collapsedSections.has(`cluster-${group.clusterKey}`) && (
529+
<>
530+
{group.regularTables.map((table) => {
531+
const { prefix, mainName } = parseTableName(table.name);
532+
const isHighlighted =
533+
navigation.state.isNavigating &&
534+
navigation.state.items[navigation.state.highlightedIndex]
535+
?.id === `table-${table.name}`;
536+
return (
537+
<div
538+
key={table.name}
539+
ref={(el) => registerElement(`table-${table.name}`, el)}
540+
className={`table-item-compact ${table.loading ? "loading" : ""} ${table.deferred ? "deferred" : ""} ${table.isError ? "error-file" : ""} ${table.loadError ? "load-failed" : ""} ${table.rowCount === 0 ? "empty-table" : ""} ${!table.loaded && !table.loading && !table.deferred ? "unloaded" : ""} ${isHighlighted ? "keyboard-highlighted" : ""}`}
541+
onClick={() => handleTableClick(table)}
542+
>
543+
{table.loaded &&
544+
table.rowCount !== undefined &&
545+
!table.isError &&
546+
!table.loadError &&
547+
!table.deferred && (
548+
<span className="table-row-count">
549+
{table.rowCount.toLocaleString()} rows
550+
</span>
551+
)}
552+
{table.loading && (
553+
<span className="loading-spinner-small" />
554+
)}
555+
<div className="table-name-compact">
556+
{prefix && <span className="table-prefix">{prefix}</span>}
557+
<span className="table-main-name">{mainName}</span>
558+
</div>
559+
{(table.isError || table.loadError || table.deferred) && (
560+
<div className="table-status-compact">
561+
{table.isError && (
562+
<span className="status-icon">⚠️</span>
563+
)}
564+
{table.loadError && (
565+
<span className="status-icon"></span>
566+
)}
567+
{table.deferred && (
568+
<span className="status-text">
569+
{formatFileSize(table.size || 0)}
570+
</span>
571+
)}
572+
</div>
573+
)}
574+
{/* Chunk progress bar */}
575+
{table.chunkProgress && (
576+
<div className="chunk-progress-bar">
577+
<div
578+
className="chunk-progress-fill"
579+
style={{
580+
width: `${table.chunkProgress.percentage}%`,
581+
height: "3px",
582+
backgroundColor: "#007acc",
583+
transition: "width 0.3s ease",
584+
}}
585+
/>
586+
</div>
587+
)}
588+
</div>
589+
);
590+
})}
591+
592+
{/* Empty Tables Section for this cluster */}
593+
{group.emptyTables.length > 0 && (
594+
<>
595+
<div
596+
className="subsection-header clickable"
597+
onClick={() => toggleSection(`empty-${group.clusterKey}`)}
598+
>
599+
<span className="section-chevron">
600+
{collapsedSections.has(`empty-${group.clusterKey}`) ? "▶" : "▼"}
601+
</span>
602+
Empty Tables ({group.emptyTables.length})
603+
</div>
604+
{!collapsedSections.has(`empty-${group.clusterKey}`) &&
605+
group.emptyTables.map((table) => {
606+
const { prefix, mainName } = parseTableName(table.name);
607+
const isHighlighted =
608+
navigation.state.isNavigating &&
609+
navigation.state.items[
610+
navigation.state.highlightedIndex
611+
]?.id === `table-${table.name}`;
612+
return (
613+
<div
614+
key={table.name}
615+
ref={(el) =>
616+
registerElement(`table-${table.name}`, el)
617+
}
618+
className={`table-item-compact empty-table ${isHighlighted ? "keyboard-highlighted" : ""}`}
619+
onClick={() => handleTableClick(table)}
620+
>
621+
<span className="table-row-count">0 rows</span>
622+
<div className="table-name-compact">
623+
{prefix && (
624+
<span className="table-prefix">{prefix}</span>
625+
)}
626+
<span className="table-main-name">{mainName}</span>
627+
</div>
628+
</div>
629+
);
630+
})}
631+
</>
632+
)}
633+
</>
634+
)}
635+
</div>
636+
))}
637+
</>
638+
) : clusterGroups.length === 1 ? (
639+
/* Single cluster - show tables directly under a "Tables" section */
451640
<div className="table-section">
452641
<div
453642
className="section-header sub-header clickable"
@@ -456,11 +645,11 @@ function TablesView() {
456645
<span className="section-chevron">
457646
{collapsedSections.has("tables") ? "▶" : "▼"}
458647
</span>
459-
Tables
648+
Tables ({clusterGroups[0].regularTables.length + clusterGroups[0].emptyTables.length})
460649
</div>
461650
{!collapsedSections.has("tables") && (
462651
<>
463-
{regularTables.map((table) => {
652+
{clusterGroups[0].regularTables.map((table) => {
464653
const { prefix, mainName } = parseTableName(table.name);
465654
const isHighlighted =
466655
navigation.state.isNavigating &&
@@ -504,7 +693,6 @@ function TablesView() {
504693
)}
505694
</div>
506695
)}
507-
{/* Chunk progress bar */}
508696
{table.chunkProgress && (
509697
<div className="chunk-progress-bar">
510698
<div
@@ -523,7 +711,7 @@ function TablesView() {
523711
})}
524712

525713
{/* Empty Tables Section */}
526-
{emptyTables.length > 0 && (
714+
{clusterGroups[0].emptyTables.length > 0 && (
527715
<>
528716
<div
529717
className="subsection-header clickable"
@@ -532,10 +720,10 @@ function TablesView() {
532720
<span className="section-chevron">
533721
{collapsedSections.has("empty") ? "▶" : "▼"}
534722
</span>
535-
Empty Tables ({emptyTables.length})
723+
Empty Tables ({clusterGroups[0].emptyTables.length})
536724
</div>
537725
{!collapsedSections.has("empty") &&
538-
emptyTables.map((table) => {
726+
clusterGroups[0].emptyTables.map((table) => {
539727
const { prefix, mainName } = parseTableName(table.name);
540728
const isHighlighted =
541729
navigation.state.isNavigating &&
@@ -566,7 +754,7 @@ function TablesView() {
566754
</>
567755
)}
568756
</div>
569-
)}
757+
) : null}
570758

571759
{/* Show message if everything is filtered out */}
572760
{filter &&

0 commit comments

Comments
 (0)