ta_data_mas 0.0.2

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 (219) hide show
  1. package/README.md +42 -0
  2. package/apps/api/.prettierrc +4 -0
  3. package/apps/api/README.md +98 -0
  4. package/apps/api/drizzle/0000_right_drax.sql +87 -0
  5. package/apps/api/drizzle/meta/0000_snapshot.json +588 -0
  6. package/apps/api/drizzle/meta/_journal.json +13 -0
  7. package/apps/api/drizzle.config.ts +12 -0
  8. package/apps/api/eslint.config.mjs +35 -0
  9. package/apps/api/nest-cli.json +8 -0
  10. package/apps/api/package.json +99 -0
  11. package/apps/api/src/app.controller.spec.ts +25 -0
  12. package/apps/api/src/app.controller.ts +9 -0
  13. package/apps/api/src/app.module.ts +37 -0
  14. package/apps/api/src/app.service.ts +8 -0
  15. package/apps/api/src/auth/auth.controller.ts +113 -0
  16. package/apps/api/src/auth/auth.module.ts +15 -0
  17. package/apps/api/src/auth/auth.service.ts +309 -0
  18. package/apps/api/src/auth/decorators/current-user-decorator.ts +10 -0
  19. package/apps/api/src/auth/decorators/require-org-role-decorator.ts +7 -0
  20. package/apps/api/src/auth/dto/login.dto.ts +10 -0
  21. package/apps/api/src/auth/dto/register.dto.ts +14 -0
  22. package/apps/api/src/auth/guards/jwt-auth-guard.ts +40 -0
  23. package/apps/api/src/auth/guards/org-role.guards.ts +60 -0
  24. package/apps/api/src/db/db.module.ts +9 -0
  25. package/apps/api/src/db/drizzle.service.ts +16 -0
  26. package/apps/api/src/db/schema/index.ts +6 -0
  27. package/apps/api/src/db/schema/organizations.ts +30 -0
  28. package/apps/api/src/db/schema/projects.ts +26 -0
  29. package/apps/api/src/db/schema/query-history.ts +16 -0
  30. package/apps/api/src/db/schema/storage-buckets.ts +17 -0
  31. package/apps/api/src/db/schema/storage-objects.ts +18 -0
  32. package/apps/api/src/db/schema/users.ts +14 -0
  33. package/apps/api/src/main.ts +51 -0
  34. package/apps/api/src/members/dto/invite-member-dto.ts +6 -0
  35. package/apps/api/src/members/dto/update-role.dto.ts +8 -0
  36. package/apps/api/src/members/invite.service.ts +147 -0
  37. package/apps/api/src/members/members.controller.ts +56 -0
  38. package/apps/api/src/members/members.module.ts +13 -0
  39. package/apps/api/src/members/members.service.ts +85 -0
  40. package/apps/api/src/orgs/dto/create-org.dto.ts +8 -0
  41. package/apps/api/src/orgs/orgs.controller.ts +27 -0
  42. package/apps/api/src/orgs/orgs.module.ts +12 -0
  43. package/apps/api/src/orgs/orgs.service.ts +86 -0
  44. package/apps/api/src/project-api/project-api.controller.ts +95 -0
  45. package/apps/api/src/project-api/project-api.module.ts +12 -0
  46. package/apps/api/src/project-api/project-api.service.ts +196 -0
  47. package/apps/api/src/project-api/project-key.guard.ts +45 -0
  48. package/apps/api/src/project-api/query-parser.ts +129 -0
  49. package/apps/api/src/project-auth/dto/magic-link.dto.ts +7 -0
  50. package/apps/api/src/project-auth/dto/signin.dto.ts +10 -0
  51. package/apps/api/src/project-auth/dto/signup.dto.ts +11 -0
  52. package/apps/api/src/project-auth/project-auth-dashboard.controller.ts +35 -0
  53. package/apps/api/src/project-auth/project-auth.controller.ts +111 -0
  54. package/apps/api/src/project-auth/project-auth.module.ts +13 -0
  55. package/apps/api/src/project-auth/project-auth.service.ts +440 -0
  56. package/apps/api/src/projects/dto/create-project.dto.ts +9 -0
  57. package/apps/api/src/projects/projects.controller.ts +34 -0
  58. package/apps/api/src/projects/projects.module.ts +12 -0
  59. package/apps/api/src/projects/projects.service.ts +136 -0
  60. package/apps/api/src/realtime/realtime-socket.type.ts +14 -0
  61. package/apps/api/src/realtime/realtime.controller.ts +63 -0
  62. package/apps/api/src/realtime/realtime.gateway.ts +150 -0
  63. package/apps/api/src/realtime/realtime.module.ts +14 -0
  64. package/apps/api/src/realtime/realtime.service.ts +101 -0
  65. package/apps/api/src/realtime/trigger.service.ts +95 -0
  66. package/apps/api/src/sql-editor/dto/execute-query.dto.ts +7 -0
  67. package/apps/api/src/sql-editor/sql-editor.controller.ts +28 -0
  68. package/apps/api/src/sql-editor/sql-editor.module.ts +12 -0
  69. package/apps/api/src/sql-editor/sql-editor.service.ts +154 -0
  70. package/apps/api/src/storage/project-storage.controller.ts +140 -0
  71. package/apps/api/src/storage/storage.controller.ts +118 -0
  72. package/apps/api/src/storage/storage.module.ts +15 -0
  73. package/apps/api/src/storage/storage.service.ts +191 -0
  74. package/apps/api/src/storage/uploadthing.ts +23 -0
  75. package/apps/api/src/table-editor/dto/alter-table.dto.ts +15 -0
  76. package/apps/api/src/table-editor/dto/create-table.dto.ts +54 -0
  77. package/apps/api/src/table-editor/table-editor.controller.ts +117 -0
  78. package/apps/api/src/table-editor/table-editor.module.ts +12 -0
  79. package/apps/api/src/table-editor/table-editor.service.ts +366 -0
  80. package/apps/api/test/app.e2e-spec.ts +29 -0
  81. package/apps/api/test/jest-e2e.json +9 -0
  82. package/apps/api/tsconfig.build.json +4 -0
  83. package/apps/api/tsconfig.json +25 -0
  84. package/apps/web/.env.example +12 -0
  85. package/apps/web/AGENTS.md +9 -0
  86. package/apps/web/CLAUDE.md +1 -0
  87. package/apps/web/README.md +36 -0
  88. package/apps/web/components.json +25 -0
  89. package/apps/web/eslint.config.mjs +18 -0
  90. package/apps/web/next.config.ts +11 -0
  91. package/apps/web/package.json +48 -0
  92. package/apps/web/postcss.config.mjs +7 -0
  93. package/apps/web/public/file.svg +1 -0
  94. package/apps/web/public/globe.svg +1 -0
  95. package/apps/web/public/logo.svg +4 -0
  96. package/apps/web/public/next.svg +1 -0
  97. package/apps/web/public/vercel.svg +1 -0
  98. package/apps/web/public/window.svg +1 -0
  99. package/apps/web/src/app/(dashboard)/layout.tsx +33 -0
  100. package/apps/web/src/app/(dashboard)/organizations/(list)/layout.tsx +7 -0
  101. package/apps/web/src/app/(dashboard)/organizations/(list)/new/page.tsx +26 -0
  102. package/apps/web/src/app/(dashboard)/organizations/(list)/page.tsx +44 -0
  103. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/api/page.tsx +13 -0
  104. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/layout.tsx +19 -0
  105. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/page.tsx +10 -0
  106. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/providers/page.tsx +19 -0
  107. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/url-configuration/page.tsx +24 -0
  108. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/users/page.tsx +13 -0
  109. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/database/page.tsx +29 -0
  110. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/realtime/page.tsx +28 -0
  111. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/sql/page.tsx +21 -0
  112. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/storage/page.tsx +28 -0
  113. package/apps/web/src/app/(dashboard)/organizations/[slug]/layout.tsx +20 -0
  114. package/apps/web/src/app/(dashboard)/organizations/[slug]/projects/page.tsx +62 -0
  115. package/apps/web/src/app/(dashboard)/organizations/[slug]/settings/members/page.tsx +28 -0
  116. package/apps/web/src/app/(dashboard)/organizations/[slug]/settings/page.tsx +10 -0
  117. package/apps/web/src/app/(dashboard)/settings/page.tsx +12 -0
  118. package/apps/web/src/app/dashboard/page.tsx +14 -0
  119. package/apps/web/src/app/favicon.ico +0 -0
  120. package/apps/web/src/app/globals.css +115 -0
  121. package/apps/web/src/app/layout.tsx +36 -0
  122. package/apps/web/src/app/login/page.tsx +199 -0
  123. package/apps/web/src/app/page.tsx +100 -0
  124. package/apps/web/src/app/register/page.tsx +218 -0
  125. package/apps/web/src/components/app-sidebar.tsx +301 -0
  126. package/apps/web/src/components/navbar.tsx +63 -0
  127. package/apps/web/src/components/topNav.tsx +68 -0
  128. package/apps/web/src/components/ui/avatar.tsx +112 -0
  129. package/apps/web/src/components/ui/badge.tsx +48 -0
  130. package/apps/web/src/components/ui/button.tsx +66 -0
  131. package/apps/web/src/components/ui/card.tsx +102 -0
  132. package/apps/web/src/components/ui/checkbox.tsx +32 -0
  133. package/apps/web/src/components/ui/dialog.tsx +168 -0
  134. package/apps/web/src/components/ui/dropdown-menu.tsx +269 -0
  135. package/apps/web/src/components/ui/field.tsx +238 -0
  136. package/apps/web/src/components/ui/input.tsx +19 -0
  137. package/apps/web/src/components/ui/label.tsx +24 -0
  138. package/apps/web/src/components/ui/resizable.tsx +50 -0
  139. package/apps/web/src/components/ui/scroll-area.tsx +55 -0
  140. package/apps/web/src/components/ui/select.tsx +192 -0
  141. package/apps/web/src/components/ui/separator.tsx +28 -0
  142. package/apps/web/src/components/ui/sheet.tsx +147 -0
  143. package/apps/web/src/components/ui/sidebar.tsx +703 -0
  144. package/apps/web/src/components/ui/skeleton.tsx +13 -0
  145. package/apps/web/src/components/ui/switch.tsx +33 -0
  146. package/apps/web/src/components/ui/table.tsx +116 -0
  147. package/apps/web/src/components/ui/tooltip.tsx +57 -0
  148. package/apps/web/src/features/api-docs/api-docs-client.tsx +227 -0
  149. package/apps/web/src/features/api-docs/api-docs-helpers.server.ts +86 -0
  150. package/apps/web/src/features/auth/action.ts +67 -0
  151. package/apps/web/src/features/auth/client.schema.ts +13 -0
  152. package/apps/web/src/features/auth/constants.ts +6 -0
  153. package/apps/web/src/features/auth/cookies.ts +86 -0
  154. package/apps/web/src/features/auth/server.schema.ts +11 -0
  155. package/apps/web/src/features/auth/sign-out.ts +17 -0
  156. package/apps/web/src/features/members/action.ts +66 -0
  157. package/apps/web/src/features/members/client.schema.ts +11 -0
  158. package/apps/web/src/features/members/constants.ts +8 -0
  159. package/apps/web/src/features/members/members-client.tsx +198 -0
  160. package/apps/web/src/features/members/members-helpers.server.ts +21 -0
  161. package/apps/web/src/features/members/server.schema.ts +19 -0
  162. package/apps/web/src/features/organization/action.ts +45 -0
  163. package/apps/web/src/features/organization/client.schema.ts +5 -0
  164. package/apps/web/src/features/organization/constants.tsx +3 -0
  165. package/apps/web/src/features/organization/create-org-form.tsx +66 -0
  166. package/apps/web/src/features/organization/organization-helpers.server.ts +35 -0
  167. package/apps/web/src/features/organization/server.schema.ts +10 -0
  168. package/apps/web/src/features/project-auth/project-auth-action.ts +78 -0
  169. package/apps/web/src/features/project-auth/project-auth-client.schema.ts +18 -0
  170. package/apps/web/src/features/project-auth/project-auth-constants.ts +8 -0
  171. package/apps/web/src/features/project-auth/project-auth-helpers.server.ts +39 -0
  172. package/apps/web/src/features/project-auth/project-auth-providers.tsx +222 -0
  173. package/apps/web/src/features/project-auth/project-auth-server.schema.ts +22 -0
  174. package/apps/web/src/features/project-auth/project-auth-shell.tsx +106 -0
  175. package/apps/web/src/features/project-auth/project-auth-url-config.tsx +151 -0
  176. package/apps/web/src/features/project-auth/project-auth-users.tsx +138 -0
  177. package/apps/web/src/features/projects/action.ts +46 -0
  178. package/apps/web/src/features/projects/client.schema.ts +9 -0
  179. package/apps/web/src/features/projects/constants.ts +6 -0
  180. package/apps/web/src/features/projects/create-project-modal.tsx +117 -0
  181. package/apps/web/src/features/projects/project-helpers.server.ts +38 -0
  182. package/apps/web/src/features/projects/server.schema.ts +10 -0
  183. package/apps/web/src/features/realtime/realtime-client.tsx +294 -0
  184. package/apps/web/src/features/realtime/realtime-helpers.server.ts +40 -0
  185. package/apps/web/src/features/sql-editor/sql-editor-client.tsx +324 -0
  186. package/apps/web/src/features/sql-editor/sql-editor-helpers.server.ts +22 -0
  187. package/apps/web/src/features/storage/storage-client.tsx +500 -0
  188. package/apps/web/src/features/storage/storage-helpers.server.ts +36 -0
  189. package/apps/web/src/features/table-editor/action.ts +97 -0
  190. package/apps/web/src/features/table-editor/add-column-dialog.tsx +184 -0
  191. package/apps/web/src/features/table-editor/client.schema.ts +36 -0
  192. package/apps/web/src/features/table-editor/create-table-drawer.tsx +542 -0
  193. package/apps/web/src/features/table-editor/server.schema.ts +41 -0
  194. package/apps/web/src/features/table-editor/table-editor-client.tsx +737 -0
  195. package/apps/web/src/features/table-editor/table-editor-helpers.server.ts +39 -0
  196. package/apps/web/src/hooks/use-mobile.ts +19 -0
  197. package/apps/web/src/lib/axios.ts +8 -0
  198. package/apps/web/src/lib/utils.ts +6 -0
  199. package/apps/web/src/proxy.ts +72 -0
  200. package/apps/web/src/server-utils/utils.ts +10 -0
  201. package/apps/web/tsconfig.json +34 -0
  202. package/package.json +22 -0
  203. package/packages/apiDatabase-js/package.json +39 -0
  204. package/packages/apiDatabase-js/src/auth/index.ts +121 -0
  205. package/packages/apiDatabase-js/src/client.ts +25 -0
  206. package/packages/apiDatabase-js/src/db/index.ts +15 -0
  207. package/packages/apiDatabase-js/src/db/query-builder.ts +194 -0
  208. package/packages/apiDatabase-js/src/index.ts +16 -0
  209. package/packages/apiDatabase-js/src/realtime/index.ts +84 -0
  210. package/packages/apiDatabase-js/src/storage/index.ts +130 -0
  211. package/packages/apiDatabase-js/tsconfig.json +13 -0
  212. package/packages/apiDatabase-js/tsdown.config.ts +20 -0
  213. package/packages/constants/package.json +6 -0
  214. package/packages/constants/src/index.ts +76 -0
  215. package/packages/tsconfig.json +13 -0
  216. package/packages/tsdown.config.ts +20 -0
  217. package/packages/types/package.json +6 -0
  218. package/packages/types/src/index.ts +254 -0
  219. package/pnpm-workspace.yaml +10 -0
@@ -0,0 +1,737 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useMemo, useRef, useEffect } from "react";
4
+ import {
5
+ useReactTable,
6
+ getCoreRowModel,
7
+ flexRender,
8
+ type ColumnDef,
9
+ } from "@tanstack/react-table";
10
+ import {
11
+ ResizablePanelGroup,
12
+ ResizablePanel,
13
+ ResizableHandle,
14
+ } from "@/components/ui/resizable";
15
+ import { ScrollArea } from "@/components/ui/scroll-area";
16
+ import { Button } from "@/components/ui/button";
17
+ import { Input } from "@/components/ui/input";
18
+ import {
19
+ Sheet,
20
+ SheetContent,
21
+ SheetHeader,
22
+ SheetTitle,
23
+ } from "@/components/ui/sheet";
24
+ import {
25
+ DropdownMenu,
26
+ DropdownMenuContent,
27
+ DropdownMenuItem,
28
+ DropdownMenuSeparator,
29
+ DropdownMenuTrigger,
30
+ } from "@/components/ui/dropdown-menu";
31
+ import {
32
+ Table,
33
+ TableBody,
34
+ TableCell,
35
+ TableHead,
36
+ TableHeader,
37
+ TableRow,
38
+ } from "@/components/ui/table";
39
+ import {
40
+ Search,
41
+ Plus,
42
+ MoreVertical,
43
+ Expand,
44
+ Table2,
45
+ X,
46
+ ChevronLeft,
47
+ } from "lucide-react";
48
+ import { cn } from "@/lib/utils";
49
+ import type { TableInfo } from "@apiDatabase/types";
50
+ import { tableEditorAction } from "@/features/table-editor/action";
51
+ import { TABLE_EDITOR_INTENT } from "@apiDatabase/constants";
52
+ import { CreateTableDrawer } from "./create-table-drawer";
53
+ import { AddColumnDialog } from "./add-column-dialog";
54
+
55
+ interface TableEditorClientProps {
56
+ orgSlug: string;
57
+ projectSlug: string;
58
+ dbSchema: string;
59
+ initialTables: string[];
60
+ }
61
+
62
+ interface OpenTab {
63
+ tableName: string;
64
+ info: TableInfo | null;
65
+ rows: Record<string, unknown>[];
66
+ count: number;
67
+ loading: boolean;
68
+ }
69
+
70
+ export function TableEditorClient({
71
+ orgSlug,
72
+ projectSlug,
73
+ dbSchema,
74
+ initialTables,
75
+ }: TableEditorClientProps) {
76
+ const projectKey = `${orgSlug}/${projectSlug}`;
77
+ const [tables, setTables] = useState<string[]>(initialTables);
78
+ const lastProjectKeyRef = useRef(projectKey);
79
+
80
+ useEffect(() => {
81
+ if (lastProjectKeyRef.current !== projectKey) {
82
+ lastProjectKeyRef.current = projectKey;
83
+ setTables(initialTables);
84
+ }
85
+ }, [projectKey, initialTables]);
86
+
87
+ const [search, setSearch] = useState("");
88
+ const [activeTable, setActiveTable] = useState<string | null>(null);
89
+ const [openTabs, setOpenTabs] = useState<OpenTab[]>([]);
90
+ const [activeTab, setActiveTab] = useState<string | null>(null);
91
+ const [expandedRow, setExpandedRow] = useState<Record<
92
+ string,
93
+ unknown
94
+ > | null>(null);
95
+ const [createDrawerOpen, setCreateDrawerOpen] = useState(false);
96
+
97
+ const tableFetchGenRef = useRef<Map<string, number>>(new Map());
98
+
99
+ const beginTableFetch = (tableName: string) => {
100
+ const gen = (tableFetchGenRef.current.get(tableName) ?? 0) + 1;
101
+ tableFetchGenRef.current.set(tableName, gen);
102
+ return gen;
103
+ };
104
+
105
+ const isTableFetchStale = (tableName: string, gen: number) =>
106
+ (tableFetchGenRef.current.get(tableName) ?? 0) !== gen;
107
+
108
+ const actionCtx = { orgSlug, projectSlug };
109
+
110
+ const openTable = useCallback(
111
+ async (tableName: string) => {
112
+ setActiveTable(tableName);
113
+ setActiveTab(tableName);
114
+
115
+ let alreadyOpen = false;
116
+ setOpenTabs((prev) => {
117
+ alreadyOpen = prev.some((t) => t.tableName === tableName);
118
+ if (alreadyOpen) return prev;
119
+ return [
120
+ ...prev,
121
+ { tableName, info: null, rows: [], count: 0, loading: true },
122
+ ];
123
+ });
124
+
125
+ if (alreadyOpen) return;
126
+
127
+ const fetchGen = beginTableFetch(tableName);
128
+
129
+ const formData = new FormData();
130
+ formData.set("intent", TABLE_EDITOR_INTENT.FETCH_TABLE);
131
+ formData.set("tableName", tableName);
132
+
133
+ const result = await tableEditorAction(actionCtx, {}, formData);
134
+
135
+ if (isTableFetchStale(tableName, fetchGen)) return;
136
+
137
+ if (result.error || !result.tableData) {
138
+ setOpenTabs((prev) => prev.filter((t) => t.tableName !== tableName));
139
+ return;
140
+ }
141
+
142
+ const { info, rows, count } = result.tableData;
143
+ setOpenTabs((prev) =>
144
+ prev.map((t) =>
145
+ t.tableName === tableName
146
+ ? { ...t, info, rows, count, loading: false }
147
+ : t,
148
+ ),
149
+ );
150
+ },
151
+ [orgSlug, projectSlug],
152
+ );
153
+
154
+ const refreshTable = useCallback(
155
+ async (tableName: string) => {
156
+ const fetchGen = beginTableFetch(tableName);
157
+
158
+ setOpenTabs((prev) =>
159
+ prev.map((t) =>
160
+ t.tableName === tableName ? { ...t, loading: true } : t,
161
+ ),
162
+ );
163
+
164
+ const formData = new FormData();
165
+ formData.set("intent", TABLE_EDITOR_INTENT.FETCH_TABLE);
166
+ formData.set("tableName", tableName);
167
+
168
+ const result = await tableEditorAction(actionCtx, {}, formData);
169
+
170
+ if (isTableFetchStale(tableName, fetchGen)) return;
171
+
172
+ if (result.error || !result.tableData) {
173
+ setOpenTabs((prev) =>
174
+ prev.map((t) =>
175
+ t.tableName === tableName ? { ...t, loading: false } : t,
176
+ ),
177
+ );
178
+ return;
179
+ }
180
+
181
+ const { info, rows, count } = result.tableData;
182
+ setOpenTabs((prev) =>
183
+ prev.map((t) =>
184
+ t.tableName === tableName
185
+ ? { ...t, info, rows, count, loading: false }
186
+ : t,
187
+ ),
188
+ );
189
+ },
190
+ [orgSlug, projectSlug],
191
+ );
192
+
193
+ const closeTab = useCallback((tableName: string) => {
194
+ setOpenTabs((prev) => {
195
+ const remaining = prev.filter((t) => t.tableName !== tableName);
196
+ setActiveTab((tab) =>
197
+ tab === tableName ? (remaining.at(-1)?.tableName ?? null) : tab,
198
+ );
199
+ return remaining;
200
+ });
201
+ }, []);
202
+
203
+ const deleteTable = async (tableName: string) => {
204
+ beginTableFetch(tableName);
205
+
206
+ setTables((prev) => prev.filter((t) => t !== tableName));
207
+ closeTab(tableName);
208
+ if (activeTable === tableName) setActiveTable(null);
209
+
210
+ const formData = new FormData();
211
+ formData.set("intent", TABLE_EDITOR_INTENT.DELETE_TABLE);
212
+ formData.set("tableName", tableName);
213
+
214
+ const result = await tableEditorAction(actionCtx, {}, formData);
215
+ if (result.error) {
216
+ setTables((prev) => [...prev, tableName].sort());
217
+ return;
218
+ }
219
+ };
220
+
221
+ const onTableCreated = (tableName: string) => {
222
+ setTables((prev) => [...prev, tableName].sort());
223
+ setCreateDrawerOpen(false);
224
+ void openTable(tableName);
225
+ };
226
+
227
+ const filteredTables = tables.filter((t) =>
228
+ t.toLowerCase().includes(search.toLowerCase()),
229
+ );
230
+
231
+ const currentTab = openTabs.find((t) => t.tableName === activeTab);
232
+
233
+ const backToTableList = () => {
234
+ setActiveTab(null);
235
+ setActiveTable(null);
236
+ };
237
+
238
+ const listPanel = (
239
+ <TableListPanel
240
+ dbSchema={dbSchema}
241
+ search={search}
242
+ onSearchChange={setSearch}
243
+ filteredTables={filteredTables}
244
+ activeTable={activeTable}
245
+ onOpenTable={openTable}
246
+ onNewTable={() => setCreateDrawerOpen(true)}
247
+ onDeleteTable={deleteTable}
248
+ className="md:border-r md:border-border"
249
+ />
250
+ );
251
+
252
+ const mainPanel = (
253
+ <EditorMainPanel
254
+ orgSlug={orgSlug}
255
+ projectSlug={projectSlug}
256
+ openTabs={openTabs}
257
+ activeTab={activeTab}
258
+ currentTab={currentTab}
259
+ onSelectTab={setActiveTab}
260
+ onCloseTab={closeTab}
261
+ onExpandRow={setExpandedRow}
262
+ onRefreshTable={refreshTable}
263
+ showMobileBack={false}
264
+ />
265
+ );
266
+
267
+ const mobileMainPanel = (
268
+ <EditorMainPanel
269
+ orgSlug={orgSlug}
270
+ projectSlug={projectSlug}
271
+ openTabs={openTabs}
272
+ activeTab={activeTab}
273
+ currentTab={currentTab}
274
+ onSelectTab={setActiveTab}
275
+ onCloseTab={closeTab}
276
+ onExpandRow={setExpandedRow}
277
+ onRefreshTable={refreshTable}
278
+ showMobileBack
279
+ onMobileBack={backToTableList}
280
+ />
281
+ );
282
+
283
+ return (
284
+ <div className="flex h-full min-h-0 w-full flex-1 overflow-hidden">
285
+ {/* Mobile: full-width list OR table view */}
286
+ <div className="flex h-full min-h-0 w-full flex-col md:hidden">
287
+ {activeTab ? mobileMainPanel : listPanel}
288
+ </div>
289
+
290
+ {/* Desktop: resizable split pane */}
291
+ <div className="hidden h-full min-h-0 w-full md:flex">
292
+ <ResizablePanelGroup
293
+ orientation="horizontal"
294
+ className="h-full w-full min-h-0"
295
+ defaultLayout={{ "table-list": 20, "table-main": 80 }}
296
+ >
297
+ <ResizablePanel
298
+ id="table-list"
299
+ defaultSize="20"
300
+ minSize="15"
301
+ maxSize="30"
302
+ >
303
+ {listPanel}
304
+ </ResizablePanel>
305
+
306
+ <ResizableHandle withHandle />
307
+
308
+ <ResizablePanel id="table-main" defaultSize="80" minSize="50">
309
+ {mainPanel}
310
+ </ResizablePanel>
311
+ </ResizablePanelGroup>
312
+ </div>
313
+
314
+ <Sheet open={!!expandedRow} onOpenChange={() => setExpandedRow(null)}>
315
+ <SheetContent
316
+ side="right"
317
+ className="flex h-full w-full max-w-full flex-col gap-0 p-0 sm:max-w-md"
318
+ >
319
+ <SheetHeader className="border-b px-4 py-4">
320
+ <SheetTitle>
321
+ View row from <span className="font-mono">{activeTab}</span>
322
+ </SheetTitle>
323
+ </SheetHeader>
324
+ {expandedRow && currentTab?.info && (
325
+ <div className="flex-1 space-y-4 overflow-y-auto px-4 py-4">
326
+ {currentTab.info.columns.map((col) => (
327
+ <div key={col.name} className="space-y-1">
328
+ <p className="text-sm font-medium">{col.name}</p>
329
+ <p className="text-xs text-muted-foreground">{col.type}</p>
330
+ {col.foreignKey && (
331
+ <p className="text-xs text-muted-foreground">
332
+ Foreign key →{" "}
333
+ <span className="font-mono">
334
+ {col.foreignKey.table}.{col.foreignKey.column}
335
+ </span>
336
+ </p>
337
+ )}
338
+ <Input
339
+ readOnly
340
+ defaultValue={String(expandedRow[col.name] ?? "")}
341
+ className="font-mono text-xs bg-muted/40"
342
+ />
343
+ </div>
344
+ ))}
345
+ </div>
346
+ )}
347
+ </SheetContent>
348
+ </Sheet>
349
+
350
+ <CreateTableDrawer
351
+ open={createDrawerOpen}
352
+ onOpenChange={setCreateDrawerOpen}
353
+ orgSlug={orgSlug}
354
+ projectSlug={projectSlug}
355
+ existingTables={tables}
356
+ onCreated={onTableCreated}
357
+ />
358
+ </div>
359
+ );
360
+ }
361
+
362
+ function TableListPanel({
363
+ dbSchema,
364
+ search,
365
+ onSearchChange,
366
+ filteredTables,
367
+ activeTable,
368
+ onOpenTable,
369
+ onNewTable,
370
+ onDeleteTable,
371
+ className,
372
+ }: {
373
+ dbSchema: string;
374
+ search: string;
375
+ onSearchChange: (value: string) => void;
376
+ filteredTables: string[];
377
+ activeTable: string | null;
378
+ onOpenTable: (tableName: string) => void;
379
+ onNewTable: () => void;
380
+ onDeleteTable: (tableName: string) => void;
381
+ className?: string;
382
+ }) {
383
+ return (
384
+ <div className={cn("flex h-full min-h-0 flex-col", className)}>
385
+ <div className="flex items-center justify-between border-b border-border px-3 py-2">
386
+ <span className="text-xs text-muted-foreground">
387
+ schema{" "}
388
+ <span className="font-mono font-medium text-foreground">
389
+ {dbSchema}
390
+ </span>
391
+ </span>
392
+ </div>
393
+
394
+ <div className="border-b border-border px-3 py-2">
395
+ <Button
396
+ variant="outline"
397
+ size="sm"
398
+ className="w-full justify-start gap-1.5"
399
+ onClick={onNewTable}
400
+ >
401
+ <Plus size={14} />
402
+ New table
403
+ </Button>
404
+ </div>
405
+
406
+ <div className="border-b border-border px-3 py-2">
407
+ <div className="relative">
408
+ <Search
409
+ size={12}
410
+ className="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground"
411
+ />
412
+ <Input
413
+ placeholder="Search tables..."
414
+ value={search}
415
+ onChange={(e) => onSearchChange(e.target.value)}
416
+ className="h-7 pl-7 text-xs"
417
+ />
418
+ </div>
419
+ </div>
420
+
421
+ <ScrollArea className="min-h-0 flex-1">
422
+ <div className="py-1">
423
+ {filteredTables.map((table) => (
424
+ <div
425
+ key={table}
426
+ className={cn(
427
+ "flex cursor-pointer items-center justify-between px-3 py-2",
428
+ activeTable === table
429
+ ? "bg-accent text-accent-foreground"
430
+ : "hover:bg-accent/50",
431
+ )}
432
+ onClick={() => void onOpenTable(table)}
433
+ >
434
+ <div className="flex min-w-0 items-center gap-2">
435
+ <Table2 size={14} className="shrink-0 text-muted-foreground" />
436
+ <span className="truncate text-sm">{table}</span>
437
+ </div>
438
+
439
+ {activeTable === table && (
440
+ <DropdownMenu>
441
+ <DropdownMenuTrigger asChild>
442
+ <Button
443
+ variant="ghost"
444
+ size="icon"
445
+ className="h-7 w-7 shrink-0"
446
+ onClick={(e) => e.stopPropagation()}
447
+ >
448
+ <MoreVertical size={12} />
449
+ </Button>
450
+ </DropdownMenuTrigger>
451
+ <DropdownMenuContent align="end" className="w-48">
452
+ <DropdownMenuItem
453
+ onSelect={() => void navigator.clipboard.writeText(table)}
454
+ >
455
+ Copy name
456
+ </DropdownMenuItem>
457
+ <DropdownMenuItem
458
+ onSelect={() =>
459
+ void navigator.clipboard.writeText(
460
+ `SELECT * FROM "${table}";`,
461
+ )
462
+ }
463
+ >
464
+ Copy SELECT query
465
+ </DropdownMenuItem>
466
+ <DropdownMenuSeparator />
467
+ <DropdownMenuItem
468
+ className="text-destructive"
469
+ onSelect={() => void onDeleteTable(table)}
470
+ >
471
+ Delete table
472
+ </DropdownMenuItem>
473
+ </DropdownMenuContent>
474
+ </DropdownMenu>
475
+ )}
476
+ </div>
477
+ ))}
478
+ </div>
479
+ </ScrollArea>
480
+ </div>
481
+ );
482
+ }
483
+
484
+ function EditorMainPanel({
485
+ orgSlug,
486
+ projectSlug,
487
+ openTabs,
488
+ activeTab,
489
+ currentTab,
490
+ onSelectTab,
491
+ onCloseTab,
492
+ onExpandRow,
493
+ onRefreshTable,
494
+ showMobileBack,
495
+ onMobileBack,
496
+ }: {
497
+ orgSlug: string;
498
+ projectSlug: string;
499
+ openTabs: OpenTab[];
500
+ activeTab: string | null;
501
+ currentTab: OpenTab | undefined;
502
+ onSelectTab: (tableName: string) => void;
503
+ onCloseTab: (tableName: string) => void;
504
+ onExpandRow: (row: Record<string, unknown>) => void;
505
+ onRefreshTable: (tableName: string) => void;
506
+ showMobileBack: boolean;
507
+ onMobileBack?: () => void;
508
+ }) {
509
+ return (
510
+ <div className="flex h-full min-h-0 flex-col">
511
+ {showMobileBack && activeTab && (
512
+ <div className="flex shrink-0 items-center gap-2 border-b border-border px-2 py-2">
513
+ <Button
514
+ variant="ghost"
515
+ size="icon"
516
+ className="h-8 w-8 shrink-0"
517
+ onClick={onMobileBack}
518
+ >
519
+ <ChevronLeft size={18} />
520
+ </Button>
521
+ <Table2 size={14} className="shrink-0 text-muted-foreground" />
522
+ <span className="truncate text-sm font-medium">{activeTab}</span>
523
+ </div>
524
+ )}
525
+
526
+ <div
527
+ className={cn(
528
+ "flex shrink-0 items-center overflow-x-auto border-b border-border bg-background",
529
+ showMobileBack && openTabs.length <= 1 && "hidden",
530
+ )}
531
+ >
532
+ {openTabs.map((tab) => (
533
+ <div
534
+ key={tab.tableName}
535
+ className={cn(
536
+ "flex h-9 shrink-0 cursor-pointer items-center gap-1.5 border-r border-border px-3 text-sm",
537
+ activeTab === tab.tableName
538
+ ? "bg-background text-foreground"
539
+ : "bg-muted/40 text-muted-foreground hover:bg-muted",
540
+ )}
541
+ onClick={() => onSelectTab(tab.tableName)}
542
+ >
543
+ <Table2 size={12} />
544
+ <span className="max-w-32 truncate sm:max-w-none">
545
+ {tab.tableName}
546
+ </span>
547
+ <button
548
+ onClick={(e) => {
549
+ e.stopPropagation();
550
+ onCloseTab(tab.tableName);
551
+ }}
552
+ className="ml-1 hover:text-foreground"
553
+ >
554
+ <X size={10} />
555
+ </button>
556
+ </div>
557
+ ))}
558
+ </div>
559
+
560
+ {!currentTab ? (
561
+ <div className="flex flex-1 flex-col items-center justify-center text-center px-4">
562
+ <Table2 size={32} className="mb-3 text-muted-foreground" />
563
+ <p className="text-sm text-muted-foreground">
564
+ {showMobileBack
565
+ ? "No table selected"
566
+ : "Select a table from the left panel"}
567
+ </p>
568
+ </div>
569
+ ) : currentTab.loading ? (
570
+ <div className="flex flex-1 items-center justify-center">
571
+ <p className="text-sm text-muted-foreground">Loading...</p>
572
+ </div>
573
+ ) : !currentTab.info ? (
574
+ <div className="flex flex-1 items-center justify-center">
575
+ <p className="text-sm text-muted-foreground">Failed to load table</p>
576
+ </div>
577
+ ) : (
578
+ <DataGrid
579
+ orgSlug={orgSlug}
580
+ projectSlug={projectSlug}
581
+ tableName={currentTab.tableName}
582
+ tableInfo={currentTab.info}
583
+ rows={currentTab.rows}
584
+ count={currentTab.count}
585
+ onExpandRow={onExpandRow}
586
+ onColumnAdded={() => void onRefreshTable(currentTab.tableName)}
587
+ />
588
+ )}
589
+ </div>
590
+ );
591
+ }
592
+
593
+ function DataGrid({
594
+ orgSlug,
595
+ projectSlug,
596
+ tableName,
597
+ tableInfo,
598
+ rows,
599
+ count,
600
+ onExpandRow,
601
+ onColumnAdded,
602
+ }: {
603
+ orgSlug: string;
604
+ projectSlug: string;
605
+ tableName: string;
606
+ tableInfo: TableInfo;
607
+ rows: Record<string, unknown>[];
608
+ count: number;
609
+ onExpandRow: (row: Record<string, unknown>) => void;
610
+ onColumnAdded: () => void;
611
+ }) {
612
+ const columns = useMemo<ColumnDef<Record<string, unknown>>[]>(
613
+ () => [
614
+ {
615
+ id: "expand",
616
+ size: 40,
617
+ cell: ({ row }) => (
618
+ <button
619
+ type="button"
620
+ onClick={() => onExpandRow(row.original)}
621
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-accent rounded"
622
+ >
623
+ <Expand size={12} />
624
+ </button>
625
+ ),
626
+ },
627
+ ...tableInfo.columns.map(
628
+ (col): ColumnDef<Record<string, unknown>> => ({
629
+ id: col.name,
630
+ header: () => (
631
+ <div className="flex items-center gap-1.5">
632
+ {col.isPrimaryKey && <span className="text-yellow-500">🔑</span>}
633
+ {col.foreignKey && <span className="text-blue-500">🔗</span>}
634
+ <span className="font-medium">{col.name}</span>
635
+ <span className="text-muted-foreground font-normal">
636
+ {col.type}
637
+ </span>
638
+ </div>
639
+ ),
640
+ accessorFn: (row) => row[col.name],
641
+ cell: ({ getValue }) => {
642
+ const val = getValue();
643
+ if (val === null || val === undefined) {
644
+ return (
645
+ <span className="text-muted-foreground italic text-xs">
646
+ NULL
647
+ </span>
648
+ );
649
+ }
650
+ if (typeof val === "object") {
651
+ return (
652
+ <span className="font-mono text-xs">{JSON.stringify(val)}</span>
653
+ );
654
+ }
655
+ return <span className="text-sm">{String(val)}</span>;
656
+ },
657
+ }),
658
+ ),
659
+ {
660
+ id: "add-column",
661
+ size: 48,
662
+ header: () => (
663
+ <AddColumnDialog
664
+ orgSlug={orgSlug}
665
+ projectSlug={projectSlug}
666
+ tableName={tableName}
667
+ onAdded={onColumnAdded}
668
+ className="mx-auto"
669
+ />
670
+ ),
671
+ cell: () => null,
672
+ },
673
+ ],
674
+ [tableInfo, onExpandRow, orgSlug, projectSlug, tableName, onColumnAdded],
675
+ );
676
+
677
+ const table = useReactTable({
678
+ data: rows,
679
+ columns,
680
+ getCoreRowModel: getCoreRowModel(),
681
+ });
682
+
683
+ return (
684
+ <div className="flex flex-col flex-1 overflow-hidden">
685
+ <ScrollArea className="flex-1">
686
+ <Table>
687
+ <TableHeader className="sticky top-0 bg-background z-10">
688
+ {table.getHeaderGroups().map((headerGroup) => (
689
+ <TableRow key={headerGroup.id}>
690
+ {headerGroup.headers.map((header) => (
691
+ <TableHead
692
+ key={header.id}
693
+ className={cn(
694
+ "border-r border-border text-xs",
695
+ header.column.id === "add-column" &&
696
+ "sticky right-0 z-20 w-12 min-w-12 border-l border-border bg-background p-0 text-center",
697
+ )}
698
+ >
699
+ {flexRender(
700
+ header.column.columnDef.header,
701
+ header.getContext(),
702
+ )}
703
+ </TableHead>
704
+ ))}
705
+ </TableRow>
706
+ ))}
707
+ </TableHeader>
708
+ <TableBody>
709
+ {table.getRowModel().rows.map((row) => (
710
+ <TableRow key={row.id} className="group">
711
+ {row.getVisibleCells().map((cell) => (
712
+ <TableCell
713
+ key={cell.id}
714
+ className={cn(
715
+ "border-r border-border py-1.5",
716
+ cell.column.id === "add-column" &&
717
+ "sticky right-0 z-10 w-12 min-w-12 border-l border-border bg-background p-0",
718
+ )}
719
+ >
720
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
721
+ </TableCell>
722
+ ))}
723
+ </TableRow>
724
+ ))}
725
+ </TableBody>
726
+ </Table>
727
+ </ScrollArea>
728
+
729
+ <div className="flex items-center justify-between px-4 py-2 border-t border-border text-xs text-muted-foreground shrink-0">
730
+ <span>
731
+ {count} {count === 1 ? "record" : "records"}
732
+ </span>
733
+ <span>100 rows</span>
734
+ </div>
735
+ </div>
736
+ );
737
+ }