import { useParams } from 'common' import { AlertTriangle, BookOpen, ChartLine, ChevronDown, Sparkles, Wrench } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { COMPUTE_DISK } from 'shared-data' import { Alert, AlertDescription, AlertTitle, Button, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from 'ui' import { RESOURCE_WARNING_MESSAGES } from './ResourceExhaustionWarningBanner.constants' import { getWarningContent } from './ResourceExhaustionWarningBanner.utils' import { mapComputeSizeNameToAddonVariantId } from '@/components/interfaces/DiskManagement/DiskManagement.utils' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useTrack } from '@/lib/telemetry/track' import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' const COMPUTE_UPGRADE_METRICS = ['disk_io', 'cpu', 'ram'] const COMPUTE_UPGRADE_WARNING_TYPES = [ 'disk_io_exhaustion', 'cpu_exhaustion', 'memory_and_swap_exhaustion', ] export const ResourceExhaustionWarningBanner = () => { const { ref } = useParams() const router = useRouter() const { data: organization, isLoading: isOrgLoading } = useSelectedOrganizationQuery() const { data: project } = useSelectedProjectQuery() const diskIoBaselineLabel = (() => { const variant = mapComputeSizeNameToAddonVariantId(project?.infra_compute_size) const baseline = COMPUTE_DISK[variant]?.baselineThroughputMBps return typeof baseline === 'number' ? `${baseline} MB/s` : 'its baseline' })() const applyDiskIoBaseline = (text?: string) => text ? text.replace(/\{baseline\}/g, diskIoBaselineLabel) : text const { openSidebar } = useSidebarManagerSnapshot() const aiSnap = useAiAssistantStateSnapshot() const track = useTrack() const { data: resourceWarnings } = useResourceWarningsQuery({ ref: ref }) // [Joshen Cleanup] JFYI this client side filtering can be cleaned up once BE changes are live which will only return the warnings based on the provided ref const projectResourceWarnings = (resourceWarnings ?? [])?.find( (warning) => warning.project === ref ) // [Joshen] Read only takes higher precedence over multiple resource warnings const activeWarnings = projectResourceWarnings !== undefined ? projectResourceWarnings.is_readonly_mode_enabled ? ['is_readonly_mode_enabled'] : Object.keys(projectResourceWarnings).filter( (property) => property !== 'project' && property !== 'is_readonly_mode_enabled' && projectResourceWarnings[property as keyof typeof projectResourceWarnings] !== null ) : [] const hasCriticalWarning = projectResourceWarnings !== undefined ? activeWarnings.some( (x) => projectResourceWarnings[x as keyof typeof projectResourceWarnings] === 'critical' ) : false const isCritical = activeWarnings.includes('is_readonly_mode_enabled') || hasCriticalWarning const warningContent = projectResourceWarnings !== undefined ? getWarningContent(projectResourceWarnings, activeWarnings[0], 'bannerContent') : undefined const title = applyDiskIoBaseline( activeWarnings.length > 1 ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.bannerContent[ hasCriticalWarning ? 'critical' : 'warning' ].title : warningContent?.title ) const description = applyDiskIoBaseline( activeWarnings.length > 1 ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.bannerContent[ hasCriticalWarning ? 'critical' : 'warning' ].description : warningContent?.description ) const learnMoreUrl = activeWarnings.length > 1 ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.docsUrl : RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES] ?.docsUrl const singleWarningMessage = activeWarnings.length === 1 ? RESOURCE_WARNING_MESSAGES[activeWarnings[0]] : undefined const metricsHref = singleWarningMessage?.metricsHref?.replace('[ref]', ref ?? 'default') const metric = activeWarnings.length > 1 ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.metric : RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES] ?.metric const correctionUrlVariants = { undefined: undefined, null: '/project/[ref]/settings/[infra-path]', disk_space: '/project/[ref]/settings/compute-and-disk', read_only: '/project/[ref]/settings/compute-and-disk', disk_io: '/project/[ref]/settings/compute-and-disk', cpu: '/project/[ref]/settings/compute-and-disk', ram: '/project/[ref]/settings/compute-and-disk', auth_email_rate_limit: '/project/[ref]/auth/rate-limits', auth_restricted_email_sending: '/project/[ref]/auth/smtp', default: (metric: string) => `/project/[ref]/settings/[infra-path]#${metric}`, } const getCorrectionUrl = (metric: string | undefined | null) => { const variant = metric === undefined ? 'undefined' : metric === null ? 'null' : metric const url = correctionUrlVariants[variant as keyof typeof correctionUrlVariants] || correctionUrlVariants.default(metric as string) return typeof url === 'function' ? url(metric as string) : url } const isFreePlan = organization?.plan?.id === 'free' // True for a single compute warning, or when all active warnings are compute-related const isComputeUpgradeMetric = (metric !== null && metric !== undefined && COMPUTE_UPGRADE_METRICS.includes(metric)) || (activeWarnings.length > 1 && activeWarnings.every((w) => COMPUTE_UPGRADE_WARNING_TYPES.includes(w))) const correctionUrl = (() => { if (isComputeUpgradeMetric && isFreePlan) { return `/org/${organization?.slug ?? '_'}/billing?panel=subscriptionPlan&source=resource_exhaustion_banner` } if (isComputeUpgradeMetric && activeWarnings.length > 1) { return `/project/${ref ?? 'default'}/settings/compute-and-disk` } return getCorrectionUrl(metric) ?.replace('[ref]', ref ?? 'default') ?.replace('[infra-path]', 'infrastructure') })() const buttonText = (() => { if (isComputeUpgradeMetric) return 'Upgrade compute' return activeWarnings.length > 1 ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.buttonText : RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES] ?.buttonText })() const aiPrompt = activeWarnings.length > 1 ? isComputeUpgradeMetric ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.aiPrompt : undefined : RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES] ?.aiPrompt const handleAskAI = () => { track('resource_exhaustion_banner_ai_assistant_clicked', { warningTypes: activeWarnings, }) openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) aiSnap.newChat({ initialInput: aiPrompt }) } const hasNoWarnings = activeWarnings.length === 0 const hasNoWarningContent = warningContent === undefined || (!warningContent?.title && !warningContent?.description) const isUsageOrInfraPage = router.pathname.endsWith('/usage') || router.pathname.endsWith('/infrastructure') // Compute warnings now link to compute-and-disk, so they should remain visible on infrastructure const onUsageOrInfraAndNotInReadOnlyMode = isUsageOrInfraPage && !activeWarnings.includes('is_readonly_mode_enabled') && !isComputeUpgradeMetric // Suppress when already on the target page (no-op CTA). Paid-plan compute warnings link to // compute-and-disk; free-plan links to billing instead, so we keep the banner visible for them. const onDatabaseSettingsAndInReadOnlyMode = router.pathname.endsWith('settings/compute-and-disk') && (activeWarnings.includes('is_readonly_mode_enabled') || (isComputeUpgradeMetric && !isFreePlan)) // these take precedence over each other, so there's only one active warning to check const activeWarning = RESOURCE_WARNING_MESSAGES[activeWarnings[0] as keyof typeof RESOURCE_WARNING_MESSAGES] const restrictToRoutes = activeWarning?.restrictToRoutes const isVisible = restrictToRoutes === undefined || restrictToRoutes.some((route: string) => { // check for exact match with /project/[ref] (project home) first // doing this let's us avoid checking with regex, keeping it simple if (route === '/project/[ref]') { const isExactMatch = router.pathname === '/project/[ref]' return isExactMatch } // For other routes, use the original startsWith logic const isMatch = router.pathname.startsWith(route) return isMatch }) if ( hasNoWarnings || hasNoWarningContent || onUsageOrInfraAndNotInReadOnlyMode || onDatabaseSettingsAndInReadOnlyMode || !isVisible ) { return null } return ( svg]:left-6 px-6 [&>svg]:w-[26px] [&>svg]:h-[26px]' )} >
{title} {description}
{learnMoreUrl !== undefined && aiPrompt !== undefined ? ( {metricsHref !== undefined && ( View metrics )} Documentation Ask AI Assistant ) : learnMoreUrl !== undefined ? ( ) : aiPrompt !== undefined ? ( ) : null} {correctionUrl !== undefined && ( )}
) }