diff --git a/apps/studio/components/interfaces/ProjectHome/HighAvailabilityBadge.tsx b/apps/studio/components/interfaces/ProjectHome/HighAvailabilityBadge.tsx new file mode 100644 index 00000000000..ab637850c6e --- /dev/null +++ b/apps/studio/components/interfaces/ProjectHome/HighAvailabilityBadge.tsx @@ -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 ( + + +
+ {size === 'small' ? 'HA' : 'High Availability'} +
+
+ +
Multigres
+ +
+ +
+ +
+

+ A horizontally scalable Postgres architecture that supports highly-available and + globally distributed deployments. +

+ + Read more + + +
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/ProjectHome/ServerLightGrid.tsx b/apps/studio/components/interfaces/ProjectHome/ServerLightGrid.tsx new file mode 100644 index 00000000000..786d56d21de --- /dev/null +++ b/apps/studio/components/interfaces/ProjectHome/ServerLightGrid.tsx @@ -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 ( +
+ +
+ ) +}) + +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>(() => new Set()) + const timers = useRef>>(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) => ), + [active] + ) + + return ( +
+ {cells} +
+ ) +} diff --git a/apps/studio/components/interfaces/ProjectHome/TopSection.tsx b/apps/studio/components/interfaces/ProjectHome/TopSection.tsx index 98ccfc4008a..0b102da46aa 100644 --- a/apps/studio/components/interfaces/ProjectHome/TopSection.tsx +++ b/apps/studio/components/interfaces/ProjectHome/TopSection.tsx @@ -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 && }