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,208 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import Fuse from "fuse.js";
3
+ import { useHotkey } from "../stores/hooks";
4
+ import { useStore } from "../stores/store";
5
+
6
+ interface DatabaseSwitcherProps {
7
+ onClose: () => void;
8
+ }
9
+
10
+ export function DatabaseSwitcher({ onClose }: DatabaseSwitcherProps) {
11
+ const [query, setQuery] = useState("");
12
+ const [selectedIndex, setSelectedIndex] = useState(0);
13
+ const inputRef = useRef<HTMLInputElement>(null);
14
+ const listRef = useRef<HTMLDivElement>(null);
15
+
16
+ const databaseConfigs = useStore((state) => state.databaseConfigs);
17
+ const connectionTabs = useStore((state) => state.connectionTabs);
18
+ const selectConnectionTab = useStore((state) => state.selectConnectionTab);
19
+ const createConnectionTab = useStore((state) => state.createConnectionTab);
20
+ const connectToDatabase = useStore((state) => state.connectToDatabase);
21
+
22
+ // Build list with "already open" status
23
+ const items = useMemo(() => {
24
+ return databaseConfigs.map((config) => {
25
+ const openTab = connectionTabs.find(
26
+ (t) => t.databaseConfigId === config.id,
27
+ );
28
+ return {
29
+ id: config.id,
30
+ name: config.display.name,
31
+ color: config.display.color,
32
+ connectionString: `${config.connection.host}:${config.connection.port}/${config.connection.database}`,
33
+ isOpen: !!openTab,
34
+ openTabId: openTab?.id ?? null,
35
+ };
36
+ });
37
+ }, [databaseConfigs, connectionTabs]);
38
+
39
+ // Fuse.js for fuzzy search
40
+ const fuse = useMemo(
41
+ () =>
42
+ new Fuse(items, {
43
+ keys: ["name", "connectionString"],
44
+ threshold: 0.4,
45
+ ignoreLocation: true,
46
+ }),
47
+ [items],
48
+ );
49
+
50
+ const filteredItems = useMemo(() => {
51
+ if (!query.trim()) return items;
52
+ return fuse.search(query).map((r) => r.item);
53
+ }, [fuse, items, query]);
54
+
55
+ // Reset selection when filter changes
56
+ useEffect(() => {
57
+ setSelectedIndex(0);
58
+ }, [query]);
59
+
60
+ // Auto-focus input
61
+ useEffect(() => {
62
+ inputRef.current?.focus();
63
+ }, []);
64
+
65
+ // Scroll selected item into view
66
+ useEffect(() => {
67
+ const listElement = listRef.current;
68
+ if (!listElement) return;
69
+ const selectedElement = listElement.children[selectedIndex] as HTMLElement;
70
+ if (selectedElement) {
71
+ selectedElement.scrollIntoView({ block: "nearest" });
72
+ }
73
+ }, [selectedIndex]);
74
+
75
+ useHotkey("closeModal", onClose);
76
+
77
+ const handleSelect = useCallback(
78
+ (item: (typeof items)[0]) => {
79
+ if (item.isOpen && item.openTabId) {
80
+ // Switch to existing tab
81
+ selectConnectionTab(item.openTabId);
82
+ } else {
83
+ // Open new connection tab and connect
84
+ createConnectionTab();
85
+ // connectToDatabase operates on the active tab, which is the one we just created
86
+ connectToDatabase(item.id);
87
+ }
88
+ onClose();
89
+ },
90
+ [selectConnectionTab, createConnectionTab, connectToDatabase, onClose],
91
+ );
92
+
93
+ const handleKeyDown = useCallback(
94
+ (e: React.KeyboardEvent) => {
95
+ switch (e.key) {
96
+ case "ArrowDown":
97
+ e.preventDefault();
98
+ setSelectedIndex((i) => Math.min(i + 1, filteredItems.length - 1));
99
+ break;
100
+ case "ArrowUp":
101
+ e.preventDefault();
102
+ setSelectedIndex((i) => Math.max(i - 1, 0));
103
+ break;
104
+ case "Enter":
105
+ e.preventDefault();
106
+ if (filteredItems[selectedIndex]) {
107
+ handleSelect(filteredItems[selectedIndex]);
108
+ }
109
+ break;
110
+ case "Escape":
111
+ e.preventDefault();
112
+ onClose();
113
+ break;
114
+ }
115
+ },
116
+ [filteredItems, selectedIndex, handleSelect, onClose],
117
+ );
118
+
119
+ return (
120
+ <div className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]">
121
+ {/* Backdrop */}
122
+ <div
123
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
124
+ onClick={onClose}
125
+ />
126
+
127
+ {/* Content */}
128
+ <div className="relative bg-white dark:bg-[#1a1a1a] rounded-xl shadow-2xl w-full max-w-lg mx-4 border border-stone-200 dark:border-white/10 overflow-hidden">
129
+ {/* Search input */}
130
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-stone-200 dark:border-white/10">
131
+ <svg
132
+ className="w-5 h-5 text-tertiary"
133
+ fill="none"
134
+ stroke="currentColor"
135
+ viewBox="0 0 24 24"
136
+ >
137
+ <path
138
+ strokeLinecap="round"
139
+ strokeLinejoin="round"
140
+ strokeWidth={2}
141
+ d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"
142
+ />
143
+ </svg>
144
+ <input
145
+ ref={inputRef}
146
+ type="text"
147
+ value={query}
148
+ onChange={(e) => setQuery(e.target.value)}
149
+ onKeyDown={handleKeyDown}
150
+ placeholder="Switch database..."
151
+ className="flex-1 bg-transparent text-primary placeholder-tertiary outline-none text-sm"
152
+ />
153
+ <kbd className="text-xs text-tertiary bg-stone-100 dark:bg-white/5 px-1.5 py-0.5 rounded">
154
+ esc
155
+ </kbd>
156
+ </div>
157
+
158
+ {/* Results list */}
159
+ <div ref={listRef} className="max-h-80 overflow-y-auto">
160
+ {filteredItems.length === 0 ? (
161
+ <div className="px-4 py-8 text-center text-tertiary text-sm">
162
+ {items.length === 0
163
+ ? "No databases configured"
164
+ : "No matching databases"}
165
+ </div>
166
+ ) : (
167
+ filteredItems.map((item, index) => (
168
+ <div
169
+ key={item.id}
170
+ onClick={() => handleSelect(item)}
171
+ onMouseEnter={() => setSelectedIndex(index)}
172
+ className={`
173
+ flex items-center gap-3 px-4 py-2.5 cursor-pointer
174
+ ${
175
+ index === selectedIndex
176
+ ? "bg-stone-100 dark:bg-white/5"
177
+ : "hover:bg-stone-50 dark:hover:bg-white/[0.02]"
178
+ }
179
+ `}
180
+ >
181
+ <span
182
+ className="w-2.5 h-2.5 rounded-full flex-shrink-0"
183
+ style={{ backgroundColor: item.color }}
184
+ />
185
+ <div className="flex-1 min-w-0">
186
+ <span className="text-primary text-sm">{item.name}</span>
187
+ <span className="text-tertiary text-xs ml-2">
188
+ {item.connectionString}
189
+ </span>
190
+ </div>
191
+ {item.isOpen && (
192
+ <span className="text-xs text-tertiary bg-stone-100 dark:bg-white/5 px-1.5 py-0.5 rounded">
193
+ open
194
+ </span>
195
+ )}
196
+ {index === selectedIndex && (
197
+ <kbd className="text-xs text-tertiary bg-stone-100 dark:bg-white/5 px-1.5 py-0.5 rounded">
198
+
199
+ </kbd>
200
+ )}
201
+ </div>
202
+ ))
203
+ )}
204
+ </div>
205
+ </div>
206
+ </div>
207
+ );
208
+ }
@@ -0,0 +1,215 @@
1
+ import type { DiffResponse, DiffTableResult } from "../types";
2
+ import { formatCellValue } from "./DataGrid/utils";
3
+
4
+ interface DiffViewProps {
5
+ diffResult: DiffResponse;
6
+ }
7
+
8
+ export function DiffView({ diffResult }: DiffViewProps) {
9
+ if (diffResult.tables.length === 0) {
10
+ return (
11
+ <div className="flex items-center justify-center h-full text-tertiary text-[13px]">
12
+ No changes detected
13
+ </div>
14
+ );
15
+ }
16
+
17
+ const totalChanges = diffResult.tables.reduce(
18
+ (sum, t) => sum + t.deleted.length + t.added.length + t.modified.length,
19
+ 0,
20
+ );
21
+
22
+ if (totalChanges === 0) {
23
+ return (
24
+ <div className="flex items-center justify-center h-full text-tertiary text-[13px]">
25
+ No changes detected — all rows unchanged
26
+ </div>
27
+ );
28
+ }
29
+
30
+ return (
31
+ <div className="h-full flex flex-col">
32
+ <div className="flex-shrink-0 flex items-center px-4 py-2 border-b border-stone-200 dark:border-white/[0.06] bg-stone-50 dark:bg-white/[0.02]">
33
+ <span className="text-[12px] text-secondary">
34
+ Diff preview (transaction rolled back)
35
+ </span>
36
+ </div>
37
+ <div className="flex-1 min-h-0 overflow-auto">
38
+ {diffResult.tables.map((table) => (
39
+ <DiffTable key={table.tableName} table={table} />
40
+ ))}
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ function DiffTable({ table }: { table: DiffTableResult }) {
47
+ const parts: string[] = [];
48
+ if (table.added.length > 0) parts.push(`${table.added.length} added`);
49
+ if (table.modified.length > 0)
50
+ parts.push(`${table.modified.length} modified`);
51
+ if (table.deleted.length > 0) parts.push(`${table.deleted.length} deleted`);
52
+
53
+ const colNames = table.columns.map((c) => c.name);
54
+
55
+ return (
56
+ <div className="pb-4 h-full flex flex-col">
57
+ {/* Table header */}
58
+ <div className="sticky top-0 z-10 flex items-center gap-2 px-4 py-2 bg-stone-100 dark:bg-white/[0.04] border-b border-stone-200 dark:border-white/[0.06]">
59
+ <span className="text-[13px] font-medium text-primary font-mono">
60
+ {table.tableName}
61
+ </span>
62
+ <span className="text-[12px] text-tertiary">{parts.join(", ")}</span>
63
+ </div>
64
+
65
+ {/* Diff table */}
66
+ <div className="overflow-x-auto flex-1">
67
+ <table className="w-full text-[12px] font-mono border-collapse">
68
+ <thead>
69
+ <tr className="border-b border-stone-200 dark:border-white/[0.06]">
70
+ <th className="w-6 px-1 py-1.5 text-center text-tertiary font-normal" />
71
+ {colNames.map((col) => (
72
+ <th
73
+ key={col}
74
+ className="px-3 py-1.5 text-left font-medium text-secondary whitespace-nowrap"
75
+ >
76
+ {col}
77
+ </th>
78
+ ))}
79
+ </tr>
80
+ </thead>
81
+ <tbody>
82
+ {/* Deleted rows */}
83
+ {table.deleted.map((row, i) => (
84
+ <DiffRow
85
+ key={`del-${i}`}
86
+ type="deleted"
87
+ row={row}
88
+ colNames={colNames}
89
+ />
90
+ ))}
91
+
92
+ {/* Modified rows */}
93
+ {table.modified.map((mod, i) => (
94
+ <ModifiedRows
95
+ key={`mod-${i}`}
96
+ before={mod.before}
97
+ after={mod.after}
98
+ changedColumns={mod.changedColumns}
99
+ colNames={colNames}
100
+ />
101
+ ))}
102
+
103
+ {/* Added rows */}
104
+ {table.added.map((row, i) => (
105
+ <DiffRow
106
+ key={`add-${i}`}
107
+ type="added"
108
+ row={row}
109
+ colNames={colNames}
110
+ />
111
+ ))}
112
+
113
+ {/* Unchanged summary */}
114
+ {table.unchangedCount > 0 && (
115
+ <tr>
116
+ <td
117
+ colSpan={colNames.length + 1}
118
+ className="px-4 py-2 text-center text-tertiary text-[11px] border-t border-stone-200 dark:border-white/[0.06]"
119
+ >
120
+ {table.unchangedCount} row
121
+ {table.unchangedCount !== 1 ? "s" : ""} unchanged
122
+ </td>
123
+ </tr>
124
+ )}
125
+ </tbody>
126
+ </table>
127
+ </div>
128
+ </div>
129
+ );
130
+ }
131
+
132
+ function DiffRow({
133
+ type,
134
+ row,
135
+ colNames,
136
+ highlightCols,
137
+ }: {
138
+ type: "added" | "deleted";
139
+ row: Record<string, unknown>;
140
+ colNames: string[];
141
+ highlightCols?: Set<string>;
142
+ }) {
143
+ const isAdded = type === "added";
144
+ const bgClass = isAdded
145
+ ? "bg-green-50 dark:bg-green-950/30"
146
+ : "bg-red-50 dark:bg-red-950/30";
147
+ const textClass = isAdded
148
+ ? "text-green-800 dark:text-green-300"
149
+ : "text-red-800 dark:text-red-300";
150
+ const gutterClass = isAdded
151
+ ? "text-green-500 dark:text-green-400"
152
+ : "text-red-500 dark:text-red-400";
153
+
154
+ return (
155
+ <tr className={bgClass}>
156
+ <td
157
+ className={`px-1 py-1 text-center font-bold select-none ${gutterClass}`}
158
+ >
159
+ {isAdded ? "+" : "\u2212"}
160
+ </td>
161
+ {colNames.map((col) => {
162
+ const emphasize = highlightCols?.has(col);
163
+ const value = row[col];
164
+ return (
165
+ <td
166
+ key={col}
167
+ className={`px-3 py-1 whitespace-nowrap ${textClass} ${
168
+ emphasize
169
+ ? isAdded
170
+ ? "bg-green-200/50 dark:bg-green-800/40 font-medium"
171
+ : "bg-red-200/50 dark:bg-red-800/40 font-medium"
172
+ : ""
173
+ }`}
174
+ >
175
+ {value === null ? (
176
+ <span className="italic opacity-60">NULL</span>
177
+ ) : (
178
+ formatCellValue(value)
179
+ )}
180
+ </td>
181
+ );
182
+ })}
183
+ </tr>
184
+ );
185
+ }
186
+
187
+ function ModifiedRows({
188
+ before,
189
+ after,
190
+ changedColumns,
191
+ colNames,
192
+ }: {
193
+ before: Record<string, unknown>;
194
+ after: Record<string, unknown>;
195
+ changedColumns: string[];
196
+ colNames: string[];
197
+ }) {
198
+ const changedSet = new Set(changedColumns);
199
+ return (
200
+ <>
201
+ <DiffRow
202
+ type="deleted"
203
+ row={before}
204
+ colNames={colNames}
205
+ highlightCols={changedSet}
206
+ />
207
+ <DiffRow
208
+ type="added"
209
+ row={after}
210
+ colNames={colNames}
211
+ highlightCols={changedSet}
212
+ />
213
+ </>
214
+ );
215
+ }