dbdiff-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/bin/cli.js +83 -0
- package/bin/install-local.js +57 -0
- package/electron/generate-icon.mjs +54 -0
- package/electron/icon.icns +0 -0
- package/electron/icon.png +0 -0
- package/electron/icon.svg +21 -0
- package/electron/main.js +169 -0
- package/electron/patch-dev-plist.js +31 -0
- package/electron/preload.cjs +18 -0
- package/electron/wait-for-vite.js +43 -0
- package/index.html +13 -0
- package/package.json +91 -0
- package/public/favicon.svg +15 -0
- package/public/vite.svg +1 -0
- package/server/export.ts +57 -0
- package/server/index.ts +392 -0
- package/src/App.css +1 -0
- package/src/App.tsx +543 -0
- package/src/assets/react.svg +1 -0
- package/src/components/CommandPalette.tsx +243 -0
- package/src/components/ConnectedView.tsx +78 -0
- package/src/components/ConnectionPicker.tsx +381 -0
- package/src/components/ConsoleView.tsx +360 -0
- package/src/components/CsvExportModal.tsx +144 -0
- package/src/components/DataGrid/DataGrid.tsx +262 -0
- package/src/components/DataGrid/DataGridCell.tsx +73 -0
- package/src/components/DataGrid/DataGridHeader.tsx +89 -0
- package/src/components/DataGrid/index.ts +20 -0
- package/src/components/DataGrid/types.ts +63 -0
- package/src/components/DataGrid/useColumnResize.ts +153 -0
- package/src/components/DataGrid/useDataGridSelection.ts +340 -0
- package/src/components/DataGrid/utils.ts +184 -0
- package/src/components/DatabaseMenu.tsx +93 -0
- package/src/components/DatabaseSwitcher.tsx +208 -0
- package/src/components/DiffView.tsx +215 -0
- package/src/components/EditConnectionModal.tsx +417 -0
- package/src/components/ErrorBoundary.tsx +69 -0
- package/src/components/GlobalShortcuts.tsx +201 -0
- package/src/components/InnerTabBar.tsx +129 -0
- package/src/components/JsonTreeViewer.tsx +387 -0
- package/src/components/MemberAccessEditor.tsx +443 -0
- package/src/components/MembersModal.tsx +446 -0
- package/src/components/NewConnectionModal.tsx +274 -0
- package/src/components/Resizer.tsx +66 -0
- package/src/components/ScanSuccessModal.tsx +113 -0
- package/src/components/ShortcutSettingsModal.tsx +318 -0
- package/src/components/Sidebar.tsx +532 -0
- package/src/components/TabBar.tsx +188 -0
- package/src/components/TableView.tsx +2147 -0
- package/src/components/ThemeToggle.tsx +44 -0
- package/src/components/index.ts +17 -0
- package/src/constants.ts +12 -0
- package/src/electron.d.ts +12 -0
- package/src/index.css +44 -0
- package/src/main.tsx +13 -0
- package/src/stores/hooks.ts +1146 -0
- package/src/stores/index.ts +12 -0
- package/src/stores/store.ts +1514 -0
- package/src/stores/useCloudSync.ts +274 -0
- package/src/stores/useSyncDatabase.ts +422 -0
- package/src/types.ts +277 -0
- package/src/utils/csv.ts +27 -0
- package/src/vite-env.d.ts +2 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/tsconfig.server.json +14 -0
- package/vite.config.ts +14 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
RefreshCw,
|
|
4
|
+
ChevronRight,
|
|
5
|
+
Key,
|
|
6
|
+
Link2,
|
|
7
|
+
Fingerprint,
|
|
8
|
+
Hash,
|
|
9
|
+
Type,
|
|
10
|
+
HashIcon,
|
|
11
|
+
ToggleLeft,
|
|
12
|
+
Calendar,
|
|
13
|
+
Clock,
|
|
14
|
+
Binary,
|
|
15
|
+
List,
|
|
16
|
+
Braces,
|
|
17
|
+
MapPin,
|
|
18
|
+
Circle,
|
|
19
|
+
FileText,
|
|
20
|
+
Boxes,
|
|
21
|
+
HelpCircle,
|
|
22
|
+
} from "lucide-react";
|
|
23
|
+
import type {
|
|
24
|
+
DatabaseConfig,
|
|
25
|
+
SchemaMetadata,
|
|
26
|
+
TableMetadata,
|
|
27
|
+
ColumnInfo,
|
|
28
|
+
} from "../types";
|
|
29
|
+
|
|
30
|
+
interface SidebarProps {
|
|
31
|
+
schemas: SchemaMetadata[];
|
|
32
|
+
databaseConfig: DatabaseConfig | null;
|
|
33
|
+
onTableClick: (tableName: string) => void;
|
|
34
|
+
onTableOpenNewTab: (tableName: string) => void;
|
|
35
|
+
onRefresh: () => void;
|
|
36
|
+
isRefreshing?: boolean;
|
|
37
|
+
width?: number;
|
|
38
|
+
activeTableName?: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Map data types to icons
|
|
42
|
+
function DataTypeIcon({ dataType }: { dataType: string }) {
|
|
43
|
+
const baseType = dataType.toLowerCase().split("(")[0].trim();
|
|
44
|
+
const iconClass = "w-3 h-3 text-tertiary flex-shrink-0";
|
|
45
|
+
|
|
46
|
+
// Text types
|
|
47
|
+
if (
|
|
48
|
+
[
|
|
49
|
+
"text",
|
|
50
|
+
"varchar",
|
|
51
|
+
"char",
|
|
52
|
+
"character",
|
|
53
|
+
"character varying",
|
|
54
|
+
"name",
|
|
55
|
+
"citext",
|
|
56
|
+
].includes(baseType)
|
|
57
|
+
) {
|
|
58
|
+
return <Type className={iconClass} strokeWidth={2} />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Numeric types
|
|
62
|
+
if (
|
|
63
|
+
[
|
|
64
|
+
"integer",
|
|
65
|
+
"int",
|
|
66
|
+
"int2",
|
|
67
|
+
"int4",
|
|
68
|
+
"int8",
|
|
69
|
+
"smallint",
|
|
70
|
+
"bigint",
|
|
71
|
+
"serial",
|
|
72
|
+
"bigserial",
|
|
73
|
+
"smallserial",
|
|
74
|
+
"numeric",
|
|
75
|
+
"decimal",
|
|
76
|
+
"real",
|
|
77
|
+
"float",
|
|
78
|
+
"float4",
|
|
79
|
+
"float8",
|
|
80
|
+
"double precision",
|
|
81
|
+
"money",
|
|
82
|
+
].includes(baseType)
|
|
83
|
+
) {
|
|
84
|
+
return <HashIcon className={iconClass} strokeWidth={2} />;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Boolean
|
|
88
|
+
if (["boolean", "bool"].includes(baseType)) {
|
|
89
|
+
return <ToggleLeft className={iconClass} strokeWidth={2} />;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Date types
|
|
93
|
+
if (["date"].includes(baseType)) {
|
|
94
|
+
return <Calendar className={iconClass} strokeWidth={2} />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Time/timestamp types
|
|
98
|
+
if (
|
|
99
|
+
[
|
|
100
|
+
"time",
|
|
101
|
+
"timetz",
|
|
102
|
+
"timestamp",
|
|
103
|
+
"timestamptz",
|
|
104
|
+
"timestamp without time zone",
|
|
105
|
+
"timestamp with time zone",
|
|
106
|
+
"interval",
|
|
107
|
+
].includes(baseType)
|
|
108
|
+
) {
|
|
109
|
+
return <Clock className={iconClass} strokeWidth={2} />;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Binary types
|
|
113
|
+
if (["bytea", "bit", "bit varying", "varbit"].includes(baseType)) {
|
|
114
|
+
return <Binary className={iconClass} strokeWidth={2} />;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Array types
|
|
118
|
+
if (baseType.endsWith("[]") || baseType === "array") {
|
|
119
|
+
return <List className={iconClass} strokeWidth={2} />;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// JSON types
|
|
123
|
+
if (["json", "jsonb"].includes(baseType)) {
|
|
124
|
+
return <Braces className={iconClass} strokeWidth={2} />;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// UUID
|
|
128
|
+
if (["uuid"].includes(baseType)) {
|
|
129
|
+
return <Fingerprint className={iconClass} strokeWidth={2} />;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Geometric/spatial types
|
|
133
|
+
if (
|
|
134
|
+
[
|
|
135
|
+
"point",
|
|
136
|
+
"line",
|
|
137
|
+
"lseg",
|
|
138
|
+
"box",
|
|
139
|
+
"path",
|
|
140
|
+
"polygon",
|
|
141
|
+
"circle",
|
|
142
|
+
"geometry",
|
|
143
|
+
"geography",
|
|
144
|
+
].includes(baseType)
|
|
145
|
+
) {
|
|
146
|
+
return <MapPin className={iconClass} strokeWidth={2} />;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Network types
|
|
150
|
+
if (["inet", "cidr", "macaddr", "macaddr8"].includes(baseType)) {
|
|
151
|
+
return <Circle className={iconClass} strokeWidth={2} />;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Text search types
|
|
155
|
+
if (["tsvector", "tsquery"].includes(baseType)) {
|
|
156
|
+
return <FileText className={iconClass} strokeWidth={2} />;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Range types
|
|
160
|
+
if (
|
|
161
|
+
baseType.endsWith("range") ||
|
|
162
|
+
[
|
|
163
|
+
"int4range",
|
|
164
|
+
"int8range",
|
|
165
|
+
"numrange",
|
|
166
|
+
"tsrange",
|
|
167
|
+
"tstzrange",
|
|
168
|
+
"daterange",
|
|
169
|
+
].includes(baseType)
|
|
170
|
+
) {
|
|
171
|
+
return <Boxes className={iconClass} strokeWidth={2} />;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// XML
|
|
175
|
+
if (["xml"].includes(baseType)) {
|
|
176
|
+
return <FileText className={iconClass} strokeWidth={2} />;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// OID and system types
|
|
180
|
+
if (
|
|
181
|
+
[
|
|
182
|
+
"oid",
|
|
183
|
+
"regclass",
|
|
184
|
+
"regtype",
|
|
185
|
+
"regproc",
|
|
186
|
+
"regoper",
|
|
187
|
+
"regconfig",
|
|
188
|
+
"regdictionary",
|
|
189
|
+
].includes(baseType)
|
|
190
|
+
) {
|
|
191
|
+
return <Circle className={iconClass} strokeWidth={2} />;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Default/unknown
|
|
195
|
+
return <HelpCircle className={iconClass} strokeWidth={2} />;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Constraint icons shown on the right
|
|
199
|
+
function ConstraintIcons({ column }: { column: ColumnInfo }) {
|
|
200
|
+
const { constraints } = column;
|
|
201
|
+
const icons: ReactNode[] = [];
|
|
202
|
+
|
|
203
|
+
if (constraints.isPrimaryKey) {
|
|
204
|
+
icons.push(
|
|
205
|
+
<span key="pk" title="Primary Key">
|
|
206
|
+
<Key className="w-3 h-3 text-amber-500" strokeWidth={2} />
|
|
207
|
+
</span>,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (constraints.isForeignKey) {
|
|
211
|
+
const fkTitle = constraints.foreignKeyRef
|
|
212
|
+
? `FK → ${constraints.foreignKeyRef.table}.${constraints.foreignKeyRef.column}`
|
|
213
|
+
: "Foreign Key";
|
|
214
|
+
icons.push(
|
|
215
|
+
<span key="fk" title={fkTitle}>
|
|
216
|
+
<Link2 className="w-3 h-3 text-blue-500" strokeWidth={2} />
|
|
217
|
+
</span>,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (constraints.isUnique) {
|
|
221
|
+
icons.push(
|
|
222
|
+
<span key="uq" title="Unique">
|
|
223
|
+
<Fingerprint className="w-3 h-3 text-purple-500" strokeWidth={2} />
|
|
224
|
+
</span>,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (constraints.isIndexed) {
|
|
228
|
+
icons.push(
|
|
229
|
+
<span key="idx" title="Indexed">
|
|
230
|
+
<Hash className="w-3 h-3 text-interactive-subtle" strokeWidth={2} />
|
|
231
|
+
</span>,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (icons.length === 0) return null;
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="flex items-center gap-0.5 flex-shrink-0 ml-1">{icons}</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function TableRow({
|
|
243
|
+
table,
|
|
244
|
+
schema,
|
|
245
|
+
isExpanded,
|
|
246
|
+
isActive,
|
|
247
|
+
onToggle,
|
|
248
|
+
onTableClick,
|
|
249
|
+
onTableOpenNewTab,
|
|
250
|
+
onContextMenu,
|
|
251
|
+
rowRef,
|
|
252
|
+
}: {
|
|
253
|
+
table: TableMetadata;
|
|
254
|
+
schema: SchemaMetadata;
|
|
255
|
+
isExpanded: boolean;
|
|
256
|
+
isActive: boolean;
|
|
257
|
+
onToggle: () => void;
|
|
258
|
+
onTableClick: (tableName: string) => void;
|
|
259
|
+
onTableOpenNewTab: (tableName: string) => void;
|
|
260
|
+
onContextMenu: (e: React.MouseEvent, tableName: string) => void;
|
|
261
|
+
rowRef?: React.RefObject<HTMLLIElement | null>;
|
|
262
|
+
}) {
|
|
263
|
+
const displayName = table.name;
|
|
264
|
+
const qualifiedName =
|
|
265
|
+
schema.name === "public" ? table.name : `${schema.name}.${table.name}`;
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<li ref={rowRef}>
|
|
269
|
+
<div
|
|
270
|
+
className={`flex items-center px-2 py-1.5 text-[13px] rounded-md cursor-pointer transition-all duration-150 ${
|
|
271
|
+
isActive
|
|
272
|
+
? "bg-blue-100 dark:bg-blue-900/30 text-primary"
|
|
273
|
+
: "text-secondary hover:text-primary hover:bg-stone-200/70 dark:hover:bg-white/[0.06]"
|
|
274
|
+
}`}
|
|
275
|
+
onClick={(e) => {
|
|
276
|
+
if (e.metaKey || e.ctrlKey) {
|
|
277
|
+
onTableOpenNewTab(qualifiedName);
|
|
278
|
+
} else {
|
|
279
|
+
onTableClick(qualifiedName);
|
|
280
|
+
}
|
|
281
|
+
}}
|
|
282
|
+
onContextMenu={(e) => {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
onContextMenu(e, qualifiedName);
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
<button
|
|
288
|
+
className="p-0.5 -ml-0.5 mr-1 rounded hover:bg-stone-300/50 dark:hover:bg-white/10 transition-colors"
|
|
289
|
+
onClick={(e) => {
|
|
290
|
+
e.stopPropagation();
|
|
291
|
+
onToggle();
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
<ChevronRight
|
|
295
|
+
className={`w-3.5 h-3.5 text-tertiary transition-transform duration-150 ${
|
|
296
|
+
isExpanded ? "rotate-90" : ""
|
|
297
|
+
}`}
|
|
298
|
+
strokeWidth={2}
|
|
299
|
+
/>
|
|
300
|
+
</button>
|
|
301
|
+
<span className="font-mono truncate">{displayName}</span>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{isExpanded && (
|
|
305
|
+
<ul className="ml-3 border-l border-stone-200 dark:border-white/[0.08] pl-1 py-1">
|
|
306
|
+
{table.columns.map((column) => (
|
|
307
|
+
<li
|
|
308
|
+
key={column.name}
|
|
309
|
+
className="flex items-center gap-1.5 px-2 py-1 text-[11px] text-tertiary font-mono min-w-0"
|
|
310
|
+
title={getColumnTooltip(column)}
|
|
311
|
+
>
|
|
312
|
+
<DataTypeIcon dataType={column.dataType} />
|
|
313
|
+
<span className="truncate flex-shrink min-w-0">
|
|
314
|
+
{column.name}
|
|
315
|
+
</span>
|
|
316
|
+
<span className="text-interactive-subtle text-[10px] truncate flex-shrink-[2] min-w-0">
|
|
317
|
+
{column.dataType}
|
|
318
|
+
</span>
|
|
319
|
+
<ConstraintIcons column={column} />
|
|
320
|
+
</li>
|
|
321
|
+
))}
|
|
322
|
+
</ul>
|
|
323
|
+
)}
|
|
324
|
+
</li>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function getColumnTooltip(column: ColumnInfo): string {
|
|
329
|
+
const parts: string[] = [column.dataType];
|
|
330
|
+
|
|
331
|
+
if (column.constraints.isPrimaryKey) parts.push("Primary Key");
|
|
332
|
+
if (column.constraints.isForeignKey && column.constraints.foreignKeyRef) {
|
|
333
|
+
const ref = column.constraints.foreignKeyRef;
|
|
334
|
+
parts.push(`FK → ${ref.schema}.${ref.table}.${ref.column}`);
|
|
335
|
+
}
|
|
336
|
+
if (column.constraints.isUnique) parts.push("Unique");
|
|
337
|
+
if (column.constraints.isIndexed) parts.push("Indexed");
|
|
338
|
+
if (!column.isNullable) parts.push("NOT NULL");
|
|
339
|
+
if (column.defaultValue) parts.push(`Default: ${column.defaultValue}`);
|
|
340
|
+
|
|
341
|
+
return parts.join(" | ");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function Sidebar({
|
|
345
|
+
schemas,
|
|
346
|
+
databaseConfig,
|
|
347
|
+
onTableClick,
|
|
348
|
+
onTableOpenNewTab,
|
|
349
|
+
onRefresh,
|
|
350
|
+
isRefreshing,
|
|
351
|
+
width,
|
|
352
|
+
activeTableName,
|
|
353
|
+
}: SidebarProps) {
|
|
354
|
+
const [expandedTables, setExpandedTables] = useState<Set<string>>(new Set());
|
|
355
|
+
const [collapsedSchemas, setCollapsedSchemas] = useState<Set<string>>(
|
|
356
|
+
new Set(),
|
|
357
|
+
);
|
|
358
|
+
const activeRowRef = useRef<HTMLLIElement | null>(null);
|
|
359
|
+
const [contextMenu, setContextMenu] = useState<{
|
|
360
|
+
x: number;
|
|
361
|
+
y: number;
|
|
362
|
+
tableName: string;
|
|
363
|
+
} | null>(null);
|
|
364
|
+
const contextMenuRef = useRef<HTMLDivElement | null>(null);
|
|
365
|
+
|
|
366
|
+
// Dismiss context menu on click-outside, Escape, or scroll
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (!contextMenu) return;
|
|
369
|
+
const handleClick = (e: MouseEvent) => {
|
|
370
|
+
if (
|
|
371
|
+
contextMenuRef.current &&
|
|
372
|
+
!contextMenuRef.current.contains(e.target as Node)
|
|
373
|
+
) {
|
|
374
|
+
setContextMenu(null);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
378
|
+
if (e.key === "Escape") setContextMenu(null);
|
|
379
|
+
};
|
|
380
|
+
const handleScroll = () => setContextMenu(null);
|
|
381
|
+
document.addEventListener("mousedown", handleClick);
|
|
382
|
+
document.addEventListener("keydown", handleKey);
|
|
383
|
+
document.addEventListener("scroll", handleScroll, true);
|
|
384
|
+
return () => {
|
|
385
|
+
document.removeEventListener("mousedown", handleClick);
|
|
386
|
+
document.removeEventListener("keydown", handleKey);
|
|
387
|
+
document.removeEventListener("scroll", handleScroll, true);
|
|
388
|
+
};
|
|
389
|
+
}, [contextMenu]);
|
|
390
|
+
|
|
391
|
+
// Scroll to active table when it changes
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
if (activeTableName && activeRowRef.current) {
|
|
394
|
+
activeRowRef.current.scrollIntoView({
|
|
395
|
+
block: "nearest",
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}, [activeTableName]);
|
|
399
|
+
|
|
400
|
+
const toggleTable = (tableKey: string) => {
|
|
401
|
+
setExpandedTables((prev) => {
|
|
402
|
+
const next = new Set(prev);
|
|
403
|
+
if (next.has(tableKey)) {
|
|
404
|
+
next.delete(tableKey);
|
|
405
|
+
} else {
|
|
406
|
+
next.add(tableKey);
|
|
407
|
+
}
|
|
408
|
+
return next;
|
|
409
|
+
});
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const toggleSchema = (schemaName: string) => {
|
|
413
|
+
setCollapsedSchemas((prev) => {
|
|
414
|
+
const next = new Set(prev);
|
|
415
|
+
if (next.has(schemaName)) {
|
|
416
|
+
next.delete(schemaName);
|
|
417
|
+
} else {
|
|
418
|
+
next.add(schemaName);
|
|
419
|
+
}
|
|
420
|
+
return next;
|
|
421
|
+
});
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const showSchemaHeaders = schemas.length > 1;
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<div
|
|
428
|
+
className="bg-stone-100 dark:bg-[#0a0a0a] border-r border-stone-200 dark:border-white/[0.06] flex flex-col flex-shrink-0"
|
|
429
|
+
style={{ width: width ?? 208 }}
|
|
430
|
+
>
|
|
431
|
+
<div className="px-4 py-4 flex items-center justify-between">
|
|
432
|
+
<div className="flex items-center gap-2">
|
|
433
|
+
<span
|
|
434
|
+
className="w-2 h-2 rounded-full"
|
|
435
|
+
style={{ backgroundColor: databaseConfig?.display.color }}
|
|
436
|
+
/>
|
|
437
|
+
<span className="text-[11px] font-semibold text-tertiary uppercase tracking-[0.08em]">
|
|
438
|
+
Schemas
|
|
439
|
+
</span>
|
|
440
|
+
</div>
|
|
441
|
+
<button
|
|
442
|
+
onClick={onRefresh}
|
|
443
|
+
disabled={isRefreshing}
|
|
444
|
+
className="p-1 rounded hover:bg-stone-200/70 dark:hover:bg-white/[0.06] text-interactive transition-colors disabled:opacity-50"
|
|
445
|
+
title="Refresh tables"
|
|
446
|
+
>
|
|
447
|
+
<RefreshCw
|
|
448
|
+
className={`w-3.5 h-3.5 ${isRefreshing ? "animate-[spin_2s_linear_infinite]" : ""}`}
|
|
449
|
+
strokeWidth={2}
|
|
450
|
+
/>
|
|
451
|
+
</button>
|
|
452
|
+
</div>
|
|
453
|
+
<ul className="flex-1 overflow-y-auto px-2 pb-4 select-none">
|
|
454
|
+
{schemas.map((schema) => {
|
|
455
|
+
const isCollapsed = collapsedSchemas.has(schema.name);
|
|
456
|
+
|
|
457
|
+
const tableRows = schema.tables.map((table) => {
|
|
458
|
+
const tableKey = `${schema.name}.${table.name}`;
|
|
459
|
+
const qualifiedName =
|
|
460
|
+
schema.name === "public"
|
|
461
|
+
? table.name
|
|
462
|
+
: `${schema.name}.${table.name}`;
|
|
463
|
+
const isActive = activeTableName === qualifiedName;
|
|
464
|
+
return (
|
|
465
|
+
<TableRow
|
|
466
|
+
key={tableKey}
|
|
467
|
+
table={table}
|
|
468
|
+
schema={schema}
|
|
469
|
+
isExpanded={expandedTables.has(tableKey)}
|
|
470
|
+
isActive={isActive}
|
|
471
|
+
onToggle={() => toggleTable(tableKey)}
|
|
472
|
+
onTableClick={onTableClick}
|
|
473
|
+
onTableOpenNewTab={onTableOpenNewTab}
|
|
474
|
+
onContextMenu={(e, name) =>
|
|
475
|
+
setContextMenu({
|
|
476
|
+
x: e.clientX,
|
|
477
|
+
y: e.clientY,
|
|
478
|
+
tableName: name,
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
rowRef={isActive ? activeRowRef : undefined}
|
|
482
|
+
/>
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
if (!showSchemaHeaders) return tableRows;
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
<li key={schema.name}>
|
|
490
|
+
<button
|
|
491
|
+
className="flex items-center gap-1.5 w-full px-1 py-1.5 mt-1 first:mt-0 cursor-pointer group"
|
|
492
|
+
onClick={() => toggleSchema(schema.name)}
|
|
493
|
+
>
|
|
494
|
+
<ChevronRight
|
|
495
|
+
className={`w-3 h-3 text-tertiary transition-transform duration-150 ${
|
|
496
|
+
!isCollapsed ? "rotate-90" : ""
|
|
497
|
+
}`}
|
|
498
|
+
strokeWidth={2}
|
|
499
|
+
/>
|
|
500
|
+
<span className="text-[11px] font-semibold text-tertiary uppercase tracking-[0.08em]">
|
|
501
|
+
{schema.name}
|
|
502
|
+
</span>
|
|
503
|
+
<span className="text-[10px] text-interactive-subtle">
|
|
504
|
+
({schema.tables.length})
|
|
505
|
+
</span>
|
|
506
|
+
</button>
|
|
507
|
+
{!isCollapsed && <ul>{tableRows}</ul>}
|
|
508
|
+
</li>
|
|
509
|
+
);
|
|
510
|
+
})}
|
|
511
|
+
</ul>
|
|
512
|
+
|
|
513
|
+
{contextMenu && (
|
|
514
|
+
<div
|
|
515
|
+
ref={contextMenuRef}
|
|
516
|
+
className="fixed z-50 min-w-[160px] py-1 bg-white dark:bg-[#1a1a1a] border border-stone-200 dark:border-white/[0.1] rounded-lg shadow-lg"
|
|
517
|
+
style={{ left: contextMenu.x, top: contextMenu.y }}
|
|
518
|
+
>
|
|
519
|
+
<button
|
|
520
|
+
className="w-full text-left px-3 py-1.5 text-[13px] text-secondary hover:text-primary hover:bg-stone-100 dark:hover:bg-white/[0.06] transition-colors"
|
|
521
|
+
onClick={() => {
|
|
522
|
+
onTableOpenNewTab(contextMenu.tableName);
|
|
523
|
+
setContextMenu(null);
|
|
524
|
+
}}
|
|
525
|
+
>
|
|
526
|
+
Open in new tab
|
|
527
|
+
</button>
|
|
528
|
+
</div>
|
|
529
|
+
)}
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
}
|