@@ -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 - z A - Z 0 - 9 _ - ] + ) \. ( s y s t e m | c r d b _ i n t e r n a l ) \. ( .+ ) $ / ) ;
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 + _ s y s t e m \. / ) ) {
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