From 4e0b5ced364f131dd284cf7ef11fa9ad1f2e49c5 Mon Sep 17 00:00:00 2001 From: kemal Date: Tue, 10 Mar 2026 13:46:11 +0000 Subject: [PATCH] feat: compute upgrade banner --- .../components/layouts/DefaultLayout.tsx | 42 +++++++++++- .../Banners/BannerComputeExhaustion.tsx | 65 +++++++++++++++++++ packages/common/constants/local-storage.ts | 3 + packages/common/telemetry-constants.ts | 26 ++++++++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 apps/studio/components/ui/BannerStack/Banners/BannerComputeExhaustion.tsx diff --git a/apps/studio/components/layouts/DefaultLayout.tsx b/apps/studio/components/layouts/DefaultLayout.tsx index 88d6852d3d4..282ef386347 100644 --- a/apps/studio/components/layouts/DefaultLayout.tsx +++ b/apps/studio/components/layouts/DefaultLayout.tsx @@ -1,6 +1,8 @@ import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { AppBannerWrapper } from 'components/interfaces/App/AppBannerWrapper' import { Sidebar } from 'components/interfaces/Sidebar' +import { BannerComputeExhaustion } from 'components/ui/BannerStack/Banners/BannerComputeExhaustion' +import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useCheckLatestDeploy } from 'hooks/use-check-latest-deploy' import { useRouter } from 'next/router' @@ -9,7 +11,7 @@ import { useAppStateSnapshot } from 'state/app-state' import { ResizablePanel, ResizablePanelGroup, SidebarProvider } from 'ui' import { BannerStack } from '../ui/BannerStack/BannerStack' -import { BannerStackProvider } from '../ui/BannerStack/BannerStackProvider' +import { BannerStackProvider, useBannerStack } from '../ui/BannerStack/BannerStackProvider' import { LayoutHeader } from './ProjectLayout/LayoutHeader/LayoutHeader' import { LayoutSidebar } from './ProjectLayout/LayoutSidebar' import { LayoutSidebarProvider } from './ProjectLayout/LayoutSidebar/LayoutSidebarProvider' @@ -17,6 +19,43 @@ import MobileNavigationBar from './ProjectLayout/NavigationBar/MobileNavigationB import { MobileSheetProvider } from './ProjectLayout/NavigationBar/MobileSheetContext' import { ProjectContextProvider } from './ProjectLayout/ProjectContext' +const ComputeExhaustionBannerController = () => { + const { ref } = useParams() + const { addBanner, dismissBanner } = useBannerStack() + + const [isBannerDismissed] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.COMPUTE_EXHAUSTION_BANNER_DISMISSED(ref ?? ''), + false + ) + + const { data: resourceWarnings } = useResourceWarningsQuery({ ref }) + const projectWarnings = resourceWarnings?.find((w) => w.project === ref) + + const cpuLevel = projectWarnings?.cpu_exhaustion ?? null + const memoryLevel = projectWarnings?.memory_and_swap_exhaustion ?? null + const worstLevel = + cpuLevel === 'critical' || memoryLevel === 'critical' + ? 'critical' + : cpuLevel === 'warning' || memoryLevel === 'warning' + ? 'warning' + : null + + useEffect(() => { + if (worstLevel && !isBannerDismissed) { + addBanner({ + id: 'compute-exhaustion-banner', + isDismissed: false, + content: , + priority: 10, + }) + } else { + dismissBanner('compute-exhaustion-banner') + } + }, [worstLevel, isBannerDismissed, addBanner, dismissBanner]) + + return null +} + export interface DefaultLayoutProps { headerTitle?: string hideMobileMenu?: boolean @@ -77,6 +116,7 @@ export const DefaultLayout = ({ +
{/* Top Banner */} diff --git a/apps/studio/components/ui/BannerStack/Banners/BannerComputeExhaustion.tsx b/apps/studio/components/ui/BannerStack/Banners/BannerComputeExhaustion.tsx new file mode 100644 index 00000000000..ce6fc5296dc --- /dev/null +++ b/apps/studio/components/ui/BannerStack/Banners/BannerComputeExhaustion.tsx @@ -0,0 +1,65 @@ +import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' +import { useTrack } from 'lib/telemetry/track' +import { Cpu } from 'lucide-react' +import Link from 'next/link' +import { Button } from 'ui' +import { BannerCard } from '../BannerCard' +import { useBannerStack } from '../BannerStackProvider' + +interface BannerComputeExhaustionProps { + isCritical: boolean +} + +export const BannerComputeExhaustion = ({ isCritical }: BannerComputeExhaustionProps) => { + const track = useTrack() + const { ref } = useParams() + const { dismissBanner } = useBannerStack() + const [, setIsDismissed] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.COMPUTE_EXHAUSTION_BANNER_DISMISSED(ref ?? ''), + false + ) + + const title = isCritical ? 'Compute is maxed out' : 'Compute is running high' + const description = isCritical + ? 'Your project CPU or memory is at 100%. Performance is severely affected — consider upgrading your compute.' + : 'Your project is experiencing high CPU or memory usage. Upgrading your compute can prevent downtime.' + + return ( + { + setIsDismissed(true) + dismissBanner('compute-exhaustion-banner') + track('compute_exhaustion_banner_dismiss_button_clicked', { is_critical: isCritical }) + }} + > +
+
+
+ +
+
+
+

{title}

+

{description}

+
+
+ +
+
+
+ ) +} diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 361262d2f3c..8cd612d3e98 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -103,6 +103,9 @@ export const LOCAL_STORAGE_KEYS = { // Observability banner dismissed OBSERVABILITY_BANNER_DISMISSED: (ref: string) => `observability-banner-dismissed-${ref}`, + // Compute exhaustion banner dismissed + COMPUTE_EXHAUSTION_BANNER_DISMISSED: (ref: string) => + `compute-exhaustion-banner-dismissed-${ref}`, TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED: (ref: string) => `table-editor-new-filter-banner-dismissed-${ref}`, diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 4eb26581df4..7b3910151ed 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -1188,6 +1188,30 @@ export interface IndexAdvisorBannerDismissButtonClickedEvent { groups: TelemetryGroups } +/** + * Compute exhaustion banner dismiss button clicked event. + * + * @group Events + * @source studio + */ +export interface ComputeExhaustionBannerDismissButtonClickedEvent { + action: 'compute_exhaustion_banner_dismiss_button_clicked' + properties: { is_critical: boolean } + groups: TelemetryGroups +} + +/** + * Compute exhaustion banner upgrade button clicked event. + * + * @group Events + * @source studio + */ +export interface ComputeExhaustionBannerUpgradeButtonClickedEvent { + action: 'compute_exhaustion_banner_upgrade_button_clicked' + properties: { is_critical: boolean } + groups: TelemetryGroups +} + /** * Index Advisor tab clicked event. * @@ -2842,3 +2866,5 @@ export type TelemetryEvent = | IntegrationInstallFailedEvent | IntegrationUninstallCompletedEvent | RlsEventTriggerBannerCreateButtonClickedEvent + | ComputeExhaustionBannerDismissButtonClickedEvent + | ComputeExhaustionBannerUpgradeButtonClickedEvent