AddHookDropdown.tsx129 lines · main
| 1 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 2 | import { useParams } from 'common' |
| 3 | import { ChevronDown } from 'lucide-react' |
| 4 | import { useMemo } from 'react' |
| 5 | import { |
| 6 | Button, |
| 7 | DropdownMenu, |
| 8 | DropdownMenuContent, |
| 9 | DropdownMenuItem, |
| 10 | DropdownMenuLabel, |
| 11 | DropdownMenuSeparator, |
| 12 | DropdownMenuTrigger, |
| 13 | } from 'ui' |
| 14 | |
| 15 | import { Hook, HOOK_DEFINITION_TITLE, HOOKS_DEFINITIONS } from './hooks.constants' |
| 16 | import { extractMethod, isValidHook } from './hooks.utils' |
| 17 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 18 | import { InlineLink } from '@/components/ui/InlineLink' |
| 19 | import { useAuthConfigQuery } from '@/data/auth/auth-config-query' |
| 20 | import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' |
| 21 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 22 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 23 | |
| 24 | interface 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 | |
| 33 | export 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 | } |