AddHookDropdown.tsx129 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useParams } from 'common'
3import { ChevronDown } from 'lucide-react'
4import { useMemo } from 'react'
5import {
6 Button,
7 DropdownMenu,
8 DropdownMenuContent,
9 DropdownMenuItem,
10 DropdownMenuLabel,
11 DropdownMenuSeparator,
12 DropdownMenuTrigger,
13} from 'ui'
14
15import { Hook, HOOK_DEFINITION_TITLE, HOOKS_DEFINITIONS } from './hooks.constants'
16import { extractMethod, isValidHook } from './hooks.utils'
17import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
18import { InlineLink } from '@/components/ui/InlineLink'
19import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
20import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
21import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
22import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
23
24interface AddHookDropdownProps {
25 buttonText?: string
26 align?: 'end' | 'center'
27 type?: 'primary' | 'default'
28 open?: boolean
29 onOpenChange?: (open: boolean) => void
30 onSelectHook: (hook: HOOK_DEFINITION_TITLE) => void
31}
32
33export const AddHookDropdown = ({
34 buttonText = 'Add hook',
35 align = 'end',
36 type = 'primary',
37 open,
38 onOpenChange,
39 onSelectHook,
40}: AddHookDropdownProps) => {
41 const { ref: projectRef } = useParams()
42 const { data: organization } = useSelectedOrganizationQuery()
43
44 const { data: authConfig } = useAuthConfigQuery({ projectRef })
45 const { can: canUpdateAuthHook } = useAsyncCheckPermissions(PermissionAction.AUTH_EXECUTE, '*')
46 const { getEntitlementSetValues: getEntitledHookSet } = useCheckEntitlements('auth.hooks')
47 const entitledHookSet = getEntitledHookSet()
48
49 const { availableHooks, nonAvailableHooks } = useMemo(() => {
50 const allHooks: Hook[] = HOOKS_DEFINITIONS.map((definition) => ({
51 ...definition,
52 enabled: authConfig?.[definition.enabledKey] || false,
53 method: extractMethod(
54 authConfig?.[definition.uriKey] || '',
55 authConfig?.[definition.secretsKey] || ''
56 ),
57 }))
58
59 const availableHooks: Hook[] = allHooks.filter(
60 (h) => !isValidHook(h) && entitledHookSet.includes(h.entitlementKey)
61 )
62
63 const nonAvailableHooks: Hook[] = allHooks.filter(
64 (h) => !isValidHook(h) && !entitledHookSet.includes(h.entitlementKey)
65 )
66
67 return { availableHooks, nonAvailableHooks }
68 }, [entitledHookSet, authConfig])
69
70 if (!canUpdateAuthHook) {
71 return (
72 <ButtonTooltip
73 disabled
74 type={type}
75 tooltip={{
76 content: { side: 'bottom', text: 'You need additional permissions to add auth hooks' },
77 }}
78 >
79 {buttonText}
80 </ButtonTooltip>
81 )
82 }
83
84 return (
85 <DropdownMenu open={open} onOpenChange={onOpenChange}>
86 <DropdownMenuTrigger asChild>
87 <Button type={type} iconRight={<ChevronDown />}>
88 {buttonText}
89 </Button>
90 </DropdownMenuTrigger>
91 <DropdownMenuContent className="w-76" align={align}>
92 <div>
93 {availableHooks.length === 0 && (
94 <DropdownMenuLabel className="text-foreground-light">
95 All available hooks have been added
96 </DropdownMenuLabel>
97 )}
98 {availableHooks.map((h) => (
99 <DropdownMenuItem key={h.title} onClick={() => onSelectHook(h.title)}>
100 {h.title}
101 </DropdownMenuItem>
102 ))}
103 </div>
104 {nonAvailableHooks.length > 0 && (
105 <>
106 {availableHooks.length > 0 && <DropdownMenuSeparator />}
107
108 <DropdownMenuLabel className="grid gap-1 bg-surface-200">
109 <p className="text-foreground-light">Team or Enterprise Plan required</p>
110 <p className="text-foreground-lighter text-xs">
111 The following hooks are not available on{' '}
112 <InlineLink href={`/org/${organization?.slug ?? '_'}/billing`}>
113 your plan
114 </InlineLink>
115 .
116 </p>
117 </DropdownMenuLabel>
118
119 {nonAvailableHooks.map((h) => (
120 <DropdownMenuItem key={h.title} disabled={true}>
121 {h.title}
122 </DropdownMenuItem>
123 ))}
124 </>
125 )}
126 </DropdownMenuContent>
127 </DropdownMenu>
128 )
129}