import { PermissionAction } from '@supabase/shared-types/out/constants' import { Check, ChevronDown, Copy, Database, KeyRound, Link2, Terminal } from 'lucide-react' import { parseAsBoolean, useQueryState } from 'nuqs' import { useEffect, useMemo, useRef, useState } from 'react' import { Button, cn, copyToClipboard, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from 'ui' import { ShimmeringLoader } from 'ui-patterns' import { getConnectionStrings } from '@/components/interfaces/Connect/DatabaseSettings.utils' import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { pluckObjectFields } from '@/lib/helpers' const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user'] as const const EMPTY_CONNECTION_INFO = { db_user: '', db_host: '', db_port: '', db_name: '', } interface ProjectConnectionPopoverProps { projectRef?: string } export const ProjectConnectionPopover = ({ projectRef }: ProjectConnectionPopoverProps) => { const [open, setOpen] = useState(false) const [copiedItem, setCopiedItem] = useState(null) const copiedTimeoutRef = useRef | null>(null) const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false)) const { isLoading: isLoadingPermissions, can: canReadAPIKeys } = useAsyncCheckPermissions( PermissionAction.READ, 'service_api_keys' ) const { data: projectUrl, isPending: isLoadingApiUrl } = useProjectApiUrl({ projectRef }) const { data: apiKeys, isLoading: isLoadingKeys } = useAPIKeysQuery( { projectRef }, { enabled: open && canReadAPIKeys } ) const { publishableKey } = canReadAPIKeys ? getKeys(apiKeys) : { publishableKey: null } const { data: databases, isLoading: isLoadingDatabases } = useReadReplicasQuery( { projectRef }, { enabled: open && !!projectRef } ) const primaryDatabase = databases?.find((db) => db.identifier === projectRef) const directConnectionString = useMemo(() => { if ( !primaryDatabase?.db_host || !primaryDatabase?.db_name || !primaryDatabase?.db_user || !primaryDatabase?.db_port ) { return '' } const connectionInfo = pluckObjectFields(primaryDatabase, [...DB_FIELDS]) return getConnectionStrings({ connectionInfo: { ...EMPTY_CONNECTION_INFO, ...connectionInfo }, metadata: { projectRef }, }).direct.uri }, [primaryDatabase, projectRef]) const cliCommands = useMemo( () => [ 'supabase login', 'supabase init', `supabase link --project-ref ${projectRef ?? 'PROJECT_REF_UNAVAILABLE'}`, ].join('\n'), [projectRef] ) const menuItems = useMemo( () => [ { label: 'Project URL', value: projectUrl ?? '', displayValue: isLoadingApiUrl ? 'Loading project URL...' : (projectUrl ?? 'Project URL unavailable'), disabled: isLoadingApiUrl || !projectUrl, icon: Link2, }, { label: 'Publishable key', value: publishableKey?.api_key ?? '', displayValue: isLoadingPermissions || isLoadingKeys ? 'Loading publishable key...' : canReadAPIKeys ? (publishableKey?.api_key ?? 'Publishable key unavailable') : "You don't have permission to view API keys.", disabled: isLoadingPermissions || isLoadingKeys || !canReadAPIKeys || !publishableKey?.api_key, icon: KeyRound, }, { label: 'Direct connection string', value: directConnectionString, displayValue: isLoadingDatabases ? 'Loading connection string...' : directConnectionString || 'Connection string unavailable', disabled: isLoadingDatabases || !directConnectionString, icon: Database, }, { label: 'CLI setup commands', value: cliCommands, displayValue: cliCommands.replace(/\n/g, ' - '), disabled: !projectRef, icon: Terminal, }, ], [ canReadAPIKeys, cliCommands, directConnectionString, isLoadingApiUrl, isLoadingDatabases, isLoadingKeys, isLoadingPermissions, projectRef, projectUrl, publishableKey?.api_key, ] ) useEffect(() => { return () => { if (copiedTimeoutRef.current) clearTimeout(copiedTimeoutRef.current) } }, []) return (
{isLoadingApiUrl ? ( ) : ( {projectUrl ?? 'Project URL unavailable'} )} {menuItems.map((item) => { const Icon = item.icon return ( { event.preventDefault() if (item.disabled) return copyToClipboard(item.value) setCopiedItem(item.label) if (copiedTimeoutRef.current) clearTimeout(copiedTimeoutRef.current) copiedTimeoutRef.current = setTimeout(() => setCopiedItem(null), 1500) }} >
{item.label}
{item.displayValue}
{copiedItem === item.label ? : }
) })}
) }