diff --git a/apps/studio/components/interfaces/ProjectHome/ComputeUpgradeFloatingNotice.tsx b/apps/studio/components/interfaces/ProjectHome/ComputeUpgradeFloatingNotice.tsx new file mode 100644 index 00000000000..dddbdd0a03b --- /dev/null +++ b/apps/studio/components/interfaces/ProjectHome/ComputeUpgradeFloatingNotice.tsx @@ -0,0 +1,109 @@ +import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { AnimatePresence, motion } from 'framer-motion' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { X, Zap } from 'lucide-react' +import Link from 'next/link' +import { useState } from 'react' +import { Button } from 'ui' + +export const ComputeUpgradeFloatingNotice = () => { + const { ref } = useParams() + const { data: project } = useSelectedProjectQuery() + const { data: org } = useSelectedOrganizationQuery() + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: org?.slug }) + + const [isDismissed, setIsDismissed] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.MICRO_UPGRADE_BANNER_DISMISSED(ref ?? ''), + false + ) + const [isHovered, setIsHovered] = useState(false) + + const isProPlan = subscription?.plan.id === 'pro' + const isOnNano = project?.infra_compute_size === 'nano' + + if (isDismissed || !isProPlan || !isOnNano) return null + + return ( + // Outer: positions above badge center + handles float animation + setIsHovered(true)} + onHoverEnd={() => setIsHovered(false)} + > + {/* Inner: expands from center via layout animation */} + + + + + + + + + + + {isHovered ? ( + +
+ Upgrade to Micro + + Double the memory at no extra cost. + +
+ + +
+ ) : ( + + Upgrade available! + + )} +
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/ProjectHome/TopSection.tsx b/apps/studio/components/interfaces/ProjectHome/TopSection.tsx index 659de090a63..ab1c8aada52 100644 --- a/apps/studio/components/interfaces/ProjectHome/TopSection.tsx +++ b/apps/studio/components/interfaces/ProjectHome/TopSection.tsx @@ -14,6 +14,7 @@ import { ReactFlowProvider } from 'reactflow' import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { InstanceConfiguration } from '../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration' +import { ComputeUpgradeFloatingNotice } from './ComputeUpgradeFloatingNotice' export const TopSection = () => { const isOrioleDb = useIsOrioleDb() @@ -73,12 +74,15 @@ export const TopSection = () => { )} - +
+ + +
@@ -91,7 +95,7 @@ export const TopSection = () => {
diff --git a/apps/studio/components/layouts/DefaultLayout.tsx b/apps/studio/components/layouts/DefaultLayout.tsx index a09175a8657..afd93883fe9 100644 --- a/apps/studio/components/layouts/DefaultLayout.tsx +++ b/apps/studio/components/layouts/DefaultLayout.tsx @@ -1,7 +1,12 @@ import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { AppBannerWrapper } from 'components/interfaces/App/AppBannerWrapper' import { Sidebar } from 'components/interfaces/Sidebar' +import { BannerMicroUpgrade } from 'components/ui/BannerStack/Banners/BannerMicroUpgrade' +import { useBannerStack } from 'components/ui/BannerStack/BannerStackProvider' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useCheckLatestDeploy } from 'hooks/use-check-latest-deploy' import { useRouter } from 'next/router' import { PropsWithChildren, useEffect, useState } from 'react' @@ -23,6 +28,39 @@ export interface DefaultLayoutProps { hideMobileMenu?: boolean } +const MicroUpgradeBannerController = () => { + const { ref } = useParams() + const { addBanner, dismissBanner } = useBannerStack() + const { data: org } = useSelectedOrganizationQuery() + const { data: project } = useSelectedProjectQuery() + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: org?.slug }) + + const [isBannerDismissed] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.MICRO_UPGRADE_BANNER_DISMISSED(ref ?? ''), + false + ) + + const isProPlan = subscription?.plan.id === 'pro' + const isOnNano = project?.infra_compute_size === 'nano' + const shouldShow = isProPlan && isOnNano && !isBannerDismissed + // const shouldShow = true // Temporarily hide the banner while we work on the upgrade process + + useEffect(() => { + if (shouldShow) { + addBanner({ + id: 'micro-upgrade-banner', + isDismissed: false, + content: , + priority: 5, + }) + } else { + dismissBanner('micro-upgrade-banner') + } + }, [shouldShow, addBanner, dismissBanner]) + + return null +} + /** * Base layout for all project pages in the dashboard, rendered as the first child on all page files within a project. * @@ -78,6 +116,7 @@ export const DefaultLayout = ({ +
{/* Top Banner */} diff --git a/apps/studio/components/ui/BannerStack/Banners/BannerMicroUpgrade.tsx b/apps/studio/components/ui/BannerStack/Banners/BannerMicroUpgrade.tsx new file mode 100644 index 00000000000..a19bbac4c34 --- /dev/null +++ b/apps/studio/components/ui/BannerStack/Banners/BannerMicroUpgrade.tsx @@ -0,0 +1,35 @@ +import { useParams } from 'common' +import { Zap } from 'lucide-react' +import Link from 'next/link' +import { Button } from 'ui' +import { BannerCard } from '../BannerCard' +import { useBannerStack } from '../BannerStackProvider' + +export const BannerMicroUpgrade = () => { + const { ref } = useParams() + const { dismissBanner } = useBannerStack() + + return ( + dismissBanner('micro-upgrade-banner')}> +
+
+
+ +
+
+
+

Free Micro compute upgrade available

+

+ Your Pro plan includes a free upgrade from Nano to Micro compute — double the memory at + no extra cost. +

+
+
+ +
+
+
+ ) +} diff --git a/apps/studio/tailwind.config.js b/apps/studio/tailwind.config.js index b5fdcf67abc..2e508476a34 100644 --- a/apps/studio/tailwind.config.js +++ b/apps/studio/tailwind.config.js @@ -80,9 +80,14 @@ module.exports = config({ animation: { shimmer: 'shimmer 2s infinite linear', + float: 'float 3s ease-in-out infinite', sway: 'sway 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', }, keyframes: { + float: { + '0%, 100%': { transform: 'translateY(0px)' }, + '50%': { transform: 'translateY(-6px)' }, + }, shimmer: { '0%': { 'background-position': '-1000px 0', diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 2731360655a..b6f63b32ee4 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -100,6 +100,9 @@ export const LOCAL_STORAGE_KEYS = { // Index Advisor notice dismissed INDEX_ADVISOR_NOTICE_DISMISSED: (ref: string) => `index-advisor-notice-dismissed-${ref}`, + // Micro upgrade banner dismissed + MICRO_UPGRADE_BANNER_DISMISSED: (ref: string) => `micro-upgrade-banner-dismissed-${ref}`, + // RLS event trigger banner dismissed RLS_EVENT_TRIGGER_BANNER_DISMISSED: (ref: string) => `rls-event-trigger-banner-dismissed-${ref}`,