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.
Files changed (69) hide show
  1. package/README.md +73 -0
  2. package/bin/cli.js +83 -0
  3. package/bin/install-local.js +57 -0
  4. package/electron/generate-icon.mjs +54 -0
  5. package/electron/icon.icns +0 -0
  6. package/electron/icon.png +0 -0
  7. package/electron/icon.svg +21 -0
  8. package/electron/main.js +169 -0
  9. package/electron/patch-dev-plist.js +31 -0
  10. package/electron/preload.cjs +18 -0
  11. package/electron/wait-for-vite.js +43 -0
  12. package/index.html +13 -0
  13. package/package.json +91 -0
  14. package/public/favicon.svg +15 -0
  15. package/public/vite.svg +1 -0
  16. package/server/export.ts +57 -0
  17. package/server/index.ts +392 -0
  18. package/src/App.css +1 -0
  19. package/src/App.tsx +543 -0
  20. package/src/assets/react.svg +1 -0
  21. package/src/components/CommandPalette.tsx +243 -0
  22. package/src/components/ConnectedView.tsx +78 -0
  23. package/src/components/ConnectionPicker.tsx +381 -0
  24. package/src/components/ConsoleView.tsx +360 -0
  25. package/src/components/CsvExportModal.tsx +144 -0
  26. package/src/components/DataGrid/DataGrid.tsx +262 -0
  27. package/src/components/DataGrid/DataGridCell.tsx +73 -0
  28. package/src/components/DataGrid/DataGridHeader.tsx +89 -0
  29. package/src/components/DataGrid/index.ts +20 -0
  30. package/src/components/DataGrid/types.ts +63 -0
  31. package/src/components/DataGrid/useColumnResize.ts +153 -0
  32. package/src/components/DataGrid/useDataGridSelection.ts +340 -0
  33. package/src/components/DataGrid/utils.ts +184 -0
  34. package/src/components/DatabaseMenu.tsx +93 -0
  35. package/src/components/DatabaseSwitcher.tsx +208 -0
  36. package/src/components/DiffView.tsx +215 -0
  37. package/src/components/EditConnectionModal.tsx +417 -0
  38. package/src/components/ErrorBoundary.tsx +69 -0
  39. package/src/components/GlobalShortcuts.tsx +201 -0
  40. package/src/components/InnerTabBar.tsx +129 -0
  41. package/src/components/JsonTreeViewer.tsx +387 -0
  42. package/src/components/MemberAccessEditor.tsx +443 -0
  43. package/src/components/MembersModal.tsx +446 -0
  44. package/src/components/NewConnectionModal.tsx +274 -0
  45. package/src/components/Resizer.tsx +66 -0
  46. package/src/components/ScanSuccessModal.tsx +113 -0
  47. package/src/components/ShortcutSettingsModal.tsx +318 -0
  48. package/src/components/Sidebar.tsx +532 -0
  49. package/src/components/TabBar.tsx +188 -0
  50. package/src/components/TableView.tsx +2147 -0
  51. package/src/components/ThemeToggle.tsx +44 -0
  52. package/src/components/index.ts +17 -0
  53. package/src/constants.ts +12 -0
  54. package/src/electron.d.ts +12 -0
  55. package/src/index.css +44 -0
  56. package/src/main.tsx +13 -0
  57. package/src/stores/hooks.ts +1146 -0
  58. package/src/stores/index.ts +12 -0
  59. package/src/stores/store.ts +1514 -0
  60. package/src/stores/useCloudSync.ts +274 -0
  61. package/src/stores/useSyncDatabase.ts +422 -0
  62. package/src/types.ts +277 -0
  63. package/src/utils/csv.ts +27 -0
  64. package/src/vite-env.d.ts +2 -0
  65. package/tsconfig.app.json +28 -0
  66. package/tsconfig.json +7 -0
  67. package/tsconfig.node.json +26 -0
  68. package/tsconfig.server.json +14 -0
  69. package/vite.config.ts +14 -0
@@ -0,0 +1,443 @@
1
+ import { useState, useMemo } from "react";
2
+ import type {
3
+ ConnectionMember,
4
+ AccessMap,
5
+ AccessLevel,
6
+ SchemaMetadata,
7
+ } from "../types";
8
+
9
+ interface MemberAccessEditorProps {
10
+ member: ConnectionMember;
11
+ schemas: SchemaMetadata[];
12
+ onSave: (access: AccessMap) => void;
13
+ onBack: () => void;
14
+ isSaving: boolean;
15
+ }
16
+
17
+ type BaseLevel = "write" | "read" | "none";
18
+
19
+ interface TableRule {
20
+ key: string; // "schema.table"
21
+ level: AccessLevel;
22
+ }
23
+
24
+ interface ColumnRule {
25
+ key: string; // "schema.table.column"
26
+ }
27
+
28
+ function parseAccessMap(access: AccessMap): {
29
+ base: BaseLevel;
30
+ tableRules: TableRule[];
31
+ columnRules: ColumnRule[];
32
+ } {
33
+ const base: BaseLevel = (access["*"] as BaseLevel) ?? "read";
34
+ const tableRules: TableRule[] = [];
35
+ const columnRules: ColumnRule[] = [];
36
+
37
+ for (const [key, level] of Object.entries(access)) {
38
+ if (key === "*") continue;
39
+ const parts = key.split(".");
40
+ if (parts.length === 2) {
41
+ tableRules.push({ key, level });
42
+ } else if (parts.length === 3) {
43
+ columnRules.push({ key });
44
+ }
45
+ }
46
+
47
+ return { base, tableRules, columnRules };
48
+ }
49
+
50
+ function buildAccessMap(
51
+ base: BaseLevel,
52
+ tableRules: TableRule[],
53
+ columnRules: ColumnRule[],
54
+ ): AccessMap {
55
+ const map: AccessMap = { "*": base };
56
+ for (const rule of tableRules) {
57
+ map[rule.key] = rule.level;
58
+ }
59
+ for (const rule of columnRules) {
60
+ map[rule.key] = "none";
61
+ }
62
+ return map;
63
+ }
64
+
65
+ function BackIcon() {
66
+ return (
67
+ <svg
68
+ className="w-4 h-4"
69
+ fill="none"
70
+ stroke="currentColor"
71
+ viewBox="0 0 24 24"
72
+ >
73
+ <path
74
+ strokeLinecap="round"
75
+ strokeLinejoin="round"
76
+ strokeWidth={2}
77
+ d="M15 19l-7-7 7-7"
78
+ />
79
+ </svg>
80
+ );
81
+ }
82
+
83
+ function XIcon() {
84
+ return (
85
+ <svg
86
+ className="w-3.5 h-3.5"
87
+ fill="none"
88
+ stroke="currentColor"
89
+ viewBox="0 0 24 24"
90
+ >
91
+ <path
92
+ strokeLinecap="round"
93
+ strokeLinejoin="round"
94
+ strokeWidth={2}
95
+ d="M6 18L18 6M6 6l12 12"
96
+ />
97
+ </svg>
98
+ );
99
+ }
100
+
101
+ function PlusIcon() {
102
+ return (
103
+ <svg
104
+ className="w-3.5 h-3.5"
105
+ fill="none"
106
+ stroke="currentColor"
107
+ viewBox="0 0 24 24"
108
+ >
109
+ <path
110
+ strokeLinecap="round"
111
+ strokeLinejoin="round"
112
+ strokeWidth={2}
113
+ d="M12 4v16m8-8H4"
114
+ />
115
+ </svg>
116
+ );
117
+ }
118
+
119
+ const BASE_OPTIONS: { value: BaseLevel; label: string; description: string }[] =
120
+ [
121
+ {
122
+ value: "write",
123
+ label: "Full Access",
124
+ description: "Can read and write all tables",
125
+ },
126
+ {
127
+ value: "read",
128
+ label: "Read Only",
129
+ description: "Can read all tables, cannot modify data",
130
+ },
131
+ {
132
+ value: "none",
133
+ label: "No Access",
134
+ description: "Cannot access anything unless explicitly allowed below",
135
+ },
136
+ ];
137
+
138
+ export function MemberAccessEditor({
139
+ member,
140
+ schemas,
141
+ onSave,
142
+ onBack,
143
+ isSaving,
144
+ }: MemberAccessEditorProps) {
145
+ const initial = useMemo(() => parseAccessMap(member.access), [member.access]);
146
+
147
+ const [base, setBase] = useState<BaseLevel>(initial.base);
148
+ const [tableRules, setTableRules] = useState<TableRule[]>(initial.tableRules);
149
+ const [columnRules, setColumnRules] = useState<ColumnRule[]>(
150
+ initial.columnRules,
151
+ );
152
+ const [showColumnRules, setShowColumnRules] = useState(
153
+ initial.columnRules.length > 0,
154
+ );
155
+
156
+ // Available tables from schemas
157
+ const allTables = useMemo(() => {
158
+ const tables: { key: string; label: string }[] = [];
159
+ for (const schema of schemas) {
160
+ for (const table of schema.tables) {
161
+ tables.push({
162
+ key: `${schema.name}.${table.name}`,
163
+ label: `${schema.name}.${table.name}`,
164
+ });
165
+ }
166
+ }
167
+ return tables;
168
+ }, [schemas]);
169
+
170
+ // Available columns from schemas
171
+ const allColumns = useMemo(() => {
172
+ const columns: { key: string; label: string }[] = [];
173
+ for (const schema of schemas) {
174
+ for (const table of schema.tables) {
175
+ for (const col of table.columns) {
176
+ columns.push({
177
+ key: `${schema.name}.${table.name}.${col.name}`,
178
+ label: `${schema.name}.${table.name}.${col.name}`,
179
+ });
180
+ }
181
+ }
182
+ }
183
+ return columns;
184
+ }, [schemas]);
185
+
186
+ // Track which tables/columns are already used in rules
187
+ const usedTableKeys = new Set(tableRules.map((r) => r.key));
188
+ const usedColumnKeys = new Set(columnRules.map((r) => r.key));
189
+
190
+ // Check if anything changed
191
+ const currentMap = buildAccessMap(base, tableRules, columnRules);
192
+ const originalMap = member.access;
193
+ const hasChanges = JSON.stringify(currentMap) !== JSON.stringify(originalMap);
194
+
195
+ function handleSave() {
196
+ onSave(buildAccessMap(base, tableRules, columnRules));
197
+ }
198
+
199
+ function addTableRule() {
200
+ const available = allTables.find((t) => !usedTableKeys.has(t.key));
201
+ if (available) {
202
+ setTableRules((prev) => [
203
+ ...prev,
204
+ { key: available.key, level: base === "none" ? "read" : "none" },
205
+ ]);
206
+ }
207
+ }
208
+
209
+ function removeTableRule(index: number) {
210
+ setTableRules((prev) => prev.filter((_, i) => i !== index));
211
+ }
212
+
213
+ function updateTableRuleKey(index: number, key: string) {
214
+ setTableRules((prev) =>
215
+ prev.map((r, i) => (i === index ? { ...r, key } : r)),
216
+ );
217
+ }
218
+
219
+ function updateTableRuleLevel(index: number, level: AccessLevel) {
220
+ setTableRules((prev) =>
221
+ prev.map((r, i) => (i === index ? { ...r, level } : r)),
222
+ );
223
+ }
224
+
225
+ function addColumnRule() {
226
+ const available = allColumns.find((c) => !usedColumnKeys.has(c.key));
227
+ if (available) {
228
+ setColumnRules((prev) => [...prev, { key: available.key }]);
229
+ }
230
+ }
231
+
232
+ function removeColumnRule(index: number) {
233
+ setColumnRules((prev) => prev.filter((_, i) => i !== index));
234
+ }
235
+
236
+ function updateColumnRuleKey(index: number, key: string) {
237
+ setColumnRules((prev) =>
238
+ prev.map((r, i) => (i === index ? { ...r, key } : r)),
239
+ );
240
+ }
241
+
242
+ return (
243
+ <div>
244
+ {/* Header */}
245
+ <div className="flex items-center gap-3 mb-6">
246
+ <button
247
+ onClick={onBack}
248
+ className="p-1 rounded text-tertiary hover:text-primary hover:bg-stone-100 dark:hover:bg-white/10 transition-colors"
249
+ >
250
+ <BackIcon />
251
+ </button>
252
+ <div className="flex-1 min-w-0">
253
+ <h2 className="text-[18px] font-semibold text-primary truncate">
254
+ {member.email}
255
+ </h2>
256
+ <p className="text-[12px] text-tertiary">Configure access level</p>
257
+ </div>
258
+ <button
259
+ onClick={handleSave}
260
+ disabled={!hasChanges || isSaving}
261
+ className="px-4 py-2 text-[13px] font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
262
+ >
263
+ {isSaving ? "Saving..." : "Save"}
264
+ </button>
265
+ </div>
266
+
267
+ {/* Base Access Level */}
268
+ <div className="mb-6">
269
+ <label className="block text-[12px] font-medium text-secondary mb-2">
270
+ Base Access Level
271
+ </label>
272
+ <div className="flex rounded-lg border border-stone-200 dark:border-white/10 overflow-hidden">
273
+ {BASE_OPTIONS.map((opt) => (
274
+ <button
275
+ key={opt.value}
276
+ onClick={() => setBase(opt.value)}
277
+ className={`flex-1 px-3 py-2 text-[13px] font-medium transition-colors ${
278
+ base === opt.value
279
+ ? "bg-blue-600 text-white"
280
+ : "bg-stone-50 dark:bg-white/[0.02] text-secondary hover:bg-stone-100 dark:hover:bg-white/[0.04]"
281
+ }`}
282
+ >
283
+ {opt.label}
284
+ </button>
285
+ ))}
286
+ </div>
287
+ <p className="text-[11px] text-tertiary mt-1.5">
288
+ {BASE_OPTIONS.find((o) => o.value === base)?.description}
289
+ </p>
290
+ </div>
291
+
292
+ {/* Table Rules */}
293
+ <div className="mb-6">
294
+ <div className="flex items-center justify-between mb-2">
295
+ <label className="text-[12px] font-medium text-secondary">
296
+ Table Overrides
297
+ </label>
298
+ <button
299
+ onClick={addTableRule}
300
+ disabled={
301
+ allTables.length === 0 || usedTableKeys.size >= allTables.length
302
+ }
303
+ className="flex items-center gap-1 px-2 py-1 text-[11px] font-medium text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
304
+ >
305
+ <PlusIcon />
306
+ Add Rule
307
+ </button>
308
+ </div>
309
+ {tableRules.length === 0 ? (
310
+ <p className="text-[12px] text-tertiary py-3">
311
+ No table-level overrides. All tables follow the base access level.
312
+ </p>
313
+ ) : (
314
+ <div className="space-y-2">
315
+ {tableRules.map((rule, i) => (
316
+ <div
317
+ key={i}
318
+ className="flex items-center gap-2 p-2 bg-stone-50 dark:bg-white/[0.02] border border-stone-200 dark:border-white/[0.06] rounded-lg"
319
+ >
320
+ <select
321
+ value={rule.key}
322
+ onChange={(e) => updateTableRuleKey(i, e.target.value)}
323
+ className="flex-1 px-2 py-1 text-[12px] bg-white dark:bg-white/5 border border-stone-200 dark:border-white/10 rounded text-primary focus:outline-none focus:ring-2 focus:ring-blue-500/50"
324
+ >
325
+ {allTables.map((t) => (
326
+ <option
327
+ key={t.key}
328
+ value={t.key}
329
+ disabled={usedTableKeys.has(t.key) && t.key !== rule.key}
330
+ >
331
+ {t.label}
332
+ </option>
333
+ ))}
334
+ </select>
335
+ <select
336
+ value={rule.level}
337
+ onChange={(e) =>
338
+ updateTableRuleLevel(i, e.target.value as AccessLevel)
339
+ }
340
+ className="px-2 py-1 text-[12px] bg-white dark:bg-white/5 border border-stone-200 dark:border-white/10 rounded text-primary focus:outline-none focus:ring-2 focus:ring-blue-500/50"
341
+ >
342
+ <option value="write">Write</option>
343
+ <option value="read">Read</option>
344
+ <option value="none">Hidden</option>
345
+ </select>
346
+ <button
347
+ onClick={() => removeTableRule(i)}
348
+ className="p-1 rounded text-tertiary hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors"
349
+ >
350
+ <XIcon />
351
+ </button>
352
+ </div>
353
+ ))}
354
+ </div>
355
+ )}
356
+ </div>
357
+
358
+ {/* Column Rules */}
359
+ <div>
360
+ <div className="flex items-center justify-between mb-2">
361
+ <button
362
+ onClick={() => setShowColumnRules(!showColumnRules)}
363
+ className="flex items-center gap-1 text-[12px] font-medium text-secondary hover:text-primary transition-colors"
364
+ >
365
+ <svg
366
+ className={`w-3 h-3 transition-transform ${showColumnRules ? "rotate-90" : ""}`}
367
+ fill="none"
368
+ stroke="currentColor"
369
+ viewBox="0 0 24 24"
370
+ >
371
+ <path
372
+ strokeLinecap="round"
373
+ strokeLinejoin="round"
374
+ strokeWidth={2}
375
+ d="M9 5l7 7-7 7"
376
+ />
377
+ </svg>
378
+ Column Overrides
379
+ </button>
380
+ {showColumnRules && (
381
+ <button
382
+ onClick={addColumnRule}
383
+ disabled={
384
+ allColumns.length === 0 ||
385
+ usedColumnKeys.size >= allColumns.length
386
+ }
387
+ className="flex items-center gap-1 px-2 py-1 text-[11px] font-medium text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
388
+ >
389
+ <PlusIcon />
390
+ Add Rule
391
+ </button>
392
+ )}
393
+ </div>
394
+ {showColumnRules && (
395
+ <>
396
+ {columnRules.length === 0 ? (
397
+ <p className="text-[12px] text-tertiary py-3">
398
+ No column-level overrides. Use this to hide specific columns
399
+ (e.g., PII).
400
+ </p>
401
+ ) : (
402
+ <div className="space-y-2">
403
+ {columnRules.map((rule, i) => (
404
+ <div
405
+ key={i}
406
+ className="flex items-center gap-2 p-2 bg-stone-50 dark:bg-white/[0.02] border border-stone-200 dark:border-white/[0.06] rounded-lg"
407
+ >
408
+ <select
409
+ value={rule.key}
410
+ onChange={(e) => updateColumnRuleKey(i, e.target.value)}
411
+ className="flex-1 px-2 py-1 text-[12px] bg-white dark:bg-white/5 border border-stone-200 dark:border-white/10 rounded text-primary focus:outline-none focus:ring-2 focus:ring-blue-500/50"
412
+ >
413
+ {allColumns.map((c) => (
414
+ <option
415
+ key={c.key}
416
+ value={c.key}
417
+ disabled={
418
+ usedColumnKeys.has(c.key) && c.key !== rule.key
419
+ }
420
+ >
421
+ {c.label}
422
+ </option>
423
+ ))}
424
+ </select>
425
+ <span className="px-2 py-1 text-[12px] text-tertiary">
426
+ Hidden
427
+ </span>
428
+ <button
429
+ onClick={() => removeColumnRule(i)}
430
+ className="p-1 rounded text-tertiary hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors"
431
+ >
432
+ <XIcon />
433
+ </button>
434
+ </div>
435
+ ))}
436
+ </div>
437
+ )}
438
+ </>
439
+ )}
440
+ </div>
441
+ </div>
442
+ );
443
+ }