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,243 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import Fuse from "fuse.js";
3
+ import {
4
+ useHotkey,
5
+ useActiveDatabaseConfig,
6
+ useOpenTableTab,
7
+ } from "../stores/hooks";
8
+
9
+ interface CommandPaletteProps {
10
+ onClose: () => void;
11
+ }
12
+
13
+ interface TableItem {
14
+ schema: string;
15
+ name: string;
16
+ fullName: string;
17
+ }
18
+
19
+ export function CommandPalette({ onClose }: CommandPaletteProps) {
20
+ const [query, setQuery] = useState("");
21
+ const [selectedIndex, setSelectedIndex] = useState(0);
22
+ const inputRef = useRef<HTMLInputElement>(null);
23
+ const listRef = useRef<HTMLDivElement>(null);
24
+
25
+ const activeDatabaseConfig = useActiveDatabaseConfig();
26
+ const openTableTab = useOpenTableTab();
27
+
28
+ // Build flat list of all tables with schema prefix
29
+ const allTables = useMemo((): TableItem[] => {
30
+ const schemas = activeDatabaseConfig?.cache?.schemas ?? [];
31
+ const tables: TableItem[] = [];
32
+
33
+ for (const schema of schemas) {
34
+ for (const table of schema.tables) {
35
+ const fullName =
36
+ schema.name === "public"
37
+ ? table.name
38
+ : `${schema.name}.${table.name}`;
39
+ tables.push({
40
+ schema: schema.name,
41
+ name: table.name,
42
+ fullName,
43
+ });
44
+ }
45
+ }
46
+
47
+ // Sort alphabetically by full name
48
+ return tables.sort((a, b) => a.fullName.localeCompare(b.fullName));
49
+ }, [activeDatabaseConfig]);
50
+
51
+ // Fuse.js instance for fuzzy search
52
+ const fuse = useMemo(
53
+ () =>
54
+ new Fuse(allTables, {
55
+ keys: ["fullName"],
56
+ threshold: 0.4,
57
+ ignoreLocation: true,
58
+ }),
59
+ [allTables],
60
+ );
61
+
62
+ // Filter tables based on query (fuzzy)
63
+ const filteredTables = useMemo(() => {
64
+ if (!query.trim()) return allTables;
65
+ return fuse.search(query).map((r) => r.item);
66
+ }, [fuse, allTables, query]);
67
+
68
+ // Reset selection when filter changes
69
+ useEffect(() => {
70
+ setSelectedIndex(0);
71
+ }, [query]);
72
+
73
+ // Auto-focus input on mount
74
+ useEffect(() => {
75
+ inputRef.current?.focus();
76
+ }, []);
77
+
78
+ // Scroll selected item into view
79
+ useEffect(() => {
80
+ const listElement = listRef.current;
81
+ if (!listElement) return;
82
+
83
+ const selectedElement = listElement.children[selectedIndex] as HTMLElement;
84
+ if (selectedElement) {
85
+ selectedElement.scrollIntoView({ block: "nearest" });
86
+ }
87
+ }, [selectedIndex]);
88
+
89
+ // Close on escape
90
+ useHotkey("closeModal", onClose);
91
+
92
+ const handleSelect = useCallback(
93
+ (table: TableItem, forceNew = false) => {
94
+ openTableTab(table.fullName, { forceNew });
95
+ onClose();
96
+ },
97
+ [openTableTab, onClose],
98
+ );
99
+
100
+ // Cmd/Ctrl+Enter to force open in new tab — window-level listener
101
+ // so it works reliably regardless of input focus/modifier quirks
102
+ useEffect(() => {
103
+ const handler = (e: KeyboardEvent) => {
104
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
105
+ e.preventDefault();
106
+ e.stopPropagation();
107
+ if (filteredTables[selectedIndex]) {
108
+ handleSelect(filteredTables[selectedIndex], true);
109
+ }
110
+ }
111
+ };
112
+ window.addEventListener("keydown", handler, { capture: true });
113
+ return () =>
114
+ window.removeEventListener("keydown", handler, { capture: true });
115
+ }, [filteredTables, selectedIndex, handleSelect]);
116
+
117
+ const handleKeyDown = useCallback(
118
+ (e: React.KeyboardEvent) => {
119
+ switch (e.key) {
120
+ case "ArrowDown":
121
+ e.preventDefault();
122
+ setSelectedIndex((i) => Math.min(i + 1, filteredTables.length - 1));
123
+ break;
124
+ case "ArrowUp":
125
+ e.preventDefault();
126
+ setSelectedIndex((i) => Math.max(i - 1, 0));
127
+ break;
128
+ case "Enter":
129
+ e.preventDefault();
130
+ if (filteredTables[selectedIndex]) {
131
+ handleSelect(filteredTables[selectedIndex]);
132
+ }
133
+ break;
134
+ case "Escape":
135
+ e.preventDefault();
136
+ onClose();
137
+ break;
138
+ }
139
+ },
140
+ [filteredTables, selectedIndex, handleSelect, onClose],
141
+ );
142
+
143
+ return (
144
+ <div className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]">
145
+ {/* Backdrop */}
146
+ <div
147
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
148
+ onClick={onClose}
149
+ />
150
+
151
+ {/* Content */}
152
+ <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">
153
+ {/* Search input */}
154
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-stone-200 dark:border-white/10">
155
+ <svg
156
+ className="w-5 h-5 text-tertiary"
157
+ fill="none"
158
+ stroke="currentColor"
159
+ viewBox="0 0 24 24"
160
+ >
161
+ <path
162
+ strokeLinecap="round"
163
+ strokeLinejoin="round"
164
+ strokeWidth={2}
165
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
166
+ />
167
+ </svg>
168
+ <input
169
+ ref={inputRef}
170
+ type="text"
171
+ value={query}
172
+ onChange={(e) => setQuery(e.target.value)}
173
+ onKeyDown={handleKeyDown}
174
+ placeholder="Search tables..."
175
+ className="flex-1 bg-transparent text-primary placeholder-tertiary outline-none text-sm"
176
+ />
177
+ <kbd className="text-xs text-tertiary bg-stone-100 dark:bg-white/5 px-1.5 py-0.5 rounded">
178
+ esc
179
+ </kbd>
180
+ </div>
181
+
182
+ {/* Results list */}
183
+ <div ref={listRef} className="max-h-80 overflow-y-auto">
184
+ {filteredTables.length === 0 ? (
185
+ <div className="px-4 py-8 text-center text-tertiary text-sm">
186
+ {allTables.length === 0
187
+ ? "No tables found"
188
+ : "No matching tables"}
189
+ </div>
190
+ ) : (
191
+ filteredTables.map((table, index) => (
192
+ <div
193
+ key={table.fullName}
194
+ onClick={(e) => handleSelect(table, e.metaKey || e.ctrlKey)}
195
+ onMouseEnter={() => setSelectedIndex(index)}
196
+ className={`
197
+ flex items-center gap-3 px-4 py-2.5 cursor-pointer
198
+ ${
199
+ index === selectedIndex
200
+ ? "bg-stone-100 dark:bg-white/5"
201
+ : "hover:bg-stone-50 dark:hover:bg-white/[0.02]"
202
+ }
203
+ `}
204
+ >
205
+ <svg
206
+ className="w-4 h-4 text-tertiary flex-shrink-0"
207
+ fill="none"
208
+ stroke="currentColor"
209
+ viewBox="0 0 24 24"
210
+ >
211
+ <path
212
+ strokeLinecap="round"
213
+ strokeLinejoin="round"
214
+ strokeWidth={2}
215
+ d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
216
+ />
217
+ </svg>
218
+ <div className="flex-1 min-w-0">
219
+ <span className="text-primary text-sm">{table.name}</span>
220
+ {table.schema !== "public" && (
221
+ <span className="text-tertiary text-xs ml-2">
222
+ {table.schema}
223
+ </span>
224
+ )}
225
+ </div>
226
+ {index === selectedIndex && (
227
+ <div className="flex items-center gap-1.5">
228
+ <kbd className="text-xs text-tertiary bg-stone-100 dark:bg-white/5 px-1.5 py-0.5 rounded">
229
+
230
+ </kbd>
231
+ <kbd className="text-xs text-tertiary bg-stone-100 dark:bg-white/5 px-1.5 py-0.5 rounded">
232
+ ⌘↵ new tab
233
+ </kbd>
234
+ </div>
235
+ )}
236
+ </div>
237
+ ))
238
+ )}
239
+ </div>
240
+ </div>
241
+ </div>
242
+ );
243
+ }
@@ -0,0 +1,78 @@
1
+ import type { DatabaseConfig, InnerTab } from "../types";
2
+ import { ConsoleView } from "./ConsoleView";
3
+ import { TableView } from "./TableView";
4
+
5
+ interface ConnectedViewProps {
6
+ name: string;
7
+ databaseConfig: DatabaseConfig | null;
8
+ activeInnerTab: InnerTab | null;
9
+ }
10
+
11
+ export function ConnectedView({
12
+ name,
13
+ databaseConfig,
14
+ activeInnerTab,
15
+ }: ConnectedViewProps) {
16
+ if (activeInnerTab) {
17
+ if (activeInnerTab.type === "console") {
18
+ return <ConsoleView tabId={activeInnerTab.id} />;
19
+ }
20
+
21
+ if (activeInnerTab.type === "table") {
22
+ return (
23
+ <TableView tabId={activeInnerTab.id} tableName={activeInnerTab.name} />
24
+ );
25
+ }
26
+
27
+ // Query tab placeholder
28
+ return (
29
+ <div className="flex flex-col items-center justify-center h-full text-center p-8">
30
+ <div className="w-12 h-12 rounded-xl bg-stone-100 dark:bg-white/[0.04] border border-stone-200 dark:border-white/[0.06] flex items-center justify-center mb-6">
31
+ <svg
32
+ className="w-5 h-5 text-tertiary"
33
+ viewBox="0 0 24 24"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ strokeWidth="1.5"
37
+ >
38
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
39
+ <path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
40
+ </svg>
41
+ </div>
42
+ <p className="text-[15px] text-primary font-medium">
43
+ Query: {activeInnerTab.name}
44
+ </p>
45
+ <p className="text-[13px] text-secondary mt-2 max-w-xs">
46
+ Saved query results
47
+ </p>
48
+ <div className="mt-8 px-4 py-2 rounded-md bg-stone-100 dark:bg-white/[0.04] border border-stone-200 dark:border-white/[0.06]">
49
+ <span className="text-[12px] text-tertiary font-mono">
50
+ {databaseConfig?.connection.host}
51
+ </span>
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <div className="flex flex-col items-center justify-center h-full text-center p-8">
59
+ <div className="w-12 h-12 rounded-xl bg-stone-100 dark:bg-white/[0.04] border border-stone-200 dark:border-white/[0.06] flex items-center justify-center mb-6">
60
+ <span
61
+ className="w-3 h-3 rounded-full"
62
+ style={{ backgroundColor: databaseConfig?.display.color }}
63
+ />
64
+ </div>
65
+ <p className="text-[15px] text-primary font-medium">
66
+ Connected to {name}
67
+ </p>
68
+ <p className="text-[13px] text-secondary mt-2 max-w-xs">
69
+ Select a table from the sidebar to view data
70
+ </p>
71
+ <div className="mt-8 px-4 py-2 rounded-md bg-stone-100 dark:bg-white/[0.04] border border-stone-200 dark:border-white/[0.06]">
72
+ <span className="text-[12px] text-tertiary font-mono">
73
+ {databaseConfig?.connection.host}
74
+ </span>
75
+ </div>
76
+ </div>
77
+ );
78
+ }