@riligar/elysia-sqlite 1.1.0 → 1.1.4

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 (72) hide show
  1. package/package.json +6 -3
  2. package/src/ui/dist/assets/_baseUniq-CtjpPx06.js +1 -0
  3. package/src/ui/dist/assets/arc-BFeA2lFC.js +1 -0
  4. package/src/ui/dist/assets/architectureDiagram-VXUJARFQ-cJh390m5.js +36 -0
  5. package/src/ui/dist/assets/blockDiagram-VD42YOAC-BhDUcRai.js +122 -0
  6. package/src/ui/dist/assets/c4Diagram-YG6GDRKO-CMTMP654.js +10 -0
  7. package/src/ui/dist/assets/channel-DVo75U_z.js +1 -0
  8. package/src/ui/dist/assets/chunk-4BX2VUAB-hf_dt5BT.js +1 -0
  9. package/src/ui/dist/assets/chunk-55IACEB6-xClE-JKq.js +1 -0
  10. package/src/ui/dist/assets/chunk-B4BG7PRW-D6pt13Ec.js +165 -0
  11. package/src/ui/dist/assets/chunk-DI55MBZ5-Dj9Qg80I.js +220 -0
  12. package/src/ui/dist/assets/chunk-FMBD7UC4-1ZlekxIE.js +15 -0
  13. package/src/ui/dist/assets/chunk-QN33PNHL-DAmen2cg.js +1 -0
  14. package/src/ui/dist/assets/chunk-QZHKN3VN-C-3XzKoW.js +1 -0
  15. package/src/ui/dist/assets/chunk-TZMSLE5B-bM4mxnvE.js +1 -0
  16. package/src/ui/dist/assets/classDiagram-2ON5EDUG-DQalC8tR.js +1 -0
  17. package/src/ui/dist/assets/classDiagram-v2-WZHVMYZB-DQalC8tR.js +1 -0
  18. package/src/ui/dist/assets/clone-VeB9KXOY.js +1 -0
  19. package/src/ui/dist/assets/cose-bilkent-S5V4N54A-BxFitB5E.js +1 -0
  20. package/src/ui/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
  21. package/src/ui/dist/assets/dagre-6UL2VRFP-DZSkvetS.js +4 -0
  22. package/src/ui/dist/assets/defaultLocale-C4B-KCzX.js +1 -0
  23. package/src/ui/dist/assets/diagram-PSM6KHXK-CYnji4ok.js +24 -0
  24. package/src/ui/dist/assets/diagram-QEK2KX5R-BlF0GwV_.js +43 -0
  25. package/src/ui/dist/assets/diagram-S2PKOQOG-BMf9iQYN.js +24 -0
  26. package/src/ui/dist/assets/erDiagram-Q2GNP2WA-B9nG6AIS.js +60 -0
  27. package/src/ui/dist/assets/flowDiagram-NV44I4VS-cRvIpj4q.js +162 -0
  28. package/src/ui/dist/assets/ganttDiagram-JELNMOA3-D5PauD1v.js +267 -0
  29. package/src/ui/dist/assets/gitGraphDiagram-NY62KEGX-BPtCepD0.js +65 -0
  30. package/src/ui/dist/assets/graph-C9jtFm6q.js +1 -0
  31. package/src/ui/dist/assets/index-D564tWvR.js +525 -0
  32. package/src/ui/dist/assets/index-DXG21mfz.css +1 -0
  33. package/src/ui/dist/assets/infoDiagram-WHAUD3N6-0hbOLvdM.js +2 -0
  34. package/src/ui/dist/assets/init-Gi6I4Gst.js +1 -0
  35. package/src/ui/dist/assets/journeyDiagram-XKPGCS4Q-CBy5upkf.js +139 -0
  36. package/src/ui/dist/assets/kanban-definition-3W4ZIXB7-DrobDgMJ.js +89 -0
  37. package/src/ui/dist/assets/katex-Cu_Erd72.js +261 -0
  38. package/src/ui/dist/assets/layout-DM9qC5Az.js +1 -0
  39. package/src/ui/dist/assets/linear-Fky2CJQk.js +1 -0
  40. package/src/ui/dist/assets/min-Bjr2uZ4W.js +1 -0
  41. package/src/ui/dist/assets/mindmap-definition-VGOIOE7T-CIlFmeVI.js +68 -0
  42. package/src/ui/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  43. package/src/ui/dist/assets/pieDiagram-ADFJNKIX-DMIZSQUQ.js +30 -0
  44. package/src/ui/dist/assets/quadrantDiagram-AYHSOK5B-DBCuVneU.js +7 -0
  45. package/src/ui/dist/assets/requirementDiagram-UZGBJVZJ-Dc87Li5r.js +64 -0
  46. package/src/ui/dist/assets/sankeyDiagram-TZEHDZUN-SnjPjzIj.js +10 -0
  47. package/src/ui/dist/assets/sequenceDiagram-WL72ISMW-CegtRWdp.js +145 -0
  48. package/src/ui/dist/assets/stateDiagram-FKZM4ZOC-DRXLGy91.js +1 -0
  49. package/src/ui/dist/assets/stateDiagram-v2-4FDKWEC3-DfAIcXf2.js +1 -0
  50. package/src/ui/dist/assets/timeline-definition-IT6M3QCI-Dfw0Almx.js +61 -0
  51. package/src/ui/dist/assets/treemap-KMMF4GRG-HxWgHRrr.js +128 -0
  52. package/src/ui/dist/assets/xychartDiagram-PRI3JC2R-C86J7PBK.js +7 -0
  53. package/src/ui/{index.html → dist/index.html} +2 -1
  54. package/src/ui/bun.lock +0 -612
  55. package/src/ui/package.json +0 -29
  56. package/src/ui/postcss.config.cjs +0 -14
  57. package/src/ui/src/App.jsx +0 -2103
  58. package/src/ui/src/components/DataGrid.jsx +0 -122
  59. package/src/ui/src/components/EditableCell.jsx +0 -166
  60. package/src/ui/src/components/ExportButton.jsx +0 -95
  61. package/src/ui/src/components/FKPreview.jsx +0 -106
  62. package/src/ui/src/components/Filter.jsx +0 -302
  63. package/src/ui/src/components/HoldButton.jsx +0 -230
  64. package/src/ui/src/components/Login.jsx +0 -148
  65. package/src/ui/src/components/Onboarding.jsx +0 -127
  66. package/src/ui/src/components/Pagination.jsx +0 -35
  67. package/src/ui/src/components/SecuritySettings.jsx +0 -273
  68. package/src/ui/src/components/TableSelector.jsx +0 -75
  69. package/src/ui/src/hooks/useFilter.js +0 -120
  70. package/src/ui/src/index.css +0 -123
  71. package/src/ui/src/main.jsx +0 -115
  72. package/src/ui/vite.config.js +0 -19
@@ -1,2103 +0,0 @@
1
- import { useState, useEffect, useCallback, useMemo } from "react";
2
- import {
3
- AppShell,
4
- NavLink,
5
- Text,
6
- Group,
7
- ActionIcon,
8
- TextInput,
9
- Button,
10
- Table,
11
- Checkbox,
12
- Badge,
13
- Modal,
14
- Select,
15
- Textarea,
16
- Menu,
17
- Kbd,
18
- Box,
19
- Tooltip,
20
- useMantineColorScheme,
21
- ScrollArea,
22
- Paper,
23
- Divider,
24
- Stack,
25
- Loader,
26
- Center,
27
- SimpleGrid,
28
- Title,
29
- ThemeIcon,
30
- HoverCard,
31
- Switch,
32
- } from "@mantine/core";
33
- import { useDisclosure, useHotkeys, useLocalStorage } from "@mantine/hooks";
34
- import { notifications } from "@mantine/notifications";
35
- import mermaid from "mermaid";
36
- import {
37
- IconDatabase,
38
- IconTable,
39
- IconPlus,
40
- IconRefresh,
41
- IconFilter,
42
- IconDownload,
43
- IconSchema,
44
- IconSearch,
45
- IconChevronLeft,
46
- IconChevronRight,
47
- IconTerminal2,
48
- IconPlayerPlay,
49
- IconHistory,
50
- IconStar,
51
- IconStarFilled,
52
- IconTrash,
53
- IconDots,
54
- IconSun,
55
- IconMoon,
56
- IconCommand,
57
- IconX,
58
- IconKey,
59
- IconLink,
60
- IconLetterA,
61
- IconHash,
62
- IconCalendar,
63
- IconCheck,
64
- IconSparkles,
65
- IconSitemap,
66
- IconSettings,
67
- IconUser,
68
- IconDeviceLaptop,
69
- IconLogout,
70
- IconSelector,
71
- IconFileText,
72
- } from "@tabler/icons-react";
73
-
74
- import { useFilter } from "./hooks/useFilter";
75
- import { Filter } from "./components/Filter";
76
- import { ExportButton } from "./components/ExportButton";
77
- import { ButtonDelete } from "./components/HoldButton";
78
- import { Onboarding } from "./components/Onboarding";
79
- import { Login } from "./components/Login";
80
- import { SecuritySettings } from "./components/SecuritySettings";
81
- import { TableSelector } from "./components/TableSelector";
82
- import { DataGrid } from "./components/DataGrid";
83
- import { Pagination } from "./components/Pagination";
84
-
85
- const API = "/admin/api";
86
-
87
- // Column type icon mapping
88
- const getColumnIcon = (type, name) => {
89
- const t = (type || "").toUpperCase();
90
- const n = (name || "").toLowerCase();
91
-
92
- if (n.includes("email")) return <IconLetterA size={14} />;
93
- if (n.includes("date") || n.includes("time"))
94
- return <IconCalendar size={14} />;
95
- if (t.includes("INT") || t.includes("REAL") || t.includes("FLOAT"))
96
- return <IconHash size={14} />;
97
- if (t.includes("BOOL")) return <IconCheck size={14} />;
98
- return <IconLetterA size={14} />;
99
- };
100
-
101
- // Tag colors
102
- const tagColors = ["gray"];
103
- const getTagColor = (value) => {
104
- if (!value) return "gray";
105
- const hash = String(value)
106
- .split("")
107
- .reduce((a, b) => a + b.charCodeAt(0), 0);
108
- return tagColors[hash % tagColors.length];
109
- };
110
-
111
- const isTagColumn = (name) => {
112
- const n = (name || "").toLowerCase();
113
- return (
114
- n.includes("status") ||
115
- n.includes("categoria") ||
116
- n.includes("category") ||
117
- n === "cor"
118
- );
119
- };
120
-
121
- // New Record Modal Component with FK Support
122
- const NewRecordModal = ({
123
- opened,
124
- onClose,
125
- columns,
126
- currentTable,
127
- onSuccess,
128
- }) => {
129
- // ... (modal implementation)
130
- const [formData, setFormData] = useState({});
131
- const [fkOptionsMap, setFkOptionsMap] = useState({});
132
- const [loadingFk, setLoadingFk] = useState({});
133
- const [saving, setSaving] = useState(false);
134
-
135
- // Reset form when modal opens/closes
136
- useEffect(() => {
137
- if (opened) {
138
- setFormData({});
139
- // Load FK options for all FK columns
140
- const fkColumns = columns.filter((c) => c.pk !== 1 && c.fk);
141
- fkColumns.forEach((col) => {
142
- loadFkOptions(col);
143
- });
144
- }
145
- }, [opened, columns]);
146
-
147
- const loadFkOptions = async (col) => {
148
- if (!col.fk) return;
149
-
150
- setLoadingFk((prev) => ({ ...prev, [col.name]: true }));
151
-
152
- try {
153
- const res = await fetch(
154
- `${API}/table/${currentTable}/fk-options?refTable=${col.fk.table}&refColumn=${col.fk.column}`
155
- );
156
- const data = await res.json();
157
-
158
- if (data.success) {
159
- setFkOptionsMap((prev) => ({
160
- ...prev,
161
- [col.name]: data.options.map((o) => ({
162
- value: String(o.value),
163
- label: `${o.label} (ID: ${o.value})`,
164
- })),
165
- }));
166
- }
167
- } catch (err) {
168
- console.error("Error loading FK options:", err);
169
- }
170
-
171
- setLoadingFk((prev) => ({ ...prev, [col.name]: false }));
172
- };
173
-
174
- const handleFieldChange = (colName, value) => {
175
- setFormData((prev) => ({
176
- ...prev,
177
- [colName]: value,
178
- }));
179
- };
180
-
181
- const handleSave = async () => {
182
- setSaving(true);
183
-
184
- try {
185
- const res = await fetch(`${API}/table/${currentTable}/insert`, {
186
- method: "POST",
187
- headers: { "Content-Type": "application/json" },
188
- body: JSON.stringify(formData),
189
- });
190
- const result = await res.json();
191
-
192
- if (result.success) {
193
- notifications.show({
194
- title: "Success",
195
- message: "Record created!",
196
- color: "green",
197
- });
198
- onSuccess();
199
- } else {
200
- notifications.show({
201
- title: "Error",
202
- message: result.error,
203
- color: "red",
204
- });
205
- }
206
- } catch (err) {
207
- notifications.show({
208
- title: "Error",
209
- message: "Failed to save",
210
- color: "red",
211
- });
212
- }
213
-
214
- setSaving(false);
215
- };
216
-
217
- const renderField = (col) => {
218
- const hasFK = !!col.fk;
219
- const isRequired = col.notnull === 1;
220
- const label = `${col.name}${isRequired ? " *" : ""}`;
221
-
222
- if (hasFK) {
223
- const options = fkOptionsMap[col.name] || [];
224
- const isLoading = loadingFk[col.name];
225
-
226
- return (
227
- <Select
228
- key={col.name}
229
- label={label}
230
- placeholder={isLoading ? "Loading..." : `Select ${col.fk.table}`}
231
- data={options}
232
- value={formData[col.name] || null}
233
- onChange={(value) => handleFieldChange(col.name, value)}
234
- searchable
235
- clearable
236
- disabled={isLoading}
237
- leftSection={<IconKey size={14} />}
238
- />
239
- );
240
- }
241
-
242
- return (
243
- <TextInput
244
- key={col.name}
245
- label={label}
246
- placeholder={col.type || "TEXT"}
247
- value={formData[col.name] || ""}
248
- onChange={(e) => handleFieldChange(col.name, e.target.value)}
249
- />
250
- );
251
- };
252
-
253
- return (
254
- <Modal opened={opened} onClose={onClose} title="New Record">
255
- <Stack>
256
- {columns.filter((c) => c.pk !== 1).map((col) => renderField(col))}
257
- <Group justify="flex-end">
258
- <Button variant="subtle" onClick={onClose} color="gray">
259
- Cancel
260
- </Button>
261
- <Button color="dark" onClick={handleSave} loading={saving}>
262
- Save
263
- </Button>
264
- </Group>
265
- </Stack>
266
- </Modal>
267
- );
268
- };
269
-
270
-
271
- export default function App() {
272
- const { colorScheme, toggleColorScheme } = useMantineColorScheme();
273
- const dark = colorScheme === "dark";
274
-
275
- // Auth State
276
- const [auth, setAuth] = useState(null);
277
-
278
- // Security Modal
279
- const [securityOpened, { open: openSecurity, close: closeSecurity }] = useDisclosure(false);
280
-
281
- // ... (rest of checkAuth and useEffect)
282
- const checkAuth = async () => {
283
- try {
284
- const res = await fetch("/admin/auth/status");
285
- const data = await res.json();
286
- setAuth(data);
287
- } catch (e) {
288
- console.error("Failed to check auth status", e);
289
- }
290
- };
291
-
292
- useEffect(() => {
293
- checkAuth();
294
- }, []);
295
-
296
- const handleLogout = async () => {
297
- try {
298
- await fetch("/admin/auth/logout", { method: "POST" });
299
- setAuth({ ...auth, authenticated: false, user: null });
300
- notifications.show({
301
- title: "Logged out",
302
- message: "You have been successfully logged out",
303
- color: "gray"
304
- });
305
- } catch (e) {
306
- notifications.show({
307
- title: "Error",
308
- message: "Failed to logout",
309
- color: "red"
310
- });
311
- }
312
- };
313
-
314
- // State
315
- const [tables, setTables] = useState([]);
316
- // ... (rest of App state)
317
-
318
- const [currentTable, setCurrentTable] = useState(null);
319
- const [columns, setColumns] = useState([]);
320
- const [rows, setRows] = useState([]);
321
- const [loading, setLoading] = useState(false);
322
- const [page, setPage] = useState(1);
323
- const [total, setTotal] = useState(0);
324
- const [sort, setSort] = useState(null);
325
- const [sortDir, setSortDir] = useState("ASC");
326
- const [selectedRows, setSelectedRows] = useState(new Set());
327
- const [searchQuery, setSearchQuery] = useState("");
328
- const [sqlMode, setSqlMode] = useState(false);
329
- const [sqlQuery, setSqlQuery] = useState("");
330
- const [aiPrompt, setAiPrompt] = useState("");
331
- const [aiLoading, setAiLoading] = useState(false);
332
- const [editingCell, setEditingCell] = useState(null); // { rowPk, column }
333
- const [fkMap, setFkMap] = useState({});
334
-
335
- useEffect(() => {
336
- const resolveFks = async () => {
337
- if (!rows.length || !columns.length) return;
338
-
339
- const fkCols = columns.filter((c) => c.fk);
340
- if (fkCols.length === 0) return;
341
-
342
- const newMap = { ...fkMap };
343
- let hasChanges = false;
344
-
345
- for (const col of fkCols) {
346
- const idsToResolve = new Set();
347
- rows.forEach((row) => {
348
- const val = row[col.name];
349
- if (val != null && !newMap[`${col.fk.table}:${val}`]) {
350
- idsToResolve.add(val);
351
- }
352
- });
353
-
354
- if (idsToResolve.size > 0) {
355
- try {
356
- const res = await fetch(`${API}/resolve-fk`, {
357
- method: "POST",
358
- headers: { "Content-Type": "application/json" },
359
- body: JSON.stringify({
360
- table: col.fk.table,
361
- idColumn: col.fk.column,
362
- ids: Array.from(idsToResolve),
363
- }),
364
- });
365
- const data = await res.json();
366
- if (data.success) {
367
- Object.entries(data.values).forEach(([id, label]) => {
368
- newMap[`${col.fk.table}:${id}`] = label;
369
- });
370
- hasChanges = true;
371
- }
372
- } catch (e) {
373
- console.error("Failed to resolve FKs", e);
374
- }
375
- }
376
- }
377
-
378
- if (hasChanges) {
379
- setFkMap(newMap);
380
- }
381
- };
382
-
383
- resolveFks();
384
- }, [rows, columns]);
385
-
386
- // Persisted state
387
- const [favorites, setFavorites] = useLocalStorage({
388
- key: "sqlite-favorites",
389
- defaultValue: [],
390
- });
391
- const [queryHistory, setQueryHistory] = useLocalStorage({
392
- key: "sqlite-history",
393
- defaultValue: [],
394
- });
395
- const [recentTables, setRecentTables] = useLocalStorage({
396
- key: "sqlite-recents",
397
- defaultValue: [],
398
- });
399
-
400
- // Modals
401
- const [newRecordOpened, { open: openNewRecord, close: closeNewRecord }] =
402
- useDisclosure(false);
403
- const [filterOpened, { open: openFilter, close: closeFilter }] =
404
- useDisclosure(false);
405
- const [schemaOpened, { open: openSchema, close: closeSchema }] =
406
- useDisclosure(false);
407
- const [exportOpened, { open: openExport, close: closeExport }] =
408
- useDisclosure(false);
409
- const [commandOpened, { open: openCommand, close: closeCommand }] =
410
- useDisclosure(false);
411
- const [historyOpened, { toggle: toggleHistory }] = useDisclosure(false);
412
- const [erdOpened, { open: openErd, close: closeErd }] = useDisclosure(false);
413
- const [erdSvg, setErdSvg] = useState("");
414
- const [commandQuery, setCommandQuery] = useState("");
415
- const [commandIndex, setCommandIndex] = useState(0);
416
-
417
- // Filters
418
- const [filters, setFilters] = useState([]);
419
- const [exportFormat, setExportFormat] = useState("csv");
420
-
421
- // Reset SQL Runner state (preserves queryHistory)
422
- const resetSqlRunnerState = () => {
423
- setSqlQuery("");
424
- setAiPrompt("");
425
- setRows([]);
426
- setColumns([]);
427
- if (historyOpened) {
428
- toggleHistory();
429
- }
430
- };
431
-
432
- // Command palette items
433
- const commandActions = [
434
- {
435
- label: "Run SQL",
436
- icon: IconTerminal2,
437
- action: () => {
438
- resetSqlRunnerState();
439
- setSqlMode(true);
440
- closeCommand();
441
- },
442
- },
443
- {
444
- label: "View ER Diagram",
445
- icon: IconSitemap,
446
- action: () => {
447
- loadErd();
448
- closeCommand();
449
- },
450
- },
451
- {
452
- label: "Toggle Theme",
453
- icon: dark ? IconSun : IconMoon,
454
- action: () => {
455
- toggleColorScheme();
456
- closeCommand();
457
- },
458
- },
459
- ];
460
-
461
- const filteredTables = tables.filter((t) =>
462
- t.name.toLowerCase().includes(commandQuery.toLowerCase())
463
- );
464
-
465
- const filteredActions = commandActions.filter((a) =>
466
- a.label.toLowerCase().includes(commandQuery.toLowerCase())
467
- );
468
-
469
- // Combined items for keyboard navigation
470
- const allItems = [
471
- ...filteredTables.slice(0, 8).map((t) => ({ type: "table", name: t.name })),
472
- ...filteredActions.map((a) => ({
473
- type: "action",
474
- label: a.label,
475
- action: a.action,
476
- })),
477
- ];
478
-
479
- const handleCommandKeyDown = (e) => {
480
- if (e.key === "ArrowDown") {
481
- e.preventDefault();
482
- setCommandIndex((i) => Math.min(i + 1, allItems.length - 1));
483
- } else if (e.key === "ArrowUp") {
484
- e.preventDefault();
485
- setCommandIndex((i) => Math.max(i - 1, 0));
486
- } else if (e.key === "Enter" && allItems.length > 0) {
487
- e.preventDefault();
488
- const item = allItems[commandIndex];
489
- if (item.type === "table") {
490
- selectTable(item.name);
491
- setCommandQuery("");
492
- setCommandIndex(0);
493
- closeCommand();
494
- } else {
495
- item.action();
496
- setCommandIndex(0);
497
- }
498
- }
499
- };
500
-
501
- // Hotkeys
502
- useHotkeys([
503
- [
504
- "mod+k",
505
- () => {
506
- setCommandQuery("");
507
- setCommandIndex(0);
508
- openCommand();
509
- },
510
- ],
511
- [
512
- "escape",
513
- () => {
514
- closeCommand();
515
- setCommandQuery("");
516
- setCommandIndex(0);
517
- },
518
- ],
519
- ]);
520
-
521
- // Load tables
522
- useEffect(() => {
523
- if (auth?.authenticated) {
524
- loadTables();
525
- }
526
- }, [auth?.authenticated]);
527
-
528
- const loadTables = async () => {
529
- try {
530
- const res = await fetch(`${API}/tables`);
531
- const data = await res.json();
532
-
533
- if (data.tables) {
534
- const tablesWithCount = await Promise.all(
535
- data.tables.map(async (name) => {
536
- try {
537
- const countRes = await fetch(`${API}/table/${name}/count`);
538
- const countData = await countRes.json();
539
- return { name, count: countData.count || 0 };
540
- } catch {
541
- return { name, count: "?" };
542
- }
543
- })
544
- );
545
- setTables(tablesWithCount);
546
- }
547
- } catch (err) {
548
- notifications.show({
549
- title: "Erro",
550
- message: "Erro ao carregar tabelas",
551
- color: "red",
552
- });
553
- }
554
- };
555
-
556
- const selectTable = async (name) => {
557
- setCurrentTable(name);
558
- setPage(1);
559
- setSort(null);
560
- setSelectedRows(new Set());
561
- setSearchQuery("");
562
- setSearchQuery("");
563
- setSqlMode(false);
564
- setFilters([]);
565
- resetSqlRunnerState();
566
-
567
- // Add to recents
568
- setRecentTables((prev) => {
569
- const filtered = prev.filter((t) => t !== name);
570
- return [name, ...filtered].slice(0, 5);
571
- });
572
-
573
- try {
574
- const res = await fetch(`${API}/table/${name}`);
575
- const data = await res.json();
576
- setColumns(data.columns || []);
577
- } catch {
578
- setColumns([]);
579
- }
580
-
581
- loadData(name);
582
- setFkMap({});
583
- };
584
-
585
- const loadData = async (tableName = currentTable) => {
586
- if (!tableName) return;
587
- setLoading(true);
588
-
589
- const offset = (page - 1) * 50;
590
- let sql = `SELECT * FROM ${tableName}`;
591
-
592
- // Add filters
593
- const conditions = [];
594
- filters.forEach((f) => {
595
- if (f.column && f.value) {
596
- if (f.operator === "LIKE") {
597
- conditions.push(`${f.column} LIKE '%${f.value}%'`);
598
- } else {
599
- conditions.push(`${f.column} ${f.operator} '${f.value}'`);
600
- }
601
- }
602
- });
603
-
604
- if (searchQuery) {
605
- const textCols = columns.filter((c) =>
606
- c.type?.toUpperCase().includes("TEXT")
607
- );
608
- if (textCols.length > 0) {
609
- const searchConds = textCols
610
- .map((c) => `${c.name} LIKE '%${searchQuery}%'`)
611
- .join(" OR ");
612
- conditions.push(`(${searchConds})`);
613
- }
614
- }
615
-
616
- if (conditions.length > 0) {
617
- sql += ` WHERE ${conditions.join(" AND ")}`;
618
- }
619
-
620
- if (sort) sql += ` ORDER BY ${sort} ${sortDir}`;
621
- sql += ` LIMIT 50 OFFSET ${offset}`;
622
-
623
- try {
624
- const [dataRes, countRes] = await Promise.all([
625
- fetch(`${API}/query`, {
626
- method: "POST",
627
- headers: { "Content-Type": "application/json" },
628
- body: JSON.stringify({ sql }),
629
- }),
630
- fetch(`${API}/table/${tableName}/count`),
631
- ]);
632
-
633
- const data = await dataRes.json();
634
- const count = await countRes.json();
635
-
636
- if (data.success) {
637
- setRows(data.rows || []);
638
- setTotal(count.count || 0);
639
- } else {
640
- notifications.show({
641
- title: "Erro",
642
- message: data.error,
643
- color: "red",
644
- });
645
- }
646
- } catch (err) {
647
- notifications.show({
648
- title: "Erro",
649
- message: "Erro ao carregar dados",
650
- color: "red",
651
- });
652
- }
653
-
654
- setLoading(false);
655
- };
656
-
657
- useEffect(() => {
658
- if (currentTable && !sqlMode) {
659
- loadData();
660
- }
661
- }, [page, sort, sortDir, filters, searchQuery]);
662
-
663
- // Toggle favorite
664
- const toggleFavorite = (table) => {
665
- if (favorites.includes(table)) {
666
- setFavorites(favorites.filter((f) => f !== table));
667
- } else {
668
- setFavorites([...favorites, table]);
669
- }
670
- };
671
-
672
- // Run SQL query
673
- const runQuery = async () => {
674
- if (!sqlQuery.trim()) return;
675
- setLoading(true);
676
-
677
- // Add to history
678
- const newHistory = [
679
- { sql: sqlQuery, timestamp: Date.now() },
680
- ...queryHistory.filter((h) => h.sql !== sqlQuery),
681
- ].slice(0, 20);
682
- setQueryHistory(newHistory);
683
-
684
- try {
685
- const res = await fetch(`${API}/query`, {
686
- method: "POST",
687
- headers: { "Content-Type": "application/json" },
688
- body: JSON.stringify({ sql: sqlQuery }),
689
- });
690
- const data = await res.json();
691
-
692
- if (data.success) {
693
- if (data.rows) {
694
- setRows(data.rows);
695
- setColumns(
696
- data.columns?.map((c) => ({ name: c, type: "TEXT" })) || []
697
- );
698
- setTotal(data.rows.length);
699
- } else {
700
- notifications.show({
701
- title: "Sucesso",
702
- message: data.message,
703
- color: "green",
704
- });
705
- loadTables();
706
- }
707
- } else {
708
- notifications.show({
709
- title: "Erro",
710
- message: data.error,
711
- color: "red",
712
- });
713
- }
714
- } catch (err) {
715
- notifications.show({
716
- title: "Erro",
717
- message: "Erro ao executar query",
718
- color: "red",
719
- });
720
- }
721
-
722
- setLoading(false);
723
- };
724
-
725
- // Delete record
726
- const deleteRecord = async (pk) => {
727
- const pkCol = columns.find((c) => c.pk === 1)?.name || columns[0]?.name;
728
-
729
- try {
730
- const res = await fetch(`${API}/table/${currentTable}/delete`, {
731
- method: "POST",
732
- headers: { "Content-Type": "application/json" },
733
- body: JSON.stringify({ column: pkCol, value: pk }),
734
- });
735
- const result = await res.json();
736
-
737
- if (result.success) {
738
- notifications.show({
739
- title: "Sucesso",
740
- message: "Excluído!",
741
- color: "green",
742
- });
743
- loadData();
744
- loadTables();
745
- } else {
746
- notifications.show({
747
- title: "Erro",
748
- message: result.error,
749
- color: "red",
750
- });
751
- }
752
- } catch (err) {
753
- notifications.show({
754
- title: "Erro",
755
- message: "Erro ao excluir",
756
- color: "red",
757
- });
758
- }
759
- };
760
-
761
- // Bulk delete
762
- const bulkDelete = async () => {
763
- const pkCol = columns.find((c) => c.pk === 1)?.name || columns[0]?.name;
764
-
765
- for (const pk of selectedRows) {
766
- await fetch(`${API}/table/${currentTable}/delete`, {
767
- method: "POST",
768
- headers: { "Content-Type": "application/json" },
769
- body: JSON.stringify({ column: pkCol, value: pk }),
770
- });
771
- }
772
-
773
- setSelectedRows(new Set());
774
- notifications.show({
775
- title: "Sucesso",
776
- message: "Excluídos!",
777
- color: "green",
778
- });
779
- loadData();
780
- loadTables();
781
- };
782
-
783
- // Export
784
- const doExport = async () => {
785
- let exportRows = rows;
786
-
787
- if (selectedRows.size > 0) {
788
- const pkCol = columns.find((c) => c.pk === 1)?.name || columns[0]?.name;
789
- exportRows = rows.filter((r) => selectedRows.has(String(r[pkCol])));
790
- }
791
-
792
- if (exportFormat === "csv") {
793
- const cols = Object.keys(exportRows[0] || {});
794
- const csv = [
795
- cols.join(","),
796
- ...exportRows.map((r) =>
797
- cols
798
- .map((c) => `"${String(r[c] || "").replace(/"/g, '""')}"`)
799
- .join(",")
800
- ),
801
- ].join("\n");
802
- downloadFile(`${currentTable}.csv`, csv, "text/csv");
803
- } else {
804
- downloadFile(
805
- `${currentTable}.json`,
806
- JSON.stringify(exportRows, null, 2),
807
- "application/json"
808
- );
809
- }
810
-
811
- closeExport();
812
- };
813
-
814
- const downloadFile = (filename, content, type) => {
815
- const blob = new Blob([content], { type });
816
- const url = URL.createObjectURL(blob);
817
- const a = document.createElement("a");
818
- a.href = url;
819
- a.download = filename;
820
- a.click();
821
- URL.revokeObjectURL(url);
822
- closeExport();
823
- };
824
-
825
- const askAi = async () => {
826
- if (!aiPrompt.trim()) return;
827
- setAiLoading(true);
828
-
829
- try {
830
- const res = await fetch(`${API}/ai/sql`, {
831
- method: "POST",
832
- headers: { "Content-Type": "application/json" },
833
- body: JSON.stringify({ prompt: aiPrompt }),
834
- });
835
- const data = await res.json();
836
-
837
- if (data.success) {
838
- setSqlQuery(data.sql);
839
- notifications.show({
840
- title: "AI",
841
- message: "SQL Gerado!",
842
- color: "blue",
843
- icon: <IconSparkles size={16} />,
844
- });
845
- } else {
846
- notifications.show({
847
- title: "Erro AI",
848
- message: data.error,
849
- color: "red",
850
- });
851
- }
852
- } catch (err) {
853
- notifications.show({
854
- title: "Erro",
855
- message: "Erro ao consultar AI",
856
- color: "red",
857
- });
858
- }
859
-
860
- setAiLoading(false);
861
- };
862
-
863
- // Inline edit - update cell
864
- const updateCell = async (rowPk, column, newValue) => {
865
- const pkCol = columns.find((c) => c.pk === 1)?.name || columns[0]?.name;
866
-
867
- try {
868
- const res = await fetch(`${API}/table/${currentTable}/update`, {
869
- method: "POST",
870
- headers: { "Content-Type": "application/json" },
871
- body: JSON.stringify({
872
- column,
873
- value: newValue,
874
- pkColumn: pkCol,
875
- pkValue: rowPk,
876
- }),
877
- });
878
- const result = await res.json();
879
-
880
- if (result.success) {
881
- // Update local state
882
- setRows((prevRows) =>
883
- prevRows.map((r) =>
884
- String(r[pkCol]) === String(rowPk)
885
- ? { ...r, [column]: newValue }
886
- : r
887
- )
888
- );
889
- notifications.show({
890
- title: "Salvo",
891
- message: `${column} atualizado`,
892
- color: "green",
893
- });
894
- } else {
895
- notifications.show({
896
- title: "Erro",
897
- message: result.error,
898
- color: "red",
899
- });
900
- }
901
- } catch (err) {
902
- notifications.show({
903
- title: "Erro",
904
- message: "Erro ao atualizar",
905
- color: "red",
906
- });
907
- }
908
-
909
- setEditingCell(null);
910
- };
911
-
912
- // Load and render ERD
913
- const loadErd = async () => {
914
- try {
915
- const res = await fetch(`${API}/meta/schema`);
916
- const data = await res.json();
917
-
918
- if (data.success) {
919
- // Generate Mermaid syntax
920
- let syntax = "erDiagram\n";
921
-
922
- data.schema.forEach((table) => {
923
- syntax += ` ${table.name} {\n`;
924
- table.columns.forEach((col) => {
925
- const type = col.type || "TEXT";
926
- const key = col.pk ? "PK" : col.fk ? "FK" : "";
927
- syntax += ` ${type} ${col.name} ${key}\n`;
928
- });
929
- syntax += " }\n";
930
-
931
- // Relationships
932
- table.fks.forEach((fk) => {
933
- syntax += ` ${table.name} }o--|| ${fk.table} : "${fk.from}"\n`;
934
- });
935
- });
936
-
937
- mermaid.initialize({
938
- startOnLoad: false,
939
- theme: dark ? "dark" : "neutral",
940
- securityLevel: "loose",
941
- });
942
-
943
- const { svg } = await mermaid.render("erd-graph", syntax);
944
- setErdSvg(svg);
945
- openErd();
946
- }
947
- } catch (err) {
948
- notifications.show({
949
- title: "Erro",
950
- message: "Erro ao gerar ERD",
951
- color: "red",
952
- });
953
- }
954
- };
955
-
956
- // Render table
957
- const pk = columns.find((c) => c.pk === 1)?.name || columns[0]?.name;
958
-
959
- // Editable Cell Component with FK support
960
- // Icon logic
961
- const getTableIcon = (name) => {
962
- if (!name) return <IconDatabase size={32} />;
963
- const n = name.toLowerCase();
964
- if (n.includes("user") || n.includes("usu")) return <IconStar size={32} />;
965
- if (n.includes("prod")) return <IconSparkles size={32} />;
966
- if (n.includes("order") || n.includes("ped"))
967
- return <IconTable size={32} />;
968
- // Deterministic random icon
969
- const icons = [IconDatabase, IconTable, IconSitemap, IconCommand, IconHash];
970
- const index = name.charCodeAt(0) % icons.length;
971
- const Icon = icons[index];
972
- return <Icon size={32} />;
973
- };
974
-
975
- // Filter Logic
976
- const filterKeys = useMemo(() => columns.map((c) => c.name), [columns]);
977
-
978
- // Create a filter options object
979
- const filterOptions = useMemo(() => {
980
- const options = {};
981
- columns.forEach((col) => {
982
- // Get unique values from current rows for this column
983
- const values = new Set(
984
- rows
985
- .map((r) => r[col.name])
986
- .filter((v) => v !== null && v !== undefined)
987
- );
988
- // Limit to 20 options to keep UI clean
989
- options[col.name] = Array.from(values).map(String).slice(0, 20);
990
- });
991
- return options;
992
- }, [columns, rows]);
993
-
994
- const {
995
- filteredItems: filteredRows,
996
- data: filterData,
997
- setData: filterSetData,
998
- } = useFilter(rows, filterKeys);
999
-
1000
- const mainApp = (
1001
- <AppShell navbar={{ width: 260, breakpoint: "sm" }} padding="md">
1002
- <AppShell.Navbar
1003
- p="xs"
1004
- style={{
1005
- backgroundColor: "#FBFAF8",
1006
- borderRight: "1px solid #E8E5E0",
1007
- }}
1008
- >
1009
- <Group justify="space-between" mb="lg" px="xs" mt="xs">
1010
- <Group gap={4}>
1011
- <IconDatabase stroke={2.5} size={24} color="#37352F" />
1012
- <Text fw={700} size="md" c="#37352F">
1013
- SQLite Admin
1014
- </Text>
1015
- </Group>
1016
- </Group>
1017
-
1018
- <Stack gap={4}>
1019
- <Text
1020
- size="xs"
1021
- fw={500}
1022
- c="#91918E"
1023
- px="xs"
1024
- mb={4}
1025
- style={{
1026
- textTransform: "uppercase",
1027
- fontSize: "11px",
1028
- letterSpacing: "0.03em",
1029
- }}
1030
- >
1031
- Platform
1032
- </Text>
1033
- <NavLink
1034
- label="Home"
1035
- leftSection={<IconCommand size={16} />}
1036
- onClick={() => {
1037
- setCurrentTable(null);
1038
- setSqlMode(false);
1039
- resetSqlRunnerState();
1040
- }}
1041
- style={{ borderRadius: 6 }}
1042
- active={!currentTable && !sqlMode}
1043
- />
1044
- <NavLink
1045
- label="Search"
1046
- leftSection={<IconSearch size={16} />}
1047
- onClick={() => openCommand()}
1048
- style={{ borderRadius: 6 }}
1049
- />
1050
- <NavLink
1051
- label="SQL Runner"
1052
- leftSection={<IconTerminal2 size={16} />}
1053
- active={sqlMode}
1054
- onClick={() => {
1055
- if (sqlMode) {
1056
- // Exiting SQL Runner - reset state
1057
- setSqlMode(false);
1058
- resetSqlRunnerState();
1059
- } else {
1060
- // Entering SQL Runner
1061
- resetSqlRunnerState();
1062
- setSqlMode(true);
1063
- setCurrentTable(null);
1064
- }
1065
- }}
1066
- style={{ borderRadius: 6 }}
1067
- />
1068
- <NavLink
1069
- label="ER Diagram"
1070
- leftSection={<IconSitemap size={16} />}
1071
- onClick={() => loadErd()}
1072
- style={{ borderRadius: 6 }}
1073
- />
1074
- </Stack>
1075
-
1076
- <Divider my="sm" color="#E8E5E0" />
1077
-
1078
- <TableSelector
1079
- tables={tables}
1080
- favorites={favorites}
1081
- currentTable={currentTable}
1082
- onSelectTable={selectTable}
1083
- />
1084
-
1085
- <Divider my="sm" color="#E8E5E0" />
1086
-
1087
- {/* User Menu */}
1088
- <Menu shadow="md" width={200} position="right-end">
1089
- <Menu.Target>
1090
- <Button
1091
- variant="subtle"
1092
- color="gray"
1093
- fullWidth
1094
- justify="space-between"
1095
- size="md"
1096
- px={8}
1097
- py={4}
1098
- style={{
1099
- height: "auto",
1100
- borderRadius: 6,
1101
- color: "var(--mantine-color-text)",
1102
- }}
1103
- >
1104
- <Group gap="xs">
1105
- <Box
1106
- style={{
1107
- width: 28,
1108
- height: 28,
1109
- borderRadius: "50%",
1110
- backgroundColor: "#E5E7EB",
1111
- display: "flex",
1112
- alignItems: "center",
1113
- justifyContent: "center",
1114
- color: "#374151",
1115
- fontSize: 12,
1116
- fontWeight: 600,
1117
- }}
1118
- >
1119
- {auth?.user?.[0]?.toUpperCase() || "U"}
1120
- </Box>
1121
- <Box style={{ textAlign: "left" }}>
1122
- <Text size="sm" fw={500} lh={1.2}>
1123
- {auth?.user || "User"}
1124
- </Text>
1125
- <Text size="xs" c="dimmed" lh={1.2}>
1126
- sqlite@local
1127
- </Text>
1128
- </Box>
1129
- </Group>
1130
- <IconSelector size={14} color="gray" />
1131
- </Button>
1132
- </Menu.Target>
1133
-
1134
- <Menu.Dropdown>
1135
- <Menu.Label>Application</Menu.Label>
1136
- <Menu.Item
1137
- closeMenuOnClick={false}
1138
- onClick={() => toggleColorScheme()}
1139
- rightSection={
1140
- <Switch
1141
- checked={dark}
1142
- size="sm"
1143
- onLabel={<IconMoon size={12} stroke={2.5} color="var(--mantine-color-yellow-4)" />}
1144
- offLabel={<IconSun size={12} stroke={2.5} color="var(--mantine-color-gray-6)" />}
1145
- readOnly
1146
- style={{ pointerEvents: "none" }}
1147
- />
1148
- }
1149
- >
1150
- Dark Mode
1151
- </Menu.Item>
1152
- <Menu.Item leftSection={<IconSettings size={14} />} onClick={openSecurity}>
1153
- Settings
1154
- </Menu.Item>
1155
-
1156
- <Menu.Divider />
1157
-
1158
- <Menu.Item color="red" leftSection={<IconLogout size={14} />} onClick={handleLogout}>
1159
- Logout
1160
- </Menu.Item>
1161
- </Menu.Dropdown>
1162
- </Menu>
1163
- </AppShell.Navbar>
1164
-
1165
- <AppShell.Main>
1166
- <SecuritySettings opened={securityOpened} onClose={closeSecurity} />
1167
- {sqlMode ? (
1168
-
1169
- // ... (rest of AppShell content)
1170
-
1171
-
1172
- // SQL MODE LAYOUT
1173
- <Box p="xl" style={{ maxWidth: 900, margin: "0 auto" }}>
1174
- <Group mb="xl" gap="sm">
1175
- <IconTerminal2 size={42} />
1176
- <Title order={1}>SQL Runner</Title>
1177
- </Group>
1178
-
1179
- <Paper
1180
- p="md"
1181
- withBorder
1182
- radius="md"
1183
- mb="lg"
1184
- bg={dark ? "dark.6" : "gray.0"}
1185
- style={{
1186
- borderColor: dark
1187
- ? "var(--mantine-color-dark-4)"
1188
- : "var(--mantine-color-gray-3)",
1189
- }}
1190
- >
1191
- <Group gap="sm">
1192
- <IconSparkles size={20} style={{ opacity: 0.6 }} />
1193
- <TextInput
1194
- placeholder="Ask AI to write SQL..."
1195
- style={{ flex: 1 }}
1196
- value={aiPrompt}
1197
- onChange={(e) => setAiPrompt(e.target.value)}
1198
- onKeyDown={(e) => e.key === "Enter" && askAi()}
1199
- />
1200
- {aiLoading && <Loader size="xs" color="gray" />}
1201
- </Group>
1202
- <Text size="xs" c="dimmed" align="right" mt="xs">
1203
- Press ENTER to generate your query
1204
- </Text>
1205
- </Paper>
1206
-
1207
- <Textarea
1208
- label="Query"
1209
- placeholder="SELECT * FROM ..."
1210
- minRows={5}
1211
- value={sqlQuery}
1212
- onChange={(e) => setSqlQuery(e.target.value)}
1213
- styles={{ input: { fontFamily: "monospace" } }}
1214
- mb="md"
1215
- />
1216
-
1217
- <Group justify="flex-end">
1218
- <Button variant="subtle" onClick={toggleHistory} color="gray">
1219
- History
1220
- </Button>
1221
- <Button color="dark" onClick={runQuery}>
1222
- Run Query
1223
- </Button>
1224
- </Group>
1225
-
1226
- {/* History */}
1227
- {historyOpened && (
1228
- <Paper withBorder p="md" radius="md" mt="xl">
1229
- <Text fw={600} mb="sm">
1230
- Query History
1231
- </Text>
1232
- <Stack gap="xs">
1233
- {queryHistory.length === 0 ? (
1234
- <Text size="sm" c="dimmed" fs="italic">
1235
- No history yet.
1236
- </Text>
1237
- ) : (
1238
- queryHistory.slice(0, 10).map((h, i) => (
1239
- <Group key={i} justify="space-between" wrap="nowrap">
1240
- <Text
1241
- size="sm"
1242
- style={{
1243
- fontFamily: "monospace",
1244
- cursor: "pointer",
1245
- flex: 1,
1246
- }}
1247
- onClick={() => setSqlQuery(h.sql)}
1248
- lineClamp={1}
1249
- >
1250
- {h.sql}
1251
- </Text>
1252
- <Group gap="xs" wrap="nowrap">
1253
- <Text size="xs" c="dimmed">
1254
- {new Date(h.timestamp).toLocaleTimeString()}
1255
- </Text>
1256
- <ActionIcon
1257
- variant="subtle"
1258
- color="dark"
1259
- size="sm"
1260
- onClick={() => {
1261
- setSqlQuery(h.sql);
1262
- setTimeout(() => runQuery(), 100);
1263
- }}
1264
- >
1265
- <IconPlayerPlay size={14} />
1266
- </ActionIcon>
1267
- </Group>
1268
- </Group>
1269
- ))
1270
- )}
1271
- </Stack>
1272
- </Paper>
1273
- )}
1274
-
1275
- {/* Results */}
1276
- {loading ? (
1277
- <Center py="xl">
1278
- <Loader color="gray" type="dots" />
1279
- </Center>
1280
- ) : (
1281
- rows.length > 0 &&
1282
- sqlMode && (
1283
- <Paper withBorder radius="md" mt="xl">
1284
- <Group
1285
- p="sm"
1286
- justify="space-between"
1287
- style={{
1288
- borderBottom: "1px solid var(--mantine-color-gray-3)",
1289
- }}
1290
- >
1291
- <Text size="sm" c="dimmed">
1292
- {rows.length} result(s)
1293
- </Text>
1294
- <ExportButton
1295
- data={rows}
1296
- columns={columns}
1297
- filename="query_results"
1298
- variant="subtle"
1299
- compact
1300
- />
1301
- </Group>
1302
- <ScrollArea>
1303
- <Table
1304
- striped={false}
1305
- highlightOnHover
1306
- verticalSpacing="xs"
1307
- >
1308
- <Table.Thead>
1309
- <Table.Tr>
1310
- {columns.map((col) => (
1311
- <Table.Th
1312
- key={col.name}
1313
- style={{ borderBottom: "1px solid #eee" }}
1314
- >
1315
- <Text size="xs" fw={500} c="dimmed">
1316
- {col.name}
1317
- </Text>
1318
- </Table.Th>
1319
- ))}
1320
- </Table.Tr>
1321
- </Table.Thead>
1322
- <Table.Tbody>
1323
- {rows.map((row, idx) => (
1324
- <Table.Tr key={idx}>
1325
- {columns.map((col) => (
1326
- <Table.Td
1327
- key={col.name}
1328
- style={{ borderBottom: "1px solid #f5f5f5" }}
1329
- >
1330
- <Text
1331
- size="sm"
1332
- lineClamp={2}
1333
- title={String(row[col.name])}
1334
- >
1335
- {col.fk ? (
1336
- <Group gap={6} wrap="nowrap">
1337
- <Badge
1338
- variant="outline"
1339
- color="gray"
1340
- size="sm"
1341
- leftSection={<IconLink size={10} />}
1342
- styles={{ label: { fontWeight: 500 } }}
1343
- >
1344
- {String(row[col.name])}
1345
- </Badge>
1346
- {fkMap[
1347
- `${col.fk.table}:${row[col.name]}`
1348
- ] && (
1349
- <Text
1350
- size="xs"
1351
- c="dimmed"
1352
- lineClamp={1}
1353
- >
1354
- {
1355
- fkMap[
1356
- `${col.fk.table}:${row[col.name]}`
1357
- ]
1358
- }
1359
- </Text>
1360
- )}
1361
- </Group>
1362
- ) : row[col.name] != null ? (
1363
- String(row[col.name])
1364
- ) : (
1365
- <Text span c="dimmed" fs="italic">
1366
- NULL
1367
- </Text>
1368
- )}
1369
- </Text>
1370
- </Table.Td>
1371
- ))}
1372
- </Table.Tr>
1373
- ))}
1374
- </Table.Tbody>
1375
- </Table>
1376
- </ScrollArea>
1377
- </Paper>
1378
- )
1379
- )}
1380
- </Box>
1381
- ) : !currentTable ? (
1382
- // DASHBOARD / OVERVIEW (NOTION STYLE)
1383
- <Box p="xl" style={{ maxWidth: 960, margin: "0 auto" }}>
1384
- {/* 1. Header Area */}
1385
- <Group align="flex-start" gap="md" mb={48} mt="xl">
1386
- <ThemeIcon size={72} variant="transparent" c="dark">
1387
- <IconDatabase size={72} stroke={1.3} />
1388
- </ThemeIcon>
1389
- <Stack gap={0} mt={4}>
1390
- <Title order={1} fw={700} fz={40}>
1391
- SQLite Admin
1392
- </Title>
1393
- <Text c="dimmed" size="lg">
1394
- Manage your local database schema and data.
1395
- </Text>
1396
- </Stack>
1397
- </Group>
1398
-
1399
- {/* 2. Gallery Section (Tools) */}
1400
- <Box mb={48}>
1401
- <Title order={3} fw={600} mb="xl" fz={18}>
1402
- Quick Access
1403
- </Title>
1404
-
1405
- <SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xl">
1406
- {/* Card: SQL Runner */}
1407
- <Paper
1408
- withBorder
1409
- p="lg"
1410
- radius="md"
1411
- onClick={() => setSqlMode(true)}
1412
- style={{
1413
- cursor: "pointer",
1414
- transition: "box-shadow 0.2s",
1415
- flexDirection: "column",
1416
- justifyContent: "space-between",
1417
- }}
1418
- className="editable-cell"
1419
- h={140}
1420
- display="flex"
1421
- >
1422
- <Group justify="space-between" align="flex-start">
1423
- <ThemeIcon variant="light" color="gray" size="lg">
1424
- <IconTerminal2 size={20} />
1425
- </ThemeIcon>
1426
- </Group>
1427
- <Box>
1428
- <Text fw={600} size="md">
1429
- SQL Runner
1430
- </Text>
1431
- <Text size="xs" c="dimmed">
1432
- Run queries & AI
1433
- </Text>
1434
- </Box>
1435
- </Paper>
1436
-
1437
- {/* Card: Search */}
1438
- <Paper
1439
- withBorder
1440
- p="lg"
1441
- radius="md"
1442
- style={{
1443
- cursor: "pointer",
1444
- flexDirection: "column",
1445
- justifyContent: "space-between",
1446
- }}
1447
- className="editable-cell"
1448
- h={140}
1449
- display="flex"
1450
- onClick={() => openCommand()}
1451
- >
1452
- <Group justify="space-between" align="flex-start">
1453
- <ThemeIcon variant="light" color="gray" size="lg">
1454
- <IconSearch size={20} />
1455
- </ThemeIcon>
1456
- </Group>
1457
- <Box>
1458
- <Text fw={600} size="md">
1459
- Global Search
1460
- </Text>
1461
- <Text size="xs" c="dimmed">
1462
- Find anything
1463
- </Text>
1464
- </Box>
1465
- </Paper>
1466
-
1467
- {/* Card: Schema */}
1468
- <Paper
1469
- withBorder
1470
- p="lg"
1471
- radius="md"
1472
- style={{
1473
- cursor: "pointer",
1474
- flexDirection: "column",
1475
- justifyContent: "space-between",
1476
- }}
1477
- className="editable-cell"
1478
- h={140}
1479
- display="flex"
1480
- onClick={() => loadErd()}
1481
- >
1482
- <Group justify="space-between" align="flex-start">
1483
- <ThemeIcon variant="light" color="gray" size="lg">
1484
- <IconSitemap size={20} />
1485
- </ThemeIcon>
1486
- </Group>
1487
- <Box>
1488
- <Text fw={600} size="md">
1489
- ER Diagram
1490
- </Text>
1491
- <Text size="xs" c="dimmed">
1492
- Visual Schema
1493
- </Text>
1494
- </Box>
1495
- </Paper>
1496
- </SimpleGrid>
1497
- </Box>
1498
-
1499
- {/* 3. Table Section (Recents) */}
1500
- <Box>
1501
- <Group mb="md" gap="xs">
1502
- <IconTable size={18} />
1503
- <Title order={3} fw={600} fz={18}>
1504
- Recent Activity
1505
- </Title>
1506
- <Divider orientation="vertical" />
1507
- <Text size="sm" c="dimmed" style={{ cursor: "pointer" }}>
1508
- Board
1509
- </Text>
1510
- </Group>
1511
-
1512
- <Paper withBorder radius="sm" overflow="hidden">
1513
- <Table verticalSpacing="xs" striped={false} highlightOnHover>
1514
- <Table.Thead bg="gray.0">
1515
- <Table.Tr>
1516
- <Table.Th style={{ width: "40%" }}>
1517
- <Group gap={4}>
1518
- <Text size="xs" c="dimmed">
1519
- Aa
1520
- </Text>
1521
- Table Name
1522
- </Group>
1523
- </Table.Th>
1524
- <Table.Th>Type</Table.Th>
1525
- <Table.Th>Action</Table.Th>
1526
- </Table.Tr>
1527
- </Table.Thead>
1528
- <Table.Tbody>
1529
- {recentTables.length === 0 ? (
1530
- <Table.Tr>
1531
- <Table.Td colSpan={3}>
1532
- <Text size="sm" c="dimmed" fs="italic" py="xs">
1533
- No recent pages visited.
1534
- </Text>
1535
- </Table.Td>
1536
- </Table.Tr>
1537
- ) : (
1538
- recentTables.map((name) => (
1539
- <Table.Tr
1540
- key={name}
1541
- onClick={() => selectTable(name)}
1542
- style={{ cursor: "pointer" }}
1543
- >
1544
- <Table.Td>
1545
- <Group gap="sm">
1546
- <IconTable size={16} />
1547
- <Text
1548
- size="sm"
1549
- fw={500}
1550
- style={{ borderBottom: "1px solid #e5e7eb" }}
1551
- >
1552
- {name}
1553
- </Text>
1554
- </Group>
1555
- </Table.Td>
1556
- <Table.Td>
1557
- <Badge size="xs" color="gray" variant="light">
1558
- Table
1559
- </Badge>
1560
- </Table.Td>
1561
- <Table.Td>
1562
- <Button
1563
- size="compact-xs"
1564
- variant="subtle"
1565
- color="gray"
1566
- >
1567
- Open
1568
- </Button>
1569
- </Table.Td>
1570
- </Table.Tr>
1571
- ))
1572
- )}
1573
- </Table.Tbody>
1574
- </Table>
1575
- </Paper>
1576
- </Box>
1577
- </Box>
1578
- ) : (
1579
- // TABLE VIEW
1580
- <Box pt="md">
1581
- {/* NOTION-LIKE HEADER */}
1582
- <Box
1583
- px="xl"
1584
- pb="md"
1585
- style={{ borderBottom: "1px solid var(--mantine-color-gray-2)" }}
1586
- >
1587
- <Group align="center" gap="md" mb="xs">
1588
- <IconTable size={32} stroke={1.5} />
1589
- <Title order={1} style={{ fontSize: 32, fontWeight: 700 }}>
1590
- {currentTable}
1591
- </Title>
1592
- </Group>
1593
- </Box>
1594
-
1595
- {/* TOOLBAR */}
1596
- <Group
1597
- justify="space-between"
1598
- px="xl"
1599
- py="sm"
1600
- style={{
1601
- borderBottom: "1px solid #E5E7EB",
1602
- backgroundColor: "#FAFAFA",
1603
- }}
1604
- >
1605
- <Group gap="xs">
1606
- <Button
1607
- size="xs"
1608
- color="dark"
1609
- radius="md"
1610
- leftSection={<IconPlus size={14} />}
1611
- onClick={openNewRecord}
1612
- styles={{
1613
- root: {
1614
- fontWeight: 500,
1615
- letterSpacing: "-0.01em",
1616
- },
1617
- }}
1618
- >
1619
- New
1620
- </Button>
1621
- <Button
1622
- size="xs"
1623
- variant="default"
1624
- radius="md"
1625
- leftSection={<IconSchema size={14} />}
1626
- onClick={openSchema}
1627
- styles={{
1628
- root: {
1629
- fontWeight: 500,
1630
- letterSpacing: "-0.01em",
1631
- },
1632
- }}
1633
- >
1634
- Structure
1635
- </Button>
1636
- <ExportButton
1637
- data={
1638
- selectedRows.size > 0
1639
- ? rows.filter((r) =>
1640
- selectedRows.has(
1641
- String(
1642
- r[
1643
- columns.find((c) => c.pk === 1)?.name ||
1644
- columns[0]?.name
1645
- ]
1646
- )
1647
- )
1648
- )
1649
- : rows
1650
- }
1651
- columns={columns}
1652
- filename={currentTable}
1653
- variant="default"
1654
- />
1655
- </Group>
1656
-
1657
- <Button
1658
- size="xs"
1659
- variant={
1660
- favorites.includes(currentTable) ? "filled" : "default"
1661
- }
1662
- color={favorites.includes(currentTable) ? "dark" : "gray"}
1663
- radius="md"
1664
- leftSection={
1665
- favorites.includes(currentTable) ? (
1666
- <IconStarFilled size={14} />
1667
- ) : (
1668
- <IconStar size={14} />
1669
- )
1670
- }
1671
- onClick={() => toggleFavorite(currentTable)}
1672
- styles={{
1673
- root: {
1674
- fontWeight: 500,
1675
- letterSpacing: "-0.01em",
1676
- },
1677
- }}
1678
- >
1679
- {favorites.includes(currentTable) ? "Favorited" : "Favorite"}
1680
- </Button>
1681
- </Group>
1682
-
1683
- {/* CONTENT */}
1684
- <Box px="xl" py="lg">
1685
- {/* Unified Filter Component */}
1686
- <Box mb="md">
1687
- <Filter
1688
- data={filterData}
1689
- setData={filterSetData}
1690
- filterOptions={filterOptions}
1691
- />
1692
- </Box>
1693
-
1694
- {/* Selection Actions Bar */}
1695
- {selectedRows.size > 0 && (
1696
- <Paper p="xs" bg="dark" radius="sm" mb="md">
1697
- <Group justify="space-between">
1698
- <Text c="white" size="sm">
1699
- {selectedRows.size} selected
1700
- </Text>
1701
- <Group>
1702
- <ButtonDelete
1703
- size="xs"
1704
- color="red"
1705
- variant="white"
1706
- onDelete={bulkDelete}
1707
- >
1708
- Delete
1709
- </ButtonDelete>
1710
- <ExportButton
1711
- data={rows.filter((r) =>
1712
- selectedRows.has(
1713
- String(
1714
- r[
1715
- columns.find((c) => c.pk === 1)?.name ||
1716
- columns[0]?.name
1717
- ]
1718
- )
1719
- )
1720
- )}
1721
- columns={columns}
1722
- filename={`${currentTable}_selected`}
1723
- variant="default"
1724
- />
1725
- <Button
1726
- size="xs"
1727
- variant="subtle"
1728
- c="white"
1729
- onClick={() => setSelectedRows(new Set())}
1730
- >
1731
- Clear
1732
- </Button>
1733
- </Group>
1734
- </Group>
1735
- </Paper>
1736
- )}
1737
-
1738
- {loading ? (
1739
- <Center py="xl">
1740
- <Loader color="gray" type="dots" />
1741
- </Center>
1742
- ) : (
1743
- <>
1744
- <DataGrid
1745
- columns={columns}
1746
- rows={filteredRows}
1747
- pk={pk}
1748
- selectedRows={selectedRows}
1749
- onSelectRow={(pk, checked) => {
1750
- const newSelected = new Set(selectedRows);
1751
- if (checked) {
1752
- newSelected.add(pk);
1753
- } else {
1754
- newSelected.delete(pk);
1755
- }
1756
- setSelectedRows(newSelected);
1757
- }}
1758
- onSelectAll={(checked) => {
1759
- if (checked) {
1760
- setSelectedRows(new Set(rows.map((r) => String(r[pk]))));
1761
- } else {
1762
- setSelectedRows(new Set());
1763
- }
1764
- }}
1765
- onSort={(colName) => {
1766
- if (sort === colName) {
1767
- setSortDir((d) => (d === "ASC" ? "DESC" : "ASC"));
1768
- } else {
1769
- setSort(colName);
1770
- setSortDir("ASC");
1771
- }
1772
- }}
1773
- sort={sort}
1774
- sortDir={sortDir}
1775
- getColumnIcon={getColumnIcon}
1776
- onDeleteRecord={deleteRecord}
1777
- editingCell={editingCell}
1778
- onStartEdit={(rowPk, column) => setEditingCell({ rowPk, column })}
1779
- onUpdateCell={updateCell}
1780
- onCancelEdit={() => setEditingCell(null)}
1781
- fkMap={fkMap}
1782
- API={API}
1783
- currentTable={currentTable}
1784
- getTagColor={getTagColor}
1785
- isTagColumn={isTagColumn}
1786
- />
1787
-
1788
- <Pagination
1789
- page={page}
1790
- total={total}
1791
- onPageChange={setPage}
1792
- />
1793
- </>
1794
- )}
1795
- </Box>
1796
- </Box>
1797
- )}
1798
- </AppShell.Main>
1799
-
1800
- {/* New Record Modal */}
1801
- <NewRecordModal
1802
- opened={newRecordOpened}
1803
- onClose={closeNewRecord}
1804
- columns={columns}
1805
- currentTable={currentTable}
1806
- onSuccess={() => {
1807
- closeNewRecord();
1808
- loadData();
1809
- loadTables();
1810
- }}
1811
- />
1812
-
1813
- {/* Filter Modal */}
1814
- <Modal opened={filterOpened} onClose={closeFilter} title="Filter">
1815
- <Stack>
1816
- {filters.map((f, i) => (
1817
- <Group key={i}>
1818
- <Select
1819
- data={columns.map((c) => c.name)}
1820
- value={f.column}
1821
- onChange={(v) => {
1822
- const newFilters = [...filters];
1823
- newFilters[i].column = v;
1824
- setFilters(newFilters);
1825
- }}
1826
- placeholder="Column"
1827
- style={{ flex: 1 }}
1828
- />
1829
- <Select
1830
- data={[
1831
- { value: "=", label: "=" },
1832
- { value: "!=", label: "≠" },
1833
- { value: ">", label: ">" },
1834
- { value: "<", label: "<" },
1835
- { value: "LIKE", label: "contains" },
1836
- ]}
1837
- value={f.operator}
1838
- onChange={(v) => {
1839
- const newFilters = [...filters];
1840
- newFilters[i].operator = v;
1841
- setFilters(newFilters);
1842
- }}
1843
- w={100}
1844
- />
1845
- <TextInput
1846
- placeholder="Value"
1847
- value={f.value}
1848
- onChange={(e) => {
1849
- const newFilters = [...filters];
1850
- newFilters[i].value = e.target.value;
1851
- setFilters(newFilters);
1852
- }}
1853
- style={{ flex: 1 }}
1854
- />
1855
- <ActionIcon
1856
- color="red"
1857
- variant="subtle"
1858
- onClick={() => setFilters(filters.filter((_, j) => j !== i))}
1859
- >
1860
- <IconX size={16} />
1861
- </ActionIcon>
1862
- </Group>
1863
- ))}
1864
- <Button
1865
- variant="subtle"
1866
- leftSection={<IconPlus size={16} />}
1867
- onClick={() =>
1868
- setFilters([...filters, { column: "", operator: "=", value: "" }])
1869
- }
1870
- >
1871
- Add Filter
1872
- </Button>
1873
- <Group justify="flex-end">
1874
- <Button
1875
- variant="subtle"
1876
- color="gray"
1877
- onClick={() => {
1878
- setFilters([]);
1879
- closeFilter();
1880
- }}
1881
- >
1882
- Clear
1883
- </Button>
1884
- <Button onClick={closeFilter} color="dark">
1885
- Apply
1886
- </Button>
1887
- </Group>
1888
- </Stack>
1889
- </Modal>
1890
-
1891
- {/* Schema Modal */}
1892
- <Modal
1893
- opened={schemaOpened}
1894
- onClose={closeSchema}
1895
- title={`Schema: ${currentTable}`}
1896
- size="lg"
1897
- >
1898
- <Table>
1899
- <Table.Thead>
1900
- <Table.Tr>
1901
- <Table.Th>Column</Table.Th>
1902
- <Table.Th>Type</Table.Th>
1903
- <Table.Th>Nullable</Table.Th>
1904
- <Table.Th>Default</Table.Th>
1905
- <Table.Th>Key</Table.Th>
1906
- </Table.Tr>
1907
- </Table.Thead>
1908
- <Table.Tbody>
1909
- {columns.map((c) => (
1910
- <Table.Tr key={c.name}>
1911
- <Table.Td>{c.name}</Table.Td>
1912
- <Table.Td>{c.type || "TEXT"}</Table.Td>
1913
- <Table.Td>{c.notnull ? "NOT NULL" : "NULL"}</Table.Td>
1914
- <Table.Td>{c.dflt_value || "-"}</Table.Td>
1915
- <Table.Td>
1916
- <Group gap={4}>
1917
- {c.pk ? (
1918
- <Tooltip label="Primary Key">
1919
- <IconKey size={16} />
1920
- </Tooltip>
1921
- ) : null}
1922
- {c.fk ? (
1923
- <Tooltip
1924
- label={`Foreign Key to ${c.fk.table}.${c.fk.column}`}
1925
- >
1926
- <IconLink
1927
- size={16}
1928
- color="var(--mantine-color-indigo-4)"
1929
- />
1930
- </Tooltip>
1931
- ) : null}
1932
- {!c.pk && !c.fk ? "-" : null}
1933
- </Group>
1934
- </Table.Td>
1935
- </Table.Tr>
1936
- ))}
1937
- </Table.Tbody>
1938
- </Table>
1939
- </Modal>
1940
-
1941
- {/* Export Modal */}
1942
- <Modal opened={exportOpened} onClose={closeExport} title="Export">
1943
- <Stack>
1944
- <Select
1945
- label="Format"
1946
- data={[
1947
- { value: "csv", label: "CSV" },
1948
- { value: "json", label: "JSON" },
1949
- ]}
1950
- value={exportFormat}
1951
- onChange={setExportFormat}
1952
- />
1953
- <Text size="sm" c="dimmed">
1954
- {selectedRows.size > 0
1955
- ? `${selectedRows.size} selected`
1956
- : `${rows.length} records on current page`}
1957
- </Text>
1958
- <Group justify="flex-end">
1959
- <Button variant="subtle" onClick={closeExport} color="gray">
1960
- Cancel
1961
- </Button>
1962
- <Button onClick={doExport} color="dark">
1963
- Export
1964
- </Button>
1965
- </Group>
1966
- </Stack>
1967
- </Modal>
1968
-
1969
- <Modal
1970
- opened={commandOpened}
1971
- onClose={() => {
1972
- closeCommand();
1973
- setCommandQuery("");
1974
- setCommandIndex(0);
1975
- }}
1976
- withCloseButton={false}
1977
- size="lg"
1978
- padding={0}
1979
- radius="md"
1980
- yOffset="10vh"
1981
- >
1982
- <TextInput
1983
- placeholder="Search tables, commands..."
1984
- variant="unstyled"
1985
- p="xs"
1986
- value={commandQuery}
1987
- onChange={(e) => {
1988
- setCommandQuery(e.target.value);
1989
- setCommandIndex(0);
1990
- }}
1991
- onKeyDown={handleCommandKeyDown}
1992
- leftSection={<IconSearch size={22} />}
1993
- styles={{ input: { border: "none" } }}
1994
- autoFocus
1995
- />
1996
- <Divider />
1997
- <Stack gap={0} p="xs" mah={400} style={{ overflow: "auto" }}>
1998
- {filteredTables.length > 0 && (
1999
- <>
2000
- <Text size="xs" c="dimmed" px="sm" py={4} fw={600}>
2001
- TABLES
2002
- </Text>
2003
- {filteredTables.slice(0, 8).map((t, i) => (
2004
- <NavLink
2005
- key={t.name}
2006
- label={t.name}
2007
- leftSection={<IconTable size={16} />}
2008
- active={commandIndex === i}
2009
- onClick={() => {
2010
- selectTable(t.name);
2011
- setCommandQuery("");
2012
- setCommandIndex(0);
2013
- closeCommand();
2014
- }}
2015
- style={{ borderRadius: 4 }}
2016
- />
2017
- ))}
2018
- </>
2019
- )}
2020
- {filteredActions.length > 0 && (
2021
- <>
2022
- <Text size="xs" c="dimmed" px="sm" py={4} fw={600} mt="xs">
2023
- ACTIONS
2024
- </Text>
2025
- {filteredActions.map((action, i) => (
2026
- <NavLink
2027
- key={action.label}
2028
- label={action.label}
2029
- leftSection={<action.icon size={16} />}
2030
- active={
2031
- commandIndex === filteredTables.slice(0, 8).length + i
2032
- }
2033
- style={{ borderRadius: 4 }}
2034
- onClick={() => {
2035
- action.action();
2036
- setCommandIndex(0);
2037
- }}
2038
- />
2039
- ))}
2040
- </>
2041
- )}
2042
- {filteredTables.length === 0 && filteredActions.length === 0 && (
2043
- <Text size="sm" c="dimmed" ta="center" py="md">
2044
- No results found for "{commandQuery}"
2045
- </Text>
2046
- )}
2047
- </Stack>
2048
- <Paper bg="gray.0" p="xs" px="md">
2049
- <Group justify="space-between">
2050
- <Text size="xs" c="dimmed">
2051
- <Kbd size="xs">↑</Kbd> <Kbd size="xs">↓</Kbd> to navigate
2052
- </Text>
2053
- <Group gap={4}>
2054
- <Kbd size="xs">enter</Kbd>
2055
- <Text size="xs" c="dimmed">
2056
- to select
2057
- </Text>
2058
- <Kbd size="xs">esc</Kbd>
2059
- <Text size="xs" c="dimmed">
2060
- to close
2061
- </Text>
2062
- </Group>
2063
- </Group>
2064
- </Paper>
2065
- </Modal>
2066
-
2067
- {/* ERD Modal */}
2068
- <Modal
2069
- opened={erdOpened}
2070
- onClose={closeErd}
2071
- title="Entity Relationship Diagram"
2072
- size="100%"
2073
- styles={{ body: { height: "calc(100vh - 100px)", overflow: "hidden" } }}
2074
- >
2075
- <ScrollArea h="100%">
2076
- <div
2077
- dangerouslySetInnerHTML={{ __html: erdSvg }}
2078
- style={{ textAlign: "center", minWidth: "1000px" }}
2079
- />
2080
- </ScrollArea>
2081
- </Modal>
2082
- </AppShell>
2083
- );
2084
-
2085
- // AUTH RENDERING
2086
- if (!auth) {
2087
- return (
2088
- <Center h="100vh">
2089
- <Loader color="dark" type="dots" size="xl" />
2090
- </Center>
2091
- );
2092
- }
2093
-
2094
- if (!auth.configured) {
2095
- return <Onboarding onConfigured={checkAuth} />;
2096
- }
2097
-
2098
- if (!auth.authenticated) {
2099
- return <Login onLogin={checkAuth} />;
2100
- }
2101
-
2102
- return mainApp;
2103
- }