CreateCommands.tsx476 lines · main
1'use client'
2
3import { IS_PLATFORM } from 'common'
4import {
5 Clock5,
6 Code2,
7 KeyRound,
8 Layers,
9 ListChecks,
10 Lock,
11 LockKeyhole,
12 Mail,
13 MessageCircle,
14 Plus,
15 Rows,
16 ShieldPlus,
17 Table2,
18 Telescope,
19 UserCog,
20 UserPlus,
21 Vault,
22 Webhook,
23 Zap,
24} from 'lucide-react'
25import dynamic from 'next/dynamic'
26import { useMemo } from 'react'
27import type { CommandOptions, ICommand } from 'ui-patterns/CommandMenu'
28import {
29 PageType,
30 useRegisterCommands,
31 useRegisterPage,
32 useSetCommandMenuOpen,
33} from 'ui-patterns/CommandMenu'
34
35import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils'
36import {
37 getIntegrationCommandName,
38 getIntegrationRoute,
39 useCreateCommandsConfig,
40} from './CreateCommands.utils'
41import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
42
43const AiIconAnimation = dynamic(() => import('ui').then((mod) => mod.AiIconAnimation))
44const Badge = dynamic(() => import('ui').then((mod) => mod.Badge))
45const EdgeFunctions = dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))
46const AnalyticsBucket = dynamic(() => import('icons').then((mod) => mod.AnalyticsBucket))
47const FilesBucket = dynamic(() => import('icons').then((mod) => mod.FilesBucket))
48const VectorBucket = dynamic(() => import('icons').then((mod) => mod.VectorBucket))
49
50const CREATE_STUDIO_ENTITY = 'Create Studio Entity'
51
52export function useCreateCommands(options?: CommandOptions) {
53 const setIsOpen = useSetCommandMenuOpen()
54 const {
55 ref,
56 setPage,
57 openSidebar,
58 snap,
59 authEnabled,
60 edgeFunctionsEnabled,
61 storageEnabled,
62 sendSmsHook,
63 sendEmailHook,
64 customAccessTokenHook,
65 mfaVerificationHook,
66 mfaVerificationHookEnabled,
67 passwordVerificationHook,
68 passwordVerificationHookEnabled,
69 beforeUserCreatedHook,
70 isVectorBucketsEnabled,
71 isAnalyticsBucketsEnabled,
72 installedIntegrationIds,
73 allIntegrations,
74 reportsEnabled,
75 } = useCreateCommandsConfig()
76
77 const databaseCommands = useMemo(
78 () =>
79 [
80 {
81 id: 'create-db-table',
82 name: 'Create Table',
83 route: `/project/${ref}/editor?create=table`,
84 icon: () => <Table2 />,
85 },
86 {
87 id: 'create-db-index',
88 name: 'Create Index',
89 route: `/project/${ref}/database/indexes?new=true`,
90 icon: () => <Rows />,
91 },
92 {
93 id: 'create-db-function',
94 name: 'Create Database Function',
95 route: `/project/${ref}/database/functions?new=true`,
96 icon: () => <Code2 />,
97 },
98 {
99 id: 'create-db-enum',
100 name: 'Create Enumerated Type',
101 route: `/project/${ref}/database/types?new=true`,
102 icon: () => <ListChecks />,
103 },
104 {
105 id: 'create-db-trigger',
106 name: 'Create Database Trigger',
107 route: `/project/${ref}/database/triggers?new=true`,
108 icon: () => <Zap />,
109 },
110 {
111 id: 'create-db-role',
112 name: 'Create Database Role',
113 route: `/project/${ref}/database/roles?new=true`,
114 icon: () => <UserCog />,
115 },
116 ].filter(Boolean) as ICommand[],
117 [ref]
118 )
119
120 const authCommands = useMemo(
121 () =>
122 authEnabled
123 ? ([
124 {
125 id: 'create-auth-user',
126 name: 'Create Auth User',
127 route: `/project/${ref}/auth/users?new=true`,
128 icon: () => <UserPlus />,
129 },
130 {
131 id: 'create-rls-policy',
132 name: 'Create RLS Policy',
133 route: `/project/${ref}/auth/policies?new=true`,
134 icon: () => <ShieldPlus />,
135 },
136 ...(IS_PLATFORM
137 ? [
138 {
139 id: 'create-auth-hook-sms',
140 name: 'Create Auth Hook (SMS)',
141 route: `/project/${ref}/auth/hooks?hook=${sendSmsHook?.id}`,
142 icon: () => <MessageCircle />,
143 },
144 {
145 id: 'create-auth-hook-email',
146 name: 'Create Auth Hook (Email)',
147 route: `/project/${ref}/auth/hooks?hook=${sendEmailHook?.id}`,
148 icon: () => <Mail />,
149 },
150 {
151 id: 'create-auth-hook-custom-access-token',
152 name: 'Create Auth Hook (Custom Access Token)',
153 route: `/project/${ref}/auth/hooks?hook=${customAccessTokenHook?.id}`,
154 icon: () => <KeyRound />,
155 },
156 {
157 id: 'create-auth-hook-mfa-verification',
158 name: 'Create Auth Hook (MFA Verification Attempt)',
159 route: `/project/${ref}/auth/hooks?hook=${mfaVerificationHook?.id}`,
160 icon: () => <ShieldPlus />,
161 badge: () => (mfaVerificationHookEnabled ? <Badge>Team</Badge> : null),
162 className: mfaVerificationHookEnabled
163 ? 'opacity-50 cursor-not-allowed pointer-events-none'
164 : '',
165 },
166 {
167 id: 'create-auth-hook-password-verification',
168 name: 'Create Auth Hook (Password Verification Attempt)',
169 route: `/project/${ref}/auth/hooks?hook=${passwordVerificationHook?.id}`,
170 icon: () => <Lock />,
171 badge: () => (passwordVerificationHookEnabled ? <Badge>Team</Badge> : null),
172 className: passwordVerificationHookEnabled
173 ? 'opacity-50 cursor-not-allowed pointer-events-none'
174 : '',
175 },
176 {
177 id: 'create-auth-hook-before-user-created',
178 name: 'Create Auth Hook (Before User Created)',
179 route: `/project/${ref}/auth/hooks?hook=${beforeUserCreatedHook?.id}`,
180 icon: () => <KeyRound />,
181 },
182 ]
183 : []),
184 ...(IS_PLATFORM
185 ? [
186 {
187 id: 'create-oauth-app',
188 name: 'Create OAuth App',
189 route: `/project/${ref}/auth/oauth-apps?new=true`,
190 icon: () => <KeyRound />,
191 },
192 ]
193 : []),
194 ].filter(Boolean) as ICommand[])
195 : [],
196 [
197 ref,
198 authEnabled,
199 sendSmsHook,
200 sendEmailHook,
201 customAccessTokenHook,
202 mfaVerificationHook,
203 mfaVerificationHookEnabled,
204 passwordVerificationHook,
205 passwordVerificationHookEnabled,
206 beforeUserCreatedHook,
207 ]
208 )
209
210 const edgeFunctionsCommands = useMemo(
211 () =>
212 edgeFunctionsEnabled
213 ? ([
214 {
215 id: 'create-edge-function-editor',
216 name: 'Create Edge Function via Editor',
217 route: `/project/${ref}/functions/new`,
218 icon: () => <EdgeFunctions />,
219 },
220 {
221 id: 'create-edge-function-cli',
222 name: 'Create Edge Function via CLI',
223 route: `/project/${ref}/functions?create=cli`,
224 icon: () => <EdgeFunctions />,
225 },
226 {
227 id: 'create-edge-function-ai',
228 name: 'Create Edge Function via AI',
229 action: () => {
230 openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
231 snap.newChat({
232 name: 'Create new edge function',
233 initialInput: `Create a new edge function that ...`,
234 suggestions: {
235 title:
236 'I can help you create a new edge function. Here are a few example prompts to get you started:',
237 prompts: [
238 {
239 label: 'Stripe Payments',
240 description:
241 'Create a new edge function that processes payments with Stripe',
242 },
243 {
244 label: 'Email with Resend',
245 description: 'Create a new edge function that sends emails with Resend',
246 },
247 {
248 label: 'PDF Generator',
249 description:
250 'Create a new edge function that generates PDFs from HTML templates',
251 },
252 ],
253 },
254 })
255 setIsOpen(false)
256 },
257 icon: () => (
258 <AiIconAnimation
259 allowHoverEffect={false}
260 size={20}
261 className="text-foreground-light"
262 />
263 ),
264 },
265 {
266 id: 'create-edge-function-secret',
267 name: 'Create Edge Function Secret',
268 route: `/project/${ref}/functions/secrets`,
269 icon: () => <LockKeyhole />,
270 },
271 ].filter(Boolean) as ICommand[])
272 : [],
273 [ref, edgeFunctionsEnabled, openSidebar, snap, setIsOpen]
274 )
275
276 const storageCommands = useMemo(
277 () =>
278 storageEnabled
279 ? ([
280 {
281 id: 'create-storage-bucket-files',
282 name: 'Create Storage Bucket (Files)',
283 route: `/project/${ref}/storage/files?new=true`,
284 icon: () => <FilesBucket />,
285 },
286 {
287 id: 'create-storage-bucket-analytics',
288 name: 'Create Storage Bucket (Analytics)',
289 route: `/project/${ref}/storage/analytics?new=true`,
290 icon: () => <AnalyticsBucket />,
291 badge: () => <Badge variant="success">New</Badge>,
292 className: !isAnalyticsBucketsEnabled
293 ? 'opacity-50 cursor-not-allowed pointer-events-none'
294 : '',
295 enabled: isAnalyticsBucketsEnabled,
296 },
297 {
298 id: 'create-storage-bucket-vectors',
299 name: 'Create Storage Bucket (Vectors)',
300 route: `/project/${ref}/storage/vectors?new=true`,
301 icon: () => <VectorBucket />,
302 badge: () => <Badge variant="success">New</Badge>,
303 className: !isVectorBucketsEnabled
304 ? 'opacity-50 cursor-not-allowed pointer-events-none'
305 : '',
306 enabled: isVectorBucketsEnabled,
307 },
308 ].filter(Boolean) as ICommand[])
309 : [],
310 [ref, storageEnabled, isAnalyticsBucketsEnabled, isVectorBucketsEnabled]
311 )
312
313 const integrationsCommands = useMemo(() => {
314 // Sort integrations: Postgres modules (non-wrappers) first, then wrappers
315 const sortedIntegrations = [...allIntegrations].sort((a, b) => {
316 const aIsWrapper = a.type === 'wrapper' ? 1 : 0
317 const bIsWrapper = b.type === 'wrapper' ? 1 : 0
318 return aIsWrapper - bIsWrapper
319 })
320
321 return sortedIntegrations
322 .map((integration) => {
323 const route = getIntegrationRoute(integration, ref, installedIntegrationIds)
324 if (!route) return null
325
326 const isWrapper = integration.type === 'wrapper'
327
328 // For wrappers, use the integration icon with wrapper styling
329 // For Postgres modules, use plain icons
330 const getIcon = () => {
331 if (isWrapper) {
332 return (
333 <div className="w-6 h-6 relative bg-white border rounded-md flex items-center justify-center [&>img]:p-1! [&>svg]:p-1!">
334 {integration.icon()}
335 </div>
336 )
337 }
338
339 // Use plain icons for Postgres modules
340 switch (integration.id) {
341 case 'vault':
342 return <Vault />
343 case 'cron':
344 return <Clock5 />
345 case 'webhooks':
346 return <Webhook />
347 case 'queues':
348 return <Layers />
349 default:
350 // Fallback to integration icon for other Postgres modules
351 return integration.icon()
352 }
353 }
354
355 return {
356 id: `create-integration-${integration.id}`,
357 name: getIntegrationCommandName(integration),
358 route,
359 icon: getIcon,
360 }
361 })
362 .filter(Boolean) as ICommand[]
363 }, [ref, allIntegrations, installedIntegrationIds])
364
365 // Observability commands are only available on the platform, not for self-hosted/CLI
366 const observabilityCommands = useMemo(
367 () => [
368 ...(IS_PLATFORM
369 ? reportsEnabled
370 ? ([
371 {
372 id: 'create-observability-report',
373 name: 'Create Custom Report',
374 route: `/project/${ref}/observability/api-overview?newReport=true`,
375 icon: () => <Telescope />,
376 },
377 ].filter(Boolean) as ICommand[])
378 : []
379 : []),
380 ],
381 [ref, reportsEnabled]
382 )
383
384 const sections = useMemo(
385 () => [
386 {
387 id: 'create-database',
388 name: 'Database',
389 commands: databaseCommands,
390 },
391 ...(authCommands.length > 0
392 ? [
393 {
394 id: 'create-auth',
395 name: 'Auth',
396 commands: authCommands,
397 },
398 ]
399 : []),
400 ...(edgeFunctionsCommands.length > 0
401 ? [
402 {
403 id: 'create-edge-functions',
404 name: 'Edge Functions',
405 commands: edgeFunctionsCommands,
406 },
407 ]
408 : []),
409 ...(storageCommands.length > 0
410 ? [
411 {
412 id: 'create-storage',
413 name: 'Storage',
414 commands: storageCommands,
415 },
416 ]
417 : []),
418 ...(observabilityCommands.length > 0
419 ? [
420 {
421 id: 'create-observability',
422 name: 'Observability',
423 commands: observabilityCommands,
424 },
425 ]
426 : []),
427 ...(integrationsCommands.length > 0
428 ? [
429 {
430 id: 'create-integrations',
431 name: 'Integrations',
432 commands: integrationsCommands,
433 },
434 ]
435 : []),
436 ],
437 [
438 databaseCommands,
439 authCommands,
440 edgeFunctionsCommands,
441 storageCommands,
442 integrationsCommands,
443 observabilityCommands,
444 ]
445 )
446
447 useRegisterPage(
448 CREATE_STUDIO_ENTITY,
449 {
450 type: PageType.Commands,
451 sections,
452 },
453 {
454 deps: [sections],
455 enabled: true,
456 }
457 )
458
459 useRegisterCommands(
460 COMMAND_MENU_SECTIONS.ACTIONS,
461 [
462 {
463 id: 'create-studio-entity',
464 name: 'Create...',
465 action: () => setPage(CREATE_STUDIO_ENTITY),
466 icon: () => <Plus />,
467 },
468 ],
469 {
470 ...options,
471 orderSection: (sections) => sections,
472 sectionMeta: { priority: 3 },
473 enabled: true,
474 }
475 )
476}