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,500 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useMemo } from "react";
4
+ import { UploadButton } from "@uploadthing/react";
5
+ import {
6
+ ResizableHandle,
7
+ ResizablePanel,
8
+ ResizablePanelGroup,
9
+ } from "@/components/ui/resizable";
10
+ import { Button } from "@/components/ui/button";
11
+ import { Badge } from "@/components/ui/badge";
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ DialogTrigger,
18
+ } from "@/components/ui/dialog";
19
+ import { Input } from "@/components/ui/input";
20
+ import { Label } from "@/components/ui/label";
21
+ import {
22
+ DropdownMenu,
23
+ DropdownMenuContent,
24
+ DropdownMenuItem,
25
+ DropdownMenuTrigger,
26
+ } from "@/components/ui/dropdown-menu";
27
+ import {
28
+ FolderOpen,
29
+ Plus,
30
+ Trash2,
31
+ Copy,
32
+ Check,
33
+ MoreVertical,
34
+ File,
35
+ Lock,
36
+ Globe,
37
+ } from "lucide-react";
38
+ import { cn } from "@/lib/utils";
39
+ import type {
40
+ StorageBucket,
41
+ StorageObject,
42
+ BucketAccess,
43
+ } from "@apiDatabase/types";
44
+ import { OurStorageRouter } from "../../../../api/src/storage/uploadthing";
45
+
46
+ interface StorageClientProps {
47
+ orgSlug: string;
48
+ projectSlug: string;
49
+ initialBuckets: StorageBucket[];
50
+ initialObjects?: StorageObject[];
51
+ }
52
+
53
+ function formatBytes(bytes: number): string {
54
+ if (bytes < 1024) return `${bytes} B`;
55
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
56
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
57
+ }
58
+
59
+ function getApiBaseUrl(): string {
60
+ return process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000/api";
61
+ }
62
+
63
+ export function StorageClient({
64
+ orgSlug,
65
+ projectSlug,
66
+ initialBuckets,
67
+ initialObjects = [],
68
+ }: StorageClientProps) {
69
+ const [buckets, setBuckets] = useState<StorageBucket[]>(initialBuckets);
70
+ const [activeBucket, setActiveBucket] = useState<StorageBucket | null>(
71
+ initialBuckets[0] ?? null,
72
+ );
73
+ const [objects, setObjects] = useState<StorageObject[]>(initialObjects);
74
+ const [loadingObjects, setLoadingObjects] = useState(false);
75
+ const [copiedId, setCopiedId] = useState<string | null>(null);
76
+
77
+ const [createOpen, setCreateOpen] = useState(false);
78
+ const [newBucketName, setNewBucketName] = useState("");
79
+ const [newBucketAccess, setNewBucketAccess] =
80
+ useState<BucketAccess>("public");
81
+ const [creating, setCreating] = useState(false);
82
+
83
+ const apiBase = getApiBaseUrl();
84
+ const storageBase = `${apiBase}/orgs/${orgSlug}/projects/${projectSlug}/storage`;
85
+ const apiOrigin = useMemo(() => new URL(apiBase).origin, [apiBase]);
86
+
87
+ const uploadFetch = useCallback(
88
+ (input: RequestInfo | URL, init?: RequestInit) => {
89
+ const href =
90
+ typeof input === "string"
91
+ ? input
92
+ : input instanceof URL
93
+ ? input.href
94
+ : input.url;
95
+ const isOurApi = new URL(href).origin === apiOrigin;
96
+ return fetch(input, {
97
+ ...init,
98
+ credentials: isOurApi ? "include" : init?.credentials,
99
+ });
100
+ },
101
+ [apiOrigin],
102
+ );
103
+
104
+ const uploadUrl = useMemo(() => {
105
+ if (!activeBucket) return "";
106
+ return `${storageBase}/buckets/${activeBucket.id}/upload`;
107
+ }, [storageBase, activeBucket]);
108
+
109
+ const loadObjects = useCallback(
110
+ async (bucket: StorageBucket) => {
111
+ setActiveBucket(bucket);
112
+ setLoadingObjects(true);
113
+ try {
114
+ const res = await fetch(`${storageBase}/buckets/${bucket.id}/objects`, {
115
+ credentials: "include",
116
+ });
117
+ const data = (await res.json()) as StorageObject[];
118
+ setObjects(data);
119
+ } finally {
120
+ setLoadingObjects(false);
121
+ }
122
+ },
123
+ [storageBase],
124
+ );
125
+
126
+ const createBucket = async () => {
127
+ if (!newBucketName.trim()) return;
128
+ setCreating(true);
129
+ try {
130
+ const res = await fetch(`${storageBase}/buckets`, {
131
+ method: "POST",
132
+ headers: { "Content-Type": "application/json" },
133
+ credentials: "include",
134
+ body: JSON.stringify({ name: newBucketName, access: newBucketAccess }),
135
+ });
136
+ const bucket = (await res.json()) as StorageBucket;
137
+ setBuckets((prev) => [...prev, bucket]);
138
+ setCreateOpen(false);
139
+ setNewBucketName("");
140
+ void loadObjects(bucket);
141
+ } finally {
142
+ setCreating(false);
143
+ }
144
+ };
145
+
146
+ const deleteBucket = async (bucketId: string) => {
147
+ await fetch(`${storageBase}/buckets/${bucketId}`, {
148
+ method: "DELETE",
149
+ credentials: "include",
150
+ });
151
+ setBuckets((prev) => prev.filter((b) => b.id !== bucketId));
152
+ if (activeBucket?.id === bucketId) {
153
+ setActiveBucket(null);
154
+ setObjects([]);
155
+ }
156
+ };
157
+
158
+ const deleteObject = async (objectId: string) => {
159
+ await fetch(`${storageBase}/objects/${objectId}`, {
160
+ method: "DELETE",
161
+ credentials: "include",
162
+ });
163
+ setObjects((prev) => prev.filter((o) => o.id !== objectId));
164
+ };
165
+
166
+ const copyUrl = async (object: StorageObject) => {
167
+ let url = object.url;
168
+
169
+ if (!url) {
170
+ const res = await fetch(
171
+ `${storageBase}/objects/${object.id}/signed-url`,
172
+ {
173
+ credentials: "include",
174
+ },
175
+ );
176
+ const data = (await res.json()) as { url: string };
177
+ url = data.url;
178
+ }
179
+
180
+ await navigator.clipboard.writeText(url);
181
+ setCopiedId(object.id);
182
+ setTimeout(() => setCopiedId(null), 2000);
183
+ };
184
+
185
+ return (
186
+ <div className="flex h-full min-h-0 w-full flex-1 overflow-hidden">
187
+ <div className="flex h-full min-h-0 w-full">
188
+ <ResizablePanelGroup
189
+ orientation="horizontal"
190
+ className="h-full w-full min-h-0"
191
+ defaultLayout={{ "storage-buckets": 20, "storage-files": 80 }}
192
+ >
193
+ <ResizablePanel
194
+ id="storage-buckets"
195
+ defaultSize="20"
196
+ minSize="15"
197
+ maxSize="30"
198
+ >
199
+ <div className="flex h-full w-full flex-col overflow-hidden border-r border-border bg-background">
200
+ <div className="flex shrink-0 items-center justify-between border-b border-border px-3 py-2">
201
+ <span className="text-xs font-medium">Buckets</span>
202
+
203
+ <Dialog open={createOpen} onOpenChange={setCreateOpen}>
204
+ <DialogTrigger asChild>
205
+ <Button variant="ghost" size="icon" className="h-6 w-6">
206
+ <Plus size={12} />
207
+ </Button>
208
+ </DialogTrigger>
209
+ <DialogContent>
210
+ <DialogHeader>
211
+ <DialogTitle>Create bucket</DialogTitle>
212
+ </DialogHeader>
213
+ <div className="mt-2 space-y-4">
214
+ <div className="space-y-1">
215
+ <Label htmlFor="bucket-name">Name</Label>
216
+ <Input
217
+ id="bucket-name"
218
+ value={newBucketName}
219
+ onChange={(e) => setNewBucketName(e.target.value)}
220
+ placeholder="my-bucket"
221
+ />
222
+ </div>
223
+ <div className="space-y-2">
224
+ <Label>Access</Label>
225
+ <div className="flex gap-2">
226
+ <Button
227
+ type="button"
228
+ variant={
229
+ newBucketAccess === "public"
230
+ ? "default"
231
+ : "outline"
232
+ }
233
+ size="sm"
234
+ className="gap-1.5"
235
+ onClick={() => setNewBucketAccess("public")}
236
+ >
237
+ <Globe size={12} />
238
+ Public
239
+ </Button>
240
+ <Button
241
+ type="button"
242
+ variant={
243
+ newBucketAccess === "private"
244
+ ? "default"
245
+ : "outline"
246
+ }
247
+ size="sm"
248
+ className="gap-1.5"
249
+ onClick={() => setNewBucketAccess("private")}
250
+ >
251
+ <Lock size={12} />
252
+ Private
253
+ </Button>
254
+ </div>
255
+ <p className="text-xs text-muted-foreground">
256
+ {newBucketAccess === "public"
257
+ ? "Files are accessible via their URL without authentication"
258
+ : "Files require a signed URL to access"}
259
+ </p>
260
+ </div>
261
+ <div className="flex justify-end gap-2">
262
+ <Button
263
+ variant="outline"
264
+ onClick={() => setCreateOpen(false)}
265
+ >
266
+ Cancel
267
+ </Button>
268
+ <Button
269
+ onClick={() => void createBucket()}
270
+ disabled={creating}
271
+ >
272
+ {creating ? "Creating..." : "Create"}
273
+ </Button>
274
+ </div>
275
+ </div>
276
+ </DialogContent>
277
+ </Dialog>
278
+ </div>
279
+
280
+ <div className="min-h-0 flex-1 overflow-y-auto py-1">
281
+ {buckets.length === 0 ? (
282
+ <p className="px-3 py-6 text-center text-xs text-muted-foreground">
283
+ No buckets yet
284
+ </p>
285
+ ) : (
286
+ buckets.map((bucket) => (
287
+ <div
288
+ key={bucket.id}
289
+ className={cn(
290
+ "flex cursor-pointer items-center justify-between px-3 py-2",
291
+ activeBucket?.id === bucket.id
292
+ ? "bg-accent text-accent-foreground"
293
+ : "hover:bg-accent/50",
294
+ )}
295
+ onClick={() => void loadObjects(bucket)}
296
+ >
297
+ <div className="flex min-w-0 items-center gap-2">
298
+ <FolderOpen
299
+ size={14}
300
+ className="shrink-0 text-muted-foreground"
301
+ />
302
+ <span className="truncate text-sm">{bucket.name}</span>
303
+ </div>
304
+ <div className="flex shrink-0 items-center gap-1">
305
+ {bucket.access === "private" ? (
306
+ <Lock size={10} className="text-muted-foreground" />
307
+ ) : (
308
+ <Globe size={10} className="text-muted-foreground" />
309
+ )}
310
+ {activeBucket?.id === bucket.id && (
311
+ <DropdownMenu>
312
+ <DropdownMenuTrigger asChild>
313
+ <Button
314
+ variant="ghost"
315
+ size="icon"
316
+ className="h-5 w-5"
317
+ onClick={(e) => e.stopPropagation()}
318
+ >
319
+ <MoreVertical size={10} />
320
+ </Button>
321
+ </DropdownMenuTrigger>
322
+ <DropdownMenuContent align="end">
323
+ <DropdownMenuItem
324
+ className="text-destructive"
325
+ onSelect={() => void deleteBucket(bucket.id)}
326
+ >
327
+ <Trash2 size={12} className="mr-1.5" />
328
+ Delete bucket
329
+ </DropdownMenuItem>
330
+ </DropdownMenuContent>
331
+ </DropdownMenu>
332
+ )}
333
+ </div>
334
+ </div>
335
+ ))
336
+ )}
337
+ </div>
338
+ </div>
339
+ </ResizablePanel>
340
+
341
+ <ResizableHandle withHandle />
342
+
343
+ <ResizablePanel id="storage-files" defaultSize="80" minSize="50">
344
+ <div className="flex h-full w-full min-h-0 flex-col overflow-hidden">
345
+ {activeBucket && (
346
+ <div className="flex shrink-0 items-center justify-between border-b border-border px-4 py-2">
347
+ <div className="flex items-center gap-2">
348
+ <span className="text-sm font-medium">
349
+ {activeBucket.name}
350
+ </span>
351
+ <Badge
352
+ variant="secondary"
353
+ className="gap-1 text-xs capitalize"
354
+ >
355
+ {activeBucket.access === "private" ? (
356
+ <>
357
+ <Lock size={10} /> Private
358
+ </>
359
+ ) : (
360
+ <>
361
+ <Globe size={10} /> Public
362
+ </>
363
+ )}
364
+ </Badge>
365
+ </div>
366
+
367
+ {uploadUrl && activeBucket ? (
368
+ <UploadButton<OurStorageRouter, "bucketUploader">
369
+ url={uploadUrl}
370
+ endpoint="bucketUploader"
371
+ fetch={uploadFetch}
372
+ onUploadError={(error) => {
373
+ console.error("Upload failed:", error.message);
374
+ }}
375
+ onClientUploadComplete={async (files) => {
376
+ for (const file of files) {
377
+ const res = await fetch(
378
+ `${storageBase}/buckets/${activeBucket.id}/objects`,
379
+ {
380
+ method: "POST",
381
+ headers: { "Content-Type": "application/json" },
382
+ credentials: "include",
383
+ body: JSON.stringify({
384
+ name: file.name,
385
+ size: file.size,
386
+ type: file.type ?? "application/octet-stream",
387
+ utKey: file.key,
388
+ url: file.url,
389
+ }),
390
+ },
391
+ );
392
+ if (!res.ok) {
393
+ console.error(
394
+ "Failed to save file:",
395
+ await res.text(),
396
+ );
397
+ continue;
398
+ }
399
+ const object = (await res.json()) as StorageObject;
400
+ setObjects((prev) => [object, ...prev]);
401
+ }
402
+ }}
403
+ appearance={{
404
+ button:
405
+ "ut-ready:bg-primary ut-ready:text-primary-foreground h-8 px-3 text-sm rounded-md",
406
+ allowedContent: "hidden",
407
+ }}
408
+ />
409
+ ) : null}
410
+ </div>
411
+ )}
412
+
413
+ {!activeBucket ? (
414
+ <div className="flex flex-1 flex-col items-center justify-center text-center">
415
+ <FolderOpen
416
+ size={32}
417
+ className="mb-3 text-muted-foreground"
418
+ />
419
+ <p className="text-sm text-muted-foreground">
420
+ Select a bucket to browse files
421
+ </p>
422
+ </div>
423
+ ) : loadingObjects ? (
424
+ <div className="flex flex-1 items-center justify-center">
425
+ <p className="text-sm text-muted-foreground">Loading...</p>
426
+ </div>
427
+ ) : objects.length === 0 ? (
428
+ <div className="flex flex-1 flex-col items-center justify-center text-center">
429
+ <p className="text-sm text-muted-foreground">
430
+ This bucket is empty
431
+ </p>
432
+ <p className="mt-1 text-xs text-muted-foreground">
433
+ Upload files using the button above
434
+ </p>
435
+ </div>
436
+ ) : (
437
+ <div className="min-h-0 w-full flex-1 overflow-y-auto">
438
+ <div className="grid w-full grid-cols-2 gap-3 p-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
439
+ {objects.map((object) => (
440
+ <div
441
+ key={object.id}
442
+ className="group relative rounded-xl border border-border p-3 transition-colors hover:bg-accent"
443
+ >
444
+ <div className="mb-2 flex h-16 items-center justify-center">
445
+ {object.mimeType.startsWith("image/") &&
446
+ object.url ? (
447
+ // eslint-disable-next-line @next/next/no-img-element
448
+ <img
449
+ src={object.url}
450
+ alt={object.name}
451
+ className="max-h-16 max-w-full rounded object-contain"
452
+ />
453
+ ) : (
454
+ <File size={32} className="text-muted-foreground" />
455
+ )}
456
+ </div>
457
+
458
+ <p className="truncate text-xs font-medium">
459
+ {object.name}
460
+ </p>
461
+ <p className="text-xs text-muted-foreground">
462
+ {formatBytes(object.size)}
463
+ </p>
464
+
465
+ <div className="absolute top-2 right-2 hidden gap-1 group-hover:flex">
466
+ <Button
467
+ variant="secondary"
468
+ size="icon"
469
+ className="h-6 w-6"
470
+ onClick={() => void copyUrl(object)}
471
+ title="Copy URL"
472
+ >
473
+ {copiedId === object.id ? (
474
+ <Check size={10} />
475
+ ) : (
476
+ <Copy size={10} />
477
+ )}
478
+ </Button>
479
+ <Button
480
+ variant="secondary"
481
+ size="icon"
482
+ className="h-6 w-6 hover:text-destructive"
483
+ onClick={() => void deleteObject(object.id)}
484
+ title="Delete"
485
+ >
486
+ <Trash2 size={10} />
487
+ </Button>
488
+ </div>
489
+ </div>
490
+ ))}
491
+ </div>
492
+ </div>
493
+ )}
494
+ </div>
495
+ </ResizablePanel>
496
+ </ResizablePanelGroup>
497
+ </div>
498
+ </div>
499
+ );
500
+ }
@@ -0,0 +1,36 @@
1
+ import { redirect } from "next/navigation";
2
+ import apiClient from "@/lib/axios";
3
+ import { retrieveTokenFromCookie } from "@/server-utils/utils";
4
+ import { COOKIE_KEYS } from "@apiDatabase/constants";
5
+ import type { StorageBucket, StorageObject } from "@apiDatabase/types";
6
+
7
+ export async function retrieveBucketsFromApi(
8
+ orgSlug: string,
9
+ projectSlug: string,
10
+ ): Promise<StorageBucket[]> {
11
+ const token = await retrieveTokenFromCookie();
12
+
13
+ try {
14
+ const { data } = await apiClient.get<StorageBucket[]>(
15
+ `/orgs/${orgSlug}/projects/${projectSlug}/storage/buckets`,
16
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
17
+ );
18
+ return data;
19
+ } catch {
20
+ redirect(`/organizations/${orgSlug}/projects`);
21
+ }
22
+ }
23
+
24
+ export async function retrieveObjectsFromApi(
25
+ orgSlug: string,
26
+ projectSlug: string,
27
+ bucketId: string,
28
+ ): Promise<StorageObject[]> {
29
+ const token = await retrieveTokenFromCookie();
30
+
31
+ const { data } = await apiClient.get<StorageObject[]>(
32
+ `/orgs/${orgSlug}/projects/${projectSlug}/storage/buckets/${bucketId}/objects`,
33
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
34
+ );
35
+ return data;
36
+ }
@@ -0,0 +1,97 @@
1
+ "use server";
2
+
3
+ import { revalidatePath } from "next/cache";
4
+ import { tableEditorServerSchema } from "./server.schema";
5
+ import { COOKIE_KEYS, TABLE_EDITOR_INTENT } from "@apiDatabase/constants";
6
+ import { retrieveTokenFromCookie } from "@/server-utils/utils";
7
+ import apiClient from "@/lib/axios";
8
+ import type { TableInfo } from "@apiDatabase/types";
9
+
10
+ export type TableEditorActionState = {
11
+ error?: string;
12
+ success?: string;
13
+ tableName?: string;
14
+ tableData?: {
15
+ info: TableInfo;
16
+ rows: Record<string, unknown>[];
17
+ count: number;
18
+ };
19
+ };
20
+
21
+ type TableEditorContext = {
22
+ orgSlug: string;
23
+ projectSlug: string;
24
+ };
25
+
26
+ function authHeaders(token: string) {
27
+ return { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } };
28
+ }
29
+
30
+ function basePath(orgSlug: string, projectSlug: string) {
31
+ return `/orgs/${orgSlug}/projects/${projectSlug}/tables`;
32
+ }
33
+
34
+ export async function tableEditorAction(
35
+ { orgSlug, projectSlug }: TableEditorContext,
36
+ _prev: TableEditorActionState,
37
+ formData: FormData,
38
+ ): Promise<TableEditorActionState> {
39
+ const raw = Object.fromEntries(formData);
40
+ const parsed = tableEditorServerSchema.safeParse(raw);
41
+
42
+ if (!parsed.success) {
43
+ return { error: parsed.error.flatten().formErrors[0] ?? "Invalid input" };
44
+ }
45
+
46
+ const token = await retrieveTokenFromCookie();
47
+ const { intent } = parsed.data;
48
+ const base = basePath(orgSlug, projectSlug);
49
+ const opts = authHeaders(token);
50
+
51
+ try {
52
+ switch (intent) {
53
+ case TABLE_EDITOR_INTENT.CREATE_TABLE: {
54
+ const { name, columns } = parsed.data;
55
+ await apiClient.post(base, { name, columns }, opts);
56
+ return { success: "Table created!", tableName: name };
57
+ }
58
+
59
+ case TABLE_EDITOR_INTENT.DELETE_TABLE: {
60
+ const { tableName } = parsed.data;
61
+ await apiClient.delete(`${base}/${tableName}`, opts);
62
+ return { success: "Table deleted!", tableName };
63
+ }
64
+
65
+ case TABLE_EDITOR_INTENT.FETCH_TABLE: {
66
+ const { tableName } = parsed.data;
67
+ const [infoRes, rowsRes] = await Promise.all([
68
+ apiClient.get<TableInfo>(`${base}/${tableName}`, opts),
69
+ apiClient.get<{ rows: Record<string, unknown>[]; count: number }>(
70
+ `${base}/${tableName}/rows?limit=100`,
71
+ opts,
72
+ ),
73
+ ]);
74
+ return {
75
+ tableData: {
76
+ info: infoRes.data,
77
+ rows: rowsRes.data.rows,
78
+ count: rowsRes.data.count,
79
+ },
80
+ };
81
+ }
82
+
83
+ case TABLE_EDITOR_INTENT.ADD_COLUMN: {
84
+ const { tableName, name, type, defaultValue } = parsed.data;
85
+ await apiClient.patch(
86
+ `${base}/${tableName}/columns`,
87
+ { name, type, defaultValue: defaultValue || undefined },
88
+ opts,
89
+ );
90
+ return { success: "Column added!", tableName };
91
+ }
92
+ }
93
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ } catch (err: any) {
95
+ return { error: err.response?.data?.message ?? "Something went wrong" };
96
+ }
97
+ }