Skip to content

Commit ba226f7

Browse files
committed
Add connector metadata and three-panel layout to catalog browser
Enrich CatalogInfo with connector type, properties (password-redacted), and database/table counts across Go backend and GraphQL schema. Redesign the Available Catalogs page from a flat tree into a three-panel resizable layout (catalog sidebar | database/table tree | column detail with DDL tab). Backend: add ConnectorType, Properties, DatabaseCount, TableCount to CatalogInfo, RedactProperties helper, TableDDL provider method (bundled generates from YAML, SQL Gateway runs SHOW CREATE TABLE), and catalogTableDDL GraphQL query. Frontend: new CatalogItem sidebar component with parsed summaries (e.g. "Postgresql @ host:port/db"), ResizablePanelGroup three-panel layout, tabbed detail panel (Columns + DDL with Shiki SQL highlighting).
1 parent 7cecf86 commit ba226f7

File tree

15 files changed

+933
-150
lines changed

15 files changed

+933
-150
lines changed

dashboard/src/components/catalogs/catalog-browser-page.tsx

Lines changed: 234 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,282 @@
11
/**
22
* @module catalog-browser-page
33
*
4-
* Top-level page for browsing Flink SQL catalogs. Fetches the catalog list
5-
* from the server on mount via {@link useCatalogStore} and renders a
6-
* navigable tree of catalogs, databases, and tables.
4+
* Three-panel catalog browser: catalog sidebar | database/table tree | column detail.
5+
* Uses resizable panels so the user can adjust proportions. Selecting a catalog
6+
* loads its databases in the middle panel; selecting a table shows columns in
7+
* the content panel.
78
*/
89

9-
import { Button, Spinner } from "@flink-reactor/ui"
10-
import { AlertTriangle, Database, RefreshCw } from "lucide-react"
11-
import { useEffect } from "react"
10+
import {
11+
ResizableHandle,
12+
ResizablePanel,
13+
ResizablePanelGroup,
14+
Spinner,
15+
Tabs,
16+
TabsContent,
17+
TabsList,
18+
TabsTrigger,
19+
} from "@flink-reactor/ui"
20+
import { AlertTriangle, Code2, Database, RefreshCw, Table2 } from "lucide-react"
21+
import { useCallback, useEffect, useState } from "react"
22+
import { cn } from "@/lib/cn"
1223
import { useCatalogStore } from "@/stores/catalog-store"
24+
import { CatalogItem } from "./catalog-card"
1325
import { CatalogTree } from "./catalog-tree"
26+
import { ColumnsTable } from "./columns-table"
27+
import { SqlHighlight } from "./sql-highlight"
1428

1529
/**
16-
* Catalog browser page with a refresh button, error display, and
17-
* the collapsible {@link CatalogTree}. Triggers initial catalog
18-
* fetch on mount.
30+
* Three-panel catalog browser page. Fetches catalogs on mount.
31+
*
32+
* Layout: [Catalogs sidebar] | [Database/table tree] | [Column detail]
1933
*/
2034
export function CatalogBrowserPage() {
2135
const loading = useCatalogStore((s) => s.loading)
2236
const error = useCatalogStore((s) => s.error)
37+
const catalogs = useCatalogStore((s) => s.catalogs)
2338
const fetchCatalogs = useCatalogStore((s) => s.fetchCatalogs)
39+
const toggleNode = useCatalogStore((s) => s.toggleNode)
40+
const expandedNodes = useCatalogStore((s) => s.expandedNodes)
41+
const columns = useCatalogStore((s) => s.columns)
42+
const ddl = useCatalogStore((s) => s.ddl)
43+
const fetchTableDDL = useCatalogStore((s) => s.fetchTableDDL)
44+
const loadingNodes = useCatalogStore((s) => s.loadingNodes)
45+
46+
const [selectedCatalog, setSelectedCatalog] = useState<string | null>(null)
47+
const [selectedTable, setSelectedTable] = useState<{
48+
catalog: string
49+
database: string
50+
table: string
51+
key: string
52+
} | null>(null)
2453

2554
useEffect(() => {
2655
fetchCatalogs()
2756
}, [fetchCatalogs])
2857

58+
const handleSelectCatalog = useCallback(
59+
(catalogName: string) => {
60+
setSelectedCatalog(catalogName)
61+
setSelectedTable(null)
62+
// Auto-expand databases if not already loaded
63+
if (!expandedNodes.has(catalogName)) {
64+
toggleNode(catalogName, catalogName)
65+
}
66+
},
67+
[expandedNodes, toggleNode],
68+
)
69+
70+
const handleSelectTable = useCallback(
71+
(catalog: string, database: string, table: string) => {
72+
const key = `${catalog}.${database}.${table}`
73+
setSelectedTable({ catalog, database, table, key })
74+
// Load columns if not already loaded
75+
if (!columns[key] && !loadingNodes.has(key)) {
76+
toggleNode(key, catalog, database, table)
77+
}
78+
// Fetch DDL in background
79+
fetchTableDDL(catalog, database, table)
80+
},
81+
[columns, loadingNodes, toggleNode, fetchTableDDL],
82+
)
83+
84+
const tableCols = selectedTable ? columns[selectedTable.key] : null
85+
const tableDdl = selectedTable ? ddl[selectedTable.key] : undefined
86+
const tableLoading = selectedTable
87+
? loadingNodes.has(selectedTable.key)
88+
: false
89+
2990
return (
30-
<div className="space-y-4 p-4">
31-
{/* Header */}
32-
<div className="flex items-center justify-between">
91+
<div className="flex h-full flex-col">
92+
{/* Header bar */}
93+
<div className="flex items-center justify-between border-b border-dash-border px-4 py-2">
3394
<div className="flex items-center gap-2">
3495
<Database className="size-4 text-zinc-400" />
3596
<h1 className="text-sm font-medium text-zinc-200">
3697
Available Catalogs
3798
</h1>
99+
{catalogs.length > 0 && (
100+
<span className="text-xs text-zinc-600">
101+
({catalogs.length})
102+
</span>
103+
)}
38104
</div>
39-
<Button
40-
size="sm"
41-
variant="outline"
105+
<button
106+
type="button"
42107
onClick={fetchCatalogs}
43108
disabled={loading}
44-
className="h-7 gap-1.5 text-xs"
109+
className={cn(
110+
"flex items-center gap-1.5 rounded-md border border-dash-border px-2.5 py-1 text-xs text-zinc-400 transition-colors",
111+
"hover:bg-white/[0.04] hover:text-zinc-300",
112+
"disabled:opacity-50",
113+
)}
45114
>
46-
<RefreshCw className={`size-3 ${loading ? "animate-spin" : ""}`} />
115+
<RefreshCw className={cn("size-3", loading && "animate-spin")} />
47116
Refresh
48-
</Button>
117+
</button>
49118
</div>
50119

51-
{/* Error */}
120+
{/* Error banner */}
52121
{error && (
53-
<div className="glass-card border-job-failed/20 bg-job-failed/5 p-3 text-sm text-job-failed">
122+
<div className="border-b border-job-failed/20 bg-job-failed/5 px-4 py-2 text-xs text-job-failed">
54123
<div className="flex items-center gap-2">
55-
<AlertTriangle className="size-4 shrink-0" />
124+
<AlertTriangle className="size-3.5 shrink-0" />
56125
<span>{error}</span>
57126
</div>
58127
</div>
59128
)}
60129

61-
{/* Loading */}
130+
{/* Loading state */}
62131
{loading && (
63-
<div className="flex items-center justify-center p-8">
132+
<div className="flex flex-1 items-center justify-center">
64133
<Spinner size="lg" />
65134
</div>
66135
)}
67136

68-
{/* Tree */}
137+
{/* Three-panel layout */}
69138
{!loading && (
70-
<div className="glass-card p-2">
71-
<CatalogTree />
72-
</div>
139+
<ResizablePanelGroup orientation="horizontal" className="flex-1">
140+
{/* Panel 1: Catalog list */}
141+
<ResizablePanel defaultSize="15%" minSize={180} maxSize="35%">
142+
<div className="flex h-full flex-col overflow-hidden">
143+
<div className="border-b border-dash-border px-3 py-2">
144+
<span className="text-[10px] font-medium uppercase tracking-wider text-zinc-600">
145+
Catalogs
146+
</span>
147+
</div>
148+
<div className="flex-1 overflow-y-auto p-1">
149+
{catalogs.length === 0 ? (
150+
<div className="p-4 text-center text-xs text-zinc-600">
151+
No catalogs found
152+
</div>
153+
) : (
154+
catalogs.map((catalog) => (
155+
<CatalogItem
156+
key={catalog.name}
157+
catalog={catalog}
158+
selected={selectedCatalog === catalog.name}
159+
onClick={() => handleSelectCatalog(catalog.name)}
160+
/>
161+
))
162+
)}
163+
</div>
164+
</div>
165+
</ResizablePanel>
166+
167+
<ResizableHandle />
168+
169+
{/* Panel 2: Database/table tree */}
170+
<ResizablePanel defaultSize="15%" minSize={180} maxSize="40%">
171+
<div className="flex h-full flex-col overflow-hidden">
172+
<div className="border-b border-dash-border px-3 py-2">
173+
<span className="text-[10px] font-medium uppercase tracking-wider text-zinc-600">
174+
{selectedCatalog ? (
175+
<>
176+
Tables —{" "}
177+
<span className="normal-case text-zinc-400">
178+
{selectedCatalog}
179+
</span>
180+
</>
181+
) : (
182+
"Tables"
183+
)}
184+
</span>
185+
</div>
186+
<div className="flex-1 overflow-y-auto">
187+
{selectedCatalog ? (
188+
<CatalogTree
189+
catalogName={selectedCatalog}
190+
selectedTable={selectedTable?.key}
191+
onSelectTable={handleSelectTable}
192+
/>
193+
) : (
194+
<div className="flex flex-col items-center justify-center gap-2 p-8 text-zinc-600">
195+
<Database className="size-6 text-zinc-700" />
196+
<span className="text-xs">Select a catalog</span>
197+
</div>
198+
)}
199+
</div>
200+
</div>
201+
</ResizablePanel>
202+
203+
<ResizableHandle />
204+
205+
{/* Panel 3: Table detail with tabs */}
206+
<ResizablePanel defaultSize="70%" minSize="25%">
207+
<div className="flex h-full flex-col overflow-hidden">
208+
{selectedTable ? (
209+
<Tabs defaultValue="columns" className="flex h-full flex-col">
210+
<div className="flex items-center gap-3 border-b border-dash-border px-3">
211+
<span className="font-mono text-[11px] text-zinc-400">
212+
{selectedTable.catalog}.{selectedTable.database}.
213+
<span className="text-zinc-200">
214+
{selectedTable.table}
215+
</span>
216+
</span>
217+
<TabsList className="detail-tabs-list">
218+
<TabsTrigger value="columns" className="detail-tab">
219+
Columns
220+
</TabsTrigger>
221+
<TabsTrigger value="ddl" className="detail-tab">
222+
<Code2 className="mr-1 size-3" />
223+
DDL
224+
</TabsTrigger>
225+
</TabsList>
226+
</div>
227+
228+
<TabsContent
229+
value="columns"
230+
className="flex-1 overflow-y-auto p-2 data-[state=inactive]:hidden"
231+
>
232+
{tableLoading && (
233+
<div className="flex items-center justify-center p-8">
234+
<Spinner size="sm" />
235+
</div>
236+
)}
237+
{!tableLoading && tableCols && tableCols.length > 0 && (
238+
<ColumnsTable columns={tableCols} />
239+
)}
240+
{!tableLoading && tableCols && tableCols.length === 0 && (
241+
<div className="p-8 text-center text-xs text-zinc-600">
242+
No columns
243+
</div>
244+
)}
245+
</TabsContent>
246+
247+
<TabsContent
248+
value="ddl"
249+
className="flex-1 overflow-y-auto p-2 data-[state=inactive]:hidden"
250+
>
251+
{tableDdl !== undefined ? (
252+
tableDdl ? (
253+
<SqlHighlight
254+
code={tableDdl}
255+
className="rounded-md border border-dash-border bg-dash-surface/50 p-4 text-xs [&_pre]:!bg-transparent"
256+
/>
257+
) : (
258+
<div className="p-8 text-center text-xs text-zinc-600">
259+
No DDL available
260+
</div>
261+
)
262+
) : (
263+
<div className="flex items-center justify-center p-8">
264+
<Spinner size="sm" />
265+
</div>
266+
)}
267+
</TabsContent>
268+
</Tabs>
269+
) : (
270+
<div className="flex h-full flex-col items-center justify-center gap-2 text-zinc-600">
271+
<Table2 className="size-6 text-zinc-700" />
272+
<span className="text-xs">
273+
Select a table to view its schema
274+
</span>
275+
</div>
276+
)}
277+
</div>
278+
</ResizablePanel>
279+
</ResizablePanelGroup>
73280
)}
74281
</div>
75282
)

0 commit comments

Comments
 (0)