mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(studio): high availability badge on projects homepage (#44269)
Adds the following badge for Multigres projects: <img width="716" height="377" alt="Screenshot 2026-03-27 at 4 54 40 PM" src="https://github.com/user-attachments/assets/b8d7b3a5-4d6d-4b17-b18d-7f58c2e2b81c" /> <img width="688" height="392" alt="Screenshot 2026-03-27 at 4 54 48 PM" src="https://github.com/user-attachments/assets/853a0ba7-de69-4476-b2ba-d596740e28ab" /> To test: - ensure the badge doesn't show on non-multigres projects - create a project with HA enabled and check the badge is visible <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a High Availability badge indicator to the project home view that displays when a project has high availability enabled * Hovering over the badge reveals detailed information about the high availability configuration, including an animated server grid visualization and a link to the high availability documentation <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> Co-authored-by: Ali Waseem <waseema393@gmail.com>
This commit is contained in:
3 files changed
+171
-11
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { cn, HoverCard, HoverCardContent, HoverCardTrigger, Separator } from 'ui'
|
||||
|
||||
import { ServerLightGrid } from './ServerLightGrid'
|
||||
|
||||
interface HighAvailabilityBadgeProps {
|
||||
size?: 'default' | 'small'
|
||||
}
|
||||
|
||||
export function HighAvailabilityBadge({ size = 'default' }: HighAvailabilityBadgeProps) {
|
||||
return (
|
||||
<HoverCard openDelay={200} closeDelay={100}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md text-center font-mono uppercase',
|
||||
'cursor-default whitespace-nowrap font-medium tracking-[0.06em] text-[11px] leading-[1.1] px-[5.5px] py-[3px]',
|
||||
'transition-all',
|
||||
'border border-purple-700 dark:border-purple-600/50',
|
||||
'bg-purple-400 text-purple-1100 dark:bg-purple-100'
|
||||
)}
|
||||
>
|
||||
{size === 'small' ? 'HA' : 'High Availability'}
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="bottom" align="start" className="w-72 overflow-hidden p-0">
|
||||
<div className="p-2 px-5 text-xs text-foreground-lighter">Multigres</div>
|
||||
<Separator />
|
||||
<div className="h-24 bg-surface-75">
|
||||
<ServerLightGrid />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-1 p-3 px-5">
|
||||
<p className="text-sm text-foreground-light">
|
||||
A horizontally scalable Postgres architecture that supports highly-available and
|
||||
globally distributed deployments.
|
||||
</p>
|
||||
<Link
|
||||
href={`${DOCS_URL}/guides/deployment/high-availability`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-1 inline-flex items-center gap-1 text-xs text-foreground-lighter transition-colors hover:text-foreground"
|
||||
>
|
||||
Read more
|
||||
<ArrowRight size={12} strokeWidth={1.5} />
|
||||
</Link>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
const ROWS = 4
|
||||
const COLS = 6
|
||||
const TOTAL = ROWS * COLS
|
||||
|
||||
const ServerLightCell = memo(function ServerLightCell({
|
||||
index,
|
||||
isActive,
|
||||
}: {
|
||||
index: number
|
||||
isActive: boolean
|
||||
}) {
|
||||
const row = Math.floor(index / COLS)
|
||||
const col = index % COLS
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center',
|
||||
col < COLS - 1 && 'border-r border-dotted border-foreground/10',
|
||||
row < ROWS - 1 && 'border-b border-dotted border-foreground/10'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'block h-1 w-1 rounded-full motion-safe:transition-all motion-safe:duration-150',
|
||||
isActive ? 'bg-brand-500 shadow-[0_0_6px_1px] shadow-brand-500/50' : 'bg-foreground/15'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const GRID_STYLE = {
|
||||
gridTemplateColumns: `repeat(${COLS}, 1fr)`,
|
||||
gridTemplateRows: `repeat(${ROWS}, 1fr)`,
|
||||
} as const
|
||||
|
||||
const CELL_INDICES = Array.from({ length: TOTAL }, (_, i) => i)
|
||||
|
||||
function randomDelay() {
|
||||
return 400 + Math.random() * 1400
|
||||
}
|
||||
|
||||
function randomOnDuration() {
|
||||
return 200 + Math.random() * 800
|
||||
}
|
||||
|
||||
export function ServerLightGrid() {
|
||||
const [active, setActive] = useState<Set<number>>(() => new Set())
|
||||
const timers = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map())
|
||||
|
||||
useEffect(() => {
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||
|
||||
const timerMap = timers.current
|
||||
|
||||
function scheduleBlink(index: number) {
|
||||
const offDelay = randomDelay()
|
||||
const timer = setTimeout(() => {
|
||||
setActive((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.add(index)
|
||||
return next
|
||||
})
|
||||
|
||||
const onDuration = randomOnDuration()
|
||||
const offTimer = setTimeout(() => {
|
||||
setActive((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.delete(index)
|
||||
return next
|
||||
})
|
||||
scheduleBlink(index)
|
||||
}, onDuration)
|
||||
|
||||
timerMap.set(index, offTimer)
|
||||
}, offDelay)
|
||||
|
||||
timerMap.set(index, timer)
|
||||
}
|
||||
|
||||
for (let i = 0; i < TOTAL; i++) {
|
||||
scheduleBlink(i)
|
||||
}
|
||||
|
||||
return () => {
|
||||
timerMap.forEach(clearTimeout)
|
||||
timerMap.clear()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const cells = useMemo(
|
||||
() => CELL_INDICES.map((i) => <ServerLightCell key={i} index={i} isActive={active.has(i)} />),
|
||||
[active]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full" style={GRID_STYLE}>
|
||||
{cells}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
import { ReactFlowProvider } from '@xyflow/react'
|
||||
import { ActivityStats } from 'components/interfaces/ProjectHome/ActivityStats'
|
||||
import { ProjectConnectionPopover } from 'components/interfaces/ProjectHome/ProjectConnectionPopover'
|
||||
import { ProjectPausedState } from 'components/layouts/ProjectLayout/PausedState/ProjectPausedState'
|
||||
import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper'
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
import { ProjectUpgradeFailedBanner } from 'components/ui/ProjectUpgradeFailedBanner'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { DOCS_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import Link from 'next/link'
|
||||
import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
|
||||
import { InstanceConfiguration } from '../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration'
|
||||
import { ActivityStats } from '@/components/interfaces/ProjectHome/ActivityStats'
|
||||
import { ProjectConnectionPopover } from '@/components/interfaces/ProjectHome/ProjectConnectionPopover'
|
||||
import { ProjectPausedState } from '@/components/layouts/ProjectLayout/PausedState/ProjectPausedState'
|
||||
import { ComputeBadgeWrapper } from '@/components/ui/ComputeBadgeWrapper'
|
||||
import { InlineLink } from '@/components/ui/InlineLink'
|
||||
import { ProjectUpgradeFailedBanner } from '@/components/ui/ProjectUpgradeFailedBanner'
|
||||
import { useBranchesQuery } from '@/data/branches/branches-query'
|
||||
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useIsOrioleDb, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { DOCS_URL, PROJECT_STATUS } from '@/lib/constants'
|
||||
import { HighAvailabilityBadge } from './HighAvailabilityBadge'
|
||||
|
||||
export const TopSection = () => {
|
||||
const isOrioleDb = useIsOrioleDb()
|
||||
@@ -79,6 +80,7 @@ export const TopSection = () => {
|
||||
cloudProvider={project?.cloud_provider}
|
||||
computeSize={project?.infra_compute_size}
|
||||
/>
|
||||
{project?.high_availability && <HighAvailabilityBadge />}
|
||||
</div>
|
||||
</div>
|
||||
<ProjectConnectionPopover projectRef={project?.ref} />
|
||||
|
||||
Reference in new issue
Block a user