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,381 @@
1
+ import type { DatabaseConfig } from "../types";
2
+ import { CLOUD_ENABLED } from "../constants";
3
+
4
+ interface ConnectionPickerProps {
5
+ databaseConfigs: DatabaseConfig[];
6
+ onConnect: (databaseConfigId: string) => void;
7
+ onAddNew: () => void;
8
+ onEdit: (config: DatabaseConfig) => void;
9
+ cloudApiKey: string | null;
10
+ onLinkCloud: () => void;
11
+ onUnlinkCloud: () => void;
12
+ onRefreshCloud: () => void;
13
+ isCloudSyncing: boolean;
14
+ cloudSyncError: string | null;
15
+ onTransferToCloud: (config: DatabaseConfig) => void;
16
+ transferringId: string | null;
17
+ onManageMembers: (config: DatabaseConfig) => void;
18
+ }
19
+
20
+ function SettingsIcon() {
21
+ return (
22
+ <svg
23
+ className="w-4 h-4"
24
+ fill="none"
25
+ stroke="currentColor"
26
+ viewBox="0 0 24 24"
27
+ >
28
+ <path
29
+ strokeLinecap="round"
30
+ strokeLinejoin="round"
31
+ strokeWidth={2}
32
+ d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
33
+ />
34
+ <path
35
+ strokeLinecap="round"
36
+ strokeLinejoin="round"
37
+ strokeWidth={2}
38
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
39
+ />
40
+ </svg>
41
+ );
42
+ }
43
+
44
+ function CloudIcon() {
45
+ return (
46
+ <svg
47
+ className="w-4 h-4"
48
+ fill="none"
49
+ stroke="currentColor"
50
+ viewBox="0 0 24 24"
51
+ >
52
+ <path
53
+ strokeLinecap="round"
54
+ strokeLinejoin="round"
55
+ strokeWidth={2}
56
+ d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"
57
+ />
58
+ </svg>
59
+ );
60
+ }
61
+
62
+ function CheckIcon() {
63
+ return (
64
+ <svg
65
+ className="w-4 h-4"
66
+ fill="none"
67
+ stroke="currentColor"
68
+ viewBox="0 0 24 24"
69
+ >
70
+ <path
71
+ strokeLinecap="round"
72
+ strokeLinejoin="round"
73
+ strokeWidth={2}
74
+ d="M5 13l4 4L19 7"
75
+ />
76
+ </svg>
77
+ );
78
+ }
79
+
80
+ function RefreshIcon({ className }: { className?: string }) {
81
+ return (
82
+ <svg
83
+ className={className ?? "w-4 h-4"}
84
+ fill="none"
85
+ stroke="currentColor"
86
+ viewBox="0 0 24 24"
87
+ >
88
+ <path
89
+ strokeLinecap="round"
90
+ strokeLinejoin="round"
91
+ strokeWidth={2}
92
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
93
+ />
94
+ </svg>
95
+ );
96
+ }
97
+
98
+ function CloudUploadIcon({ className }: { className?: string }) {
99
+ return (
100
+ <svg
101
+ className={className ?? "w-4 h-4"}
102
+ fill="none"
103
+ stroke="currentColor"
104
+ viewBox="0 0 24 24"
105
+ >
106
+ <path
107
+ strokeLinecap="round"
108
+ strokeLinejoin="round"
109
+ strokeWidth={2}
110
+ d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
111
+ />
112
+ </svg>
113
+ );
114
+ }
115
+
116
+ function UsersIcon() {
117
+ return (
118
+ <svg
119
+ className="w-4 h-4"
120
+ fill="none"
121
+ stroke="currentColor"
122
+ viewBox="0 0 24 24"
123
+ >
124
+ <path
125
+ strokeLinecap="round"
126
+ strokeLinejoin="round"
127
+ strokeWidth={2}
128
+ d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
129
+ />
130
+ </svg>
131
+ );
132
+ }
133
+
134
+ function formatConfigString(config: DatabaseConfig) {
135
+ return `${config.connection.username}:******@${config.connection.host}:${config.connection.port}/${config.connection.database}`;
136
+ }
137
+
138
+ export function ConnectionPicker({
139
+ databaseConfigs,
140
+ onConnect,
141
+ onAddNew,
142
+ onEdit,
143
+ cloudApiKey,
144
+ onLinkCloud,
145
+ onUnlinkCloud,
146
+ onRefreshCloud,
147
+ isCloudSyncing,
148
+ cloudSyncError,
149
+ onTransferToCloud,
150
+ transferringId,
151
+ onManageMembers,
152
+ }: ConnectionPickerProps) {
153
+ const localConfigs = databaseConfigs.filter((c) => c.source === "local");
154
+ const cloudConfigs = databaseConfigs.filter((c) => c.source === "cloud");
155
+ return (
156
+ <div className="flex flex-col items-center justify-center min-h-full p-8 overflow-auto">
157
+ <div className="w-full max-w-md">
158
+ {/* Cloud status */}
159
+ {CLOUD_ENABLED && (
160
+ <div className="mb-8 p-4 bg-stone-50 dark:bg-white/[0.02] border border-stone-200 dark:border-white/[0.06] rounded-xl">
161
+ <div className="flex items-center justify-between">
162
+ <div className="flex items-center gap-2">
163
+ <CloudIcon />
164
+ <span className="text-[14px] font-medium text-primary">
165
+ dbdiff Cloud
166
+ </span>
167
+ </div>
168
+ {cloudApiKey ? (
169
+ <div className="flex items-center gap-2">
170
+ <span className="flex items-center gap-1 text-[12px] text-green-600 dark:text-green-400">
171
+ <CheckIcon />
172
+ Linked
173
+ </span>
174
+ <button
175
+ onClick={onRefreshCloud}
176
+ disabled={isCloudSyncing}
177
+ className="p-1 rounded text-tertiary hover:text-primary hover:bg-stone-200 dark:hover:bg-white/10 transition-all disabled:opacity-50"
178
+ title="Refresh cloud connections"
179
+ >
180
+ <RefreshIcon
181
+ className={`w-3.5 h-3.5 ${isCloudSyncing ? "animate-spin" : ""}`}
182
+ />
183
+ </button>
184
+ <button
185
+ onClick={onUnlinkCloud}
186
+ className="text-[12px] text-tertiary hover:text-primary transition-colors"
187
+ >
188
+ Unlink
189
+ </button>
190
+ </div>
191
+ ) : (
192
+ <button
193
+ onClick={onLinkCloud}
194
+ className="px-3 py-1.5 text-[12px] font-medium bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
195
+ >
196
+ Link to Cloud
197
+ </button>
198
+ )}
199
+ </div>
200
+ {!cloudApiKey && (
201
+ <p className="text-[12px] text-tertiary mt-2">
202
+ Link to sync connections across devices and share with
203
+ teammates.
204
+ </p>
205
+ )}
206
+ {cloudSyncError && (
207
+ <p className="text-[12px] text-red-500 mt-2">
208
+ Sync error: {cloudSyncError}
209
+ </p>
210
+ )}
211
+ </div>
212
+ )}
213
+
214
+ <h2 className="text-[22px] font-semibold text-primary tracking-[-0.02em] mb-2">
215
+ Connect to a database
216
+ </h2>
217
+ <p className="text-[14px] text-secondary mb-8">
218
+ Select a saved connection to get started
219
+ </p>
220
+
221
+ {/* Cloud connections section */}
222
+ {CLOUD_ENABLED && cloudConfigs.length > 0 && (
223
+ <div className="mb-6">
224
+ <div className="flex items-center gap-2 mb-2">
225
+ <CloudIcon />
226
+ <span className="text-[12px] font-medium text-tertiary uppercase tracking-wide">
227
+ Cloud Connections
228
+ </span>
229
+ </div>
230
+ <div className="space-y-2">
231
+ {cloudConfigs.map((config) => (
232
+ <div
233
+ key={config.id}
234
+ className="group p-4 bg-stone-50 dark:bg-white/[0.02] border border-stone-200 dark:border-white/[0.06] rounded-xl hover:bg-stone-100 dark:hover:bg-white/[0.04] hover:border-stone-300 dark:hover:border-white/[0.1] cursor-pointer transition-all duration-150"
235
+ onClick={() => onConnect(config.id)}
236
+ >
237
+ <div className="flex items-center gap-3">
238
+ <span
239
+ className="w-2.5 h-2.5 rounded-full flex-shrink-0"
240
+ style={{ backgroundColor: config.display.color }}
241
+ />
242
+ <div className="flex-1 min-w-0">
243
+ <div className="flex items-center gap-1.5">
244
+ <span className="text-[14px] font-medium text-primary transition-colors">
245
+ {config.display.name}
246
+ </span>
247
+ {config.cloud?.role === "owner" ? (
248
+ <span className="px-1.5 py-0.5 text-[10px] font-medium bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400 rounded">
249
+ Owner
250
+ </span>
251
+ ) : (
252
+ <span className="px-1.5 py-0.5 text-[10px] font-medium bg-amber-100 dark:bg-amber-500/20 text-amber-600 dark:text-amber-400 rounded">
253
+ {(() => {
254
+ const access = config.cloud?.access;
255
+ if (!access) return "Member";
256
+ const keys = Object.keys(access);
257
+ if (keys.length === 1 && access["*"] === "write")
258
+ return "Full Access";
259
+ if (keys.length === 1 && access["*"] === "read")
260
+ return "Read Only";
261
+ if (keys.length === 1 && access["*"] === "none")
262
+ return "No Access";
263
+ return "Custom Access";
264
+ })()}
265
+ </span>
266
+ )}
267
+ {config.cloud?.role === "owner" && (
268
+ <button
269
+ onClick={(e) => {
270
+ e.stopPropagation();
271
+ onEdit(config);
272
+ }}
273
+ className="p-1 rounded text-interactive-subtle hover:bg-stone-200 dark:hover:bg-white/10 opacity-0 group-hover:opacity-100 transition-all duration-150"
274
+ title="Edit connection"
275
+ >
276
+ <SettingsIcon />
277
+ </button>
278
+ )}
279
+ {config.cloud?.role === "owner" && (
280
+ <button
281
+ onClick={(e) => {
282
+ e.stopPropagation();
283
+ onManageMembers(config);
284
+ }}
285
+ className="p-1 rounded text-interactive-subtle hover:bg-stone-200 dark:hover:bg-white/10 opacity-0 group-hover:opacity-100 transition-all duration-150"
286
+ title="Manage members"
287
+ >
288
+ <UsersIcon />
289
+ </button>
290
+ )}
291
+ </div>
292
+ <div className="text-[12px] text-tertiary font-mono mt-0.5 truncate">
293
+ {formatConfigString(config)}
294
+ </div>
295
+ </div>
296
+ <span className="text-interactive-subtle group-hover:text-tertiary transition-colors text-lg">
297
+
298
+ </span>
299
+ </div>
300
+ </div>
301
+ ))}
302
+ </div>
303
+ </div>
304
+ )}
305
+
306
+ {/* Local connections section */}
307
+ {localConfigs.length > 0 && (
308
+ <div className="mb-4">
309
+ {CLOUD_ENABLED && cloudConfigs.length > 0 && (
310
+ <div className="flex items-center gap-2 mb-2">
311
+ <span className="text-[12px] font-medium text-tertiary uppercase tracking-wide">
312
+ Local Connections
313
+ </span>
314
+ </div>
315
+ )}
316
+ <div className="space-y-2">
317
+ {localConfigs.map((config) => (
318
+ <div
319
+ key={config.id}
320
+ className="group p-4 bg-stone-50 dark:bg-white/[0.02] border border-stone-200 dark:border-white/[0.06] rounded-xl hover:bg-stone-100 dark:hover:bg-white/[0.04] hover:border-stone-300 dark:hover:border-white/[0.1] cursor-pointer transition-all duration-150"
321
+ onClick={() => onConnect(config.id)}
322
+ >
323
+ <div className="flex items-center gap-3">
324
+ <span
325
+ className="w-2.5 h-2.5 rounded-full flex-shrink-0"
326
+ style={{ backgroundColor: config.display.color }}
327
+ />
328
+ <div className="flex-1 min-w-0">
329
+ <div className="flex items-center gap-1.5">
330
+ <span className="text-[14px] font-medium text-primary transition-colors">
331
+ {config.display.name}
332
+ </span>
333
+ <button
334
+ onClick={(e) => {
335
+ e.stopPropagation();
336
+ onEdit(config);
337
+ }}
338
+ className="p-1 rounded text-interactive-subtle hover:bg-stone-200 dark:hover:bg-white/10 opacity-0 group-hover:opacity-100 transition-all duration-150"
339
+ title="Edit connection"
340
+ >
341
+ <SettingsIcon />
342
+ </button>
343
+ {CLOUD_ENABLED && cloudApiKey && (
344
+ <button
345
+ onClick={(e) => {
346
+ e.stopPropagation();
347
+ onTransferToCloud(config);
348
+ }}
349
+ disabled={transferringId === config.id}
350
+ className="p-1 rounded text-interactive-subtle hover:bg-stone-200 dark:hover:bg-white/10 opacity-0 group-hover:opacity-100 transition-all duration-150 disabled:opacity-50"
351
+ title="Transfer to cloud"
352
+ >
353
+ <CloudUploadIcon
354
+ className={`w-4 h-4 ${transferringId === config.id ? "animate-pulse" : ""}`}
355
+ />
356
+ </button>
357
+ )}
358
+ </div>
359
+ <div className="text-[12px] text-tertiary font-mono mt-0.5 truncate">
360
+ {formatConfigString(config)}
361
+ </div>
362
+ </div>
363
+ <span className="text-interactive-subtle group-hover:text-tertiary transition-colors text-lg">
364
+
365
+ </span>
366
+ </div>
367
+ </div>
368
+ ))}
369
+ </div>
370
+ </div>
371
+ )}
372
+ <button
373
+ onClick={onAddNew}
374
+ className="w-full mt-4 p-4 border border-dashed border-stone-300 dark:border-white/[0.12] rounded-xl text-[13px] text-interactive hover:border-stone-400 dark:hover:border-white/25 transition-all duration-150 cursor-pointer"
375
+ >
376
+ + Add new connection
377
+ </button>
378
+ </div>
379
+ </div>
380
+ );
381
+ }