Files
supabase/apps/studio/components/interfaces/App/CommandMenu/CreateCommands.tsx
Ivan Vasilov 56de26fe22 chore: Migrate the monorepo to use Tailwind v4 (#45318)
This PR migrates the whole monorepo to use Tailwind v4:
- Removed `@tailwindcss/container-queries` plugin since it's included by
default in v4,
- Bump all instances of Tailwind to v4. Made minimal changes to the
shared config to remove non-supported features (`alpha` mentions),
- Migrate all apps to be compatible with v4 configs,
- Fix the `typography.css` import in 3 apps,
- Add missing rules which were included by default in v3,
- Run `pnpm dlx @tailwindcss/upgrade` on all apps, which renames a lot
of classes
- Rename all misnamed classes according to
https://tailwindcss.com/docs/upgrade-guide#renamed-utilities in all
apps.

---------

Co-authored-by: Jordi Enric <jordi.err@gmail.com>
2026-04-30 10:53:24 +00:00

477 lines
15 KiB
TypeScript

'use client'
import { IS_PLATFORM } from 'common'
import {
Clock5,
Code2,
KeyRound,
Layers,
ListChecks,
Lock,
LockKeyhole,
Mail,
MessageCircle,
Plus,
Rows,
ShieldPlus,
Table2,
Telescope,
UserCog,
UserPlus,
Vault,
Webhook,
Zap,
} from 'lucide-react'
import dynamic from 'next/dynamic'
import { useMemo } from 'react'
import type { CommandOptions, ICommand } from 'ui-patterns/CommandMenu'
import {
PageType,
useRegisterCommands,
useRegisterPage,
useSetCommandMenuOpen,
} from 'ui-patterns/CommandMenu'
import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils'
import {
getIntegrationCommandName,
getIntegrationRoute,
useCreateCommandsConfig,
} from './CreateCommands.utils'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
const AiIconAnimation = dynamic(() => import('ui').then((mod) => mod.AiIconAnimation))
const Badge = dynamic(() => import('ui').then((mod) => mod.Badge))
const EdgeFunctions = dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))
const AnalyticsBucket = dynamic(() => import('icons').then((mod) => mod.AnalyticsBucket))
const FilesBucket = dynamic(() => import('icons').then((mod) => mod.FilesBucket))
const VectorBucket = dynamic(() => import('icons').then((mod) => mod.VectorBucket))
const CREATE_STUDIO_ENTITY = 'Create Studio Entity'
export function useCreateCommands(options?: CommandOptions) {
const setIsOpen = useSetCommandMenuOpen()
const {
ref,
setPage,
openSidebar,
snap,
authEnabled,
edgeFunctionsEnabled,
storageEnabled,
sendSmsHook,
sendEmailHook,
customAccessTokenHook,
mfaVerificationHook,
mfaVerificationHookEnabled,
passwordVerificationHook,
passwordVerificationHookEnabled,
beforeUserCreatedHook,
isVectorBucketsEnabled,
isAnalyticsBucketsEnabled,
installedIntegrationIds,
allIntegrations,
reportsEnabled,
} = useCreateCommandsConfig()
const databaseCommands = useMemo(
() =>
[
{
id: 'create-db-table',
name: 'Create Table',
route: `/project/${ref}/editor?create=table`,
icon: () => <Table2 />,
},
{
id: 'create-db-index',
name: 'Create Index',
route: `/project/${ref}/database/indexes?new=true`,
icon: () => <Rows />,
},
{
id: 'create-db-function',
name: 'Create Database Function',
route: `/project/${ref}/database/functions?new=true`,
icon: () => <Code2 />,
},
{
id: 'create-db-enum',
name: 'Create Enumerated Type',
route: `/project/${ref}/database/types?new=true`,
icon: () => <ListChecks />,
},
{
id: 'create-db-trigger',
name: 'Create Database Trigger',
route: `/project/${ref}/database/triggers?new=true`,
icon: () => <Zap />,
},
{
id: 'create-db-role',
name: 'Create Database Role',
route: `/project/${ref}/database/roles?new=true`,
icon: () => <UserCog />,
},
].filter(Boolean) as ICommand[],
[ref]
)
const authCommands = useMemo(
() =>
authEnabled
? ([
{
id: 'create-auth-user',
name: 'Create Auth User',
route: `/project/${ref}/auth/users?new=true`,
icon: () => <UserPlus />,
},
{
id: 'create-rls-policy',
name: 'Create RLS Policy',
route: `/project/${ref}/auth/policies?new=true`,
icon: () => <ShieldPlus />,
},
...(IS_PLATFORM
? [
{
id: 'create-auth-hook-sms',
name: 'Create Auth Hook (SMS)',
route: `/project/${ref}/auth/hooks?hook=${sendSmsHook?.id}`,
icon: () => <MessageCircle />,
},
{
id: 'create-auth-hook-email',
name: 'Create Auth Hook (Email)',
route: `/project/${ref}/auth/hooks?hook=${sendEmailHook?.id}`,
icon: () => <Mail />,
},
{
id: 'create-auth-hook-custom-access-token',
name: 'Create Auth Hook (Custom Access Token)',
route: `/project/${ref}/auth/hooks?hook=${customAccessTokenHook?.id}`,
icon: () => <KeyRound />,
},
{
id: 'create-auth-hook-mfa-verification',
name: 'Create Auth Hook (MFA Verification Attempt)',
route: `/project/${ref}/auth/hooks?hook=${mfaVerificationHook?.id}`,
icon: () => <ShieldPlus />,
badge: () => (mfaVerificationHookEnabled ? <Badge>Team</Badge> : null),
className: mfaVerificationHookEnabled
? 'opacity-50 cursor-not-allowed pointer-events-none'
: '',
},
{
id: 'create-auth-hook-password-verification',
name: 'Create Auth Hook (Password Verification Attempt)',
route: `/project/${ref}/auth/hooks?hook=${passwordVerificationHook?.id}`,
icon: () => <Lock />,
badge: () => (passwordVerificationHookEnabled ? <Badge>Team</Badge> : null),
className: passwordVerificationHookEnabled
? 'opacity-50 cursor-not-allowed pointer-events-none'
: '',
},
{
id: 'create-auth-hook-before-user-created',
name: 'Create Auth Hook (Before User Created)',
route: `/project/${ref}/auth/hooks?hook=${beforeUserCreatedHook?.id}`,
icon: () => <KeyRound />,
},
]
: []),
...(IS_PLATFORM
? [
{
id: 'create-oauth-app',
name: 'Create OAuth App',
route: `/project/${ref}/auth/oauth-apps?new=true`,
icon: () => <KeyRound />,
},
]
: []),
].filter(Boolean) as ICommand[])
: [],
[
ref,
authEnabled,
sendSmsHook,
sendEmailHook,
customAccessTokenHook,
mfaVerificationHook,
mfaVerificationHookEnabled,
passwordVerificationHook,
passwordVerificationHookEnabled,
beforeUserCreatedHook,
]
)
const edgeFunctionsCommands = useMemo(
() =>
edgeFunctionsEnabled
? ([
{
id: 'create-edge-function-editor',
name: 'Create Edge Function via Editor',
route: `/project/${ref}/functions/new`,
icon: () => <EdgeFunctions />,
},
{
id: 'create-edge-function-cli',
name: 'Create Edge Function via CLI',
route: `/project/${ref}/functions?create=cli`,
icon: () => <EdgeFunctions />,
},
{
id: 'create-edge-function-ai',
name: 'Create Edge Function via AI',
action: () => {
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
snap.newChat({
name: 'Create new edge function',
initialInput: `Create a new edge function that ...`,
suggestions: {
title:
'I can help you create a new edge function. Here are a few example prompts to get you started:',
prompts: [
{
label: 'Stripe Payments',
description:
'Create a new edge function that processes payments with Stripe',
},
{
label: 'Email with Resend',
description: 'Create a new edge function that sends emails with Resend',
},
{
label: 'PDF Generator',
description:
'Create a new edge function that generates PDFs from HTML templates',
},
],
},
})
setIsOpen(false)
},
icon: () => (
<AiIconAnimation
allowHoverEffect={false}
size={20}
className="text-foreground-light"
/>
),
},
{
id: 'create-edge-function-secret',
name: 'Create Edge Function Secret',
route: `/project/${ref}/functions/secrets`,
icon: () => <LockKeyhole />,
},
].filter(Boolean) as ICommand[])
: [],
[ref, edgeFunctionsEnabled, openSidebar, snap, setIsOpen]
)
const storageCommands = useMemo(
() =>
storageEnabled
? ([
{
id: 'create-storage-bucket-files',
name: 'Create Storage Bucket (Files)',
route: `/project/${ref}/storage/files?new=true`,
icon: () => <FilesBucket />,
},
{
id: 'create-storage-bucket-analytics',
name: 'Create Storage Bucket (Analytics)',
route: `/project/${ref}/storage/analytics?new=true`,
icon: () => <AnalyticsBucket />,
badge: () => <Badge variant="success">New</Badge>,
className: !isAnalyticsBucketsEnabled
? 'opacity-50 cursor-not-allowed pointer-events-none'
: '',
enabled: isAnalyticsBucketsEnabled,
},
{
id: 'create-storage-bucket-vectors',
name: 'Create Storage Bucket (Vectors)',
route: `/project/${ref}/storage/vectors?new=true`,
icon: () => <VectorBucket />,
badge: () => <Badge variant="success">New</Badge>,
className: !isVectorBucketsEnabled
? 'opacity-50 cursor-not-allowed pointer-events-none'
: '',
enabled: isVectorBucketsEnabled,
},
].filter(Boolean) as ICommand[])
: [],
[ref, storageEnabled, isAnalyticsBucketsEnabled, isVectorBucketsEnabled]
)
const integrationsCommands = useMemo(() => {
// Sort integrations: Postgres modules (non-wrappers) first, then wrappers
const sortedIntegrations = [...allIntegrations].sort((a, b) => {
const aIsWrapper = a.type === 'wrapper' ? 1 : 0
const bIsWrapper = b.type === 'wrapper' ? 1 : 0
return aIsWrapper - bIsWrapper
})
return sortedIntegrations
.map((integration) => {
const route = getIntegrationRoute(integration, ref, installedIntegrationIds)
if (!route) return null
const isWrapper = integration.type === 'wrapper'
// For wrappers, use the integration icon with wrapper styling
// For Postgres modules, use plain icons
const getIcon = () => {
if (isWrapper) {
return (
<div className="w-6 h-6 relative bg-white border rounded-md flex items-center justify-center [&>img]:p-1! [&>svg]:p-1!">
{integration.icon()}
</div>
)
}
// Use plain icons for Postgres modules
switch (integration.id) {
case 'vault':
return <Vault />
case 'cron':
return <Clock5 />
case 'webhooks':
return <Webhook />
case 'queues':
return <Layers />
default:
// Fallback to integration icon for other Postgres modules
return integration.icon()
}
}
return {
id: `create-integration-${integration.id}`,
name: getIntegrationCommandName(integration),
route,
icon: getIcon,
}
})
.filter(Boolean) as ICommand[]
}, [ref, allIntegrations, installedIntegrationIds])
// Observability commands are only available on the platform, not for self-hosted/CLI
const observabilityCommands = useMemo(
() => [
...(IS_PLATFORM
? reportsEnabled
? ([
{
id: 'create-observability-report',
name: 'Create Custom Report',
route: `/project/${ref}/observability/api-overview?newReport=true`,
icon: () => <Telescope />,
},
].filter(Boolean) as ICommand[])
: []
: []),
],
[ref, reportsEnabled]
)
const sections = useMemo(
() => [
{
id: 'create-database',
name: 'Database',
commands: databaseCommands,
},
...(authCommands.length > 0
? [
{
id: 'create-auth',
name: 'Auth',
commands: authCommands,
},
]
: []),
...(edgeFunctionsCommands.length > 0
? [
{
id: 'create-edge-functions',
name: 'Edge Functions',
commands: edgeFunctionsCommands,
},
]
: []),
...(storageCommands.length > 0
? [
{
id: 'create-storage',
name: 'Storage',
commands: storageCommands,
},
]
: []),
...(observabilityCommands.length > 0
? [
{
id: 'create-observability',
name: 'Observability',
commands: observabilityCommands,
},
]
: []),
...(integrationsCommands.length > 0
? [
{
id: 'create-integrations',
name: 'Integrations',
commands: integrationsCommands,
},
]
: []),
],
[
databaseCommands,
authCommands,
edgeFunctionsCommands,
storageCommands,
integrationsCommands,
observabilityCommands,
]
)
useRegisterPage(
CREATE_STUDIO_ENTITY,
{
type: PageType.Commands,
sections,
},
{
deps: [sections],
enabled: true,
}
)
useRegisterCommands(
COMMAND_MENU_SECTIONS.ACTIONS,
[
{
id: 'create-studio-entity',
name: 'Create...',
action: () => setPage(CREATE_STUDIO_ENTITY),
icon: () => <Plus />,
},
],
{
...options,
orderSection: (sections) => sections,
sectionMeta: { priority: 3 },
enabled: true,
}
)
}