Files
supabase/apps/studio/components/interfaces/HomeNew/ProjectConnectionHoverCard.tsx
Saxon FletcherandJoshen Lim cf24eab382 Introduce connection details in home (#42400)
<img width="855" height="482" alt="image"
src="https://github.com/user-attachments/assets/9f4451aa-2ed6-461a-88b2-933385eecda9"
/>

<img width="887" height="573" alt="image"
src="https://github.com/user-attachments/assets/a8686a38-f25b-4e1d-960a-70331b75f251"
/>

This introduces database url under project title which reveals summary
to connection details that then reveals full connect dialog if needed.
Increases exposure to connection info which we know some people struggle
to find.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a project connection hover card showing Data API (project URL,
publishable key) and Database connection string with copyable, read-only
fields, loading/permission states, and a "Get connected" action to open
connection setup.

* **UI Improvements**
* Integrated the connection hover card into the top section; reorganized
title and badge layout for clearer hierarchy and adjacent controls.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-02-04 10:29:35 +10:00

160 lines
5.8 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Label } from '@ui/components/shadcn/ui/label'
import { getConnectionStrings } from 'components/interfaces/Connect/DatabaseSettings.utils'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { pluckObjectFields } from 'lib/helpers'
import { Plug } from 'lucide-react'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { type ReactNode, useMemo, useState } from 'react'
import { Button, HoverCard, HoverCardContent, HoverCardTrigger } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
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: '',
}
const DetailRow = ({ label, children }: { label: string; children: ReactNode }) => {
return (
<div className="flex flex-col gap-2">
<Label>{label}</Label>
{children}
</div>
)
}
interface ProjectConnectionHoverCardProps {
projectRef?: string
}
export const ProjectConnectionHoverCard = ({ projectRef }: ProjectConnectionHoverCardProps) => {
const [open, setOpen] = useState(false)
const [_, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const { data: settings, isPending: isLoadingSettings } = useProjectSettingsV2Query(
{ projectRef },
{ enabled: !!projectRef }
)
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint
const projectUrl = endpoint ? `${protocol}://${endpoint}` : undefined
const { isLoading: isLoadingPermissions, can: canReadAPIKeys } = useAsyncCheckPermissions(
PermissionAction.READ,
'service_api_keys'
)
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 projectUrlLabel =
projectUrl ?? (isLoadingSettings ? 'Loading project URL...' : 'Project URL unavailable')
return (
<HoverCard openDelay={250} closeDelay={100} open={open} onOpenChange={setOpen}>
<HoverCardTrigger asChild>
<button onClick={() => setShowConnect(true)} className="flex items-center gap-4 mt-3 group">
<div className=" transition w-8 h-8 rounded-md bg-surface-75 group-hover:bg-muted border flex items-center justify-center">
<Plug strokeWidth={1.5} size={16} className="text-foreground-light rotate-90" />
</div>
<span className="transition text-foreground-light group-hover:text-foreground underline decoration-dotted decoration-foreground-muted underline-offset-4 max-w-[320px] text-left">
{projectUrlLabel}
</span>
</button>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" className="w-[420px] p-0">
<div className="p-4 border-b space-y-4">
<h3 className="heading-meta text-foreground-light">Data API</h3>
<DetailRow label="Project URL">
<Input
copy
readOnly
className="font-mono text-xs"
value={projectUrl ?? ''}
placeholder="Project URL unavailable"
/>
</DetailRow>
<DetailRow label="Publishable Key">
{isLoadingPermissions || isLoadingKeys ? (
<div className="text-xs text-foreground-lighter">Loading publishable key...</div>
) : canReadAPIKeys ? (
<Input
copy
readOnly
className="font-mono text-xs"
value={publishableKey?.api_key ?? ''}
placeholder="Publishable key unavailable"
/>
) : (
<div className="text-xs text-foreground-lighter">
You don't have permission to view API keys.
</div>
)}
</DetailRow>
</div>
<div className="p-4 space-y-4 border-b">
<h3 className="heading-meta text-foreground-light"> Database </h3>
<DetailRow label="Direct connection string">
{isLoadingDatabases ? (
<div className="text-xs text-foreground-lighter">Loading connection string...</div>
) : (
<Input
copy
readOnly
className="font-mono text-xs"
value={directConnectionString}
placeholder="Connection string unavailable"
/>
)}
</DetailRow>
</div>
<div className="p-4">
<Button
icon={<Plug className="rotate-90" />}
type="default"
size="medium"
className="w-full"
onClick={() => setShowConnect(true)}
>
Get connected
</Button>
</div>
</HoverCardContent>
</HoverCard>
)
}