CreateCommands.tsx 15 KB

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