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,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
|
+
}
|