Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
<motion.div
className="absolute bottom-full left-1/2 mb-2 z-10"
style={{ x: '-50%' }}
animate={isHovered ? { y: 0 } : { y: [0, -6, 0] }}
transition={
isHovered
? { duration: 0.2, ease: 'easeOut' }
: { duration: 3, repeat: Infinity, ease: 'easeInOut' }
}
onHoverStart={() => setIsHovered(true)}
onHoverEnd={() => setIsHovered(false)}
>
{/* Inner: expands from center via layout animation */}
<motion.div
layout
className={`relative flex items-center gap-2 rounded-full bg-background backdrop-blur-sm border border-brand-500 shadow-md text-brand-600 ${isHovered ? 'pl-2 pr-3 py-1.5' : 'pl-1 pr-2.5 py-1'} cursor-default overflow-hidden whitespace-nowrap`}
transition={{ duration: 0.2, ease: 'easeInOut' }}
>
<span className="absolute inset-0 bg-brand bg-opacity-10 w-full h-full" />

<AnimatePresence mode="wait" initial={false}>
<motion.div
key={isHovered ? 'icon-expanded' : 'icon-collapsed'}
layout
className={`flex-shrink-0 flex items-center justify-center rounded-full bg-brand-300 text-brand ${isHovered ? 'h-6 w-6' : 'h-4 w-4'}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, ease: 'easeInOut' }}
>
<Zap size={isHovered ? 12 : 10} />
</motion.div>
</AnimatePresence>

<AnimatePresence mode="wait" initial={false}>
{isHovered ? (
<motion.div
key="expanded"
className="flex items-center gap-3"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.3, ease: 'easeInOut' } }}
transition={{ duration: 0.25, delay: 0.18 }}
>
<div className="flex flex-col min-w-0">
<span className="text-xs font-medium leading-tight">Upgrade to Micro</span>
<span className="text-[11px] text-foreground leading-tight">
Double the memory at no extra cost.
</span>
</div>
<Button asChild type="primary" size="tiny" className="flex-shrink-0 !px-2 !py-1">
<Link href={`/project/${ref}/settings/compute-and-disk`}>Upgrade</Link>
</Button>
<button
type="button"
onClick={() => setIsDismissed(true)}
className="flex-shrink-0 text-brand/50 hover:text-foreground transition-colors h-4 w-4"
aria-label="Dismiss"
>
<X size={12} />
</button>
</motion.div>
) : (
<motion.span
key="collapsed"
className="text-xs font-medium"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.2, ease: 'easeInOut' } }}
transition={{ duration: 0.2, delay: 0.12 }}
>
Upgrade available!
</motion.span>
)}
</AnimatePresence>
</motion.div>
</motion.div>
)
}
18 changes: 11 additions & 7 deletions apps/studio/components/interfaces/ProjectHome/TopSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -73,12 +74,15 @@ export const TopSection = () => {
</TooltipContent>
</Tooltip>
)}
<ComputeBadgeWrapper
projectRef={project?.ref}
slug={organization?.slug}
cloudProvider={project?.cloud_provider}
computeSize={project?.infra_compute_size}
/>
<div className="relative">
<ComputeUpgradeFloatingNotice />
<ComputeBadgeWrapper
projectRef={project?.ref}
slug={organization?.slug}
cloudProvider={project?.cloud_provider}
computeSize={project?.infra_compute_size}
/>
</div>
</div>
</div>
<ProjectConnectionHoverCard projectRef={project?.ref} />
Expand All @@ -91,7 +95,7 @@ export const TopSection = () => {
<div>
<div
className={cn(
'w-full h-[400px] md:h-[500px] border border-muted rounded-md overflow-hidden flex flex-col relative'
'w-full h-[400px] md:h-[500px] border border-muted rounded-md overflow-hidden flex flex-col'
)}
>
<ReactFlowProvider>
Expand Down
39 changes: 39 additions & 0 deletions apps/studio/components/layouts/DefaultLayout.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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: <BannerMicroUpgrade />,
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.
*
Expand Down Expand Up @@ -78,6 +116,7 @@ export const DefaultLayout = ({
<ProjectContextProvider projectRef={ref}>
<MobileSheetProvider>
<BannerStackProvider>
<MicroUpgradeBannerController />
<div className="flex flex-col h-screen w-screen">
{/* Top Banner */}
<AppBannerWrapper />
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<BannerCard onDismiss={() => dismissBanner('micro-upgrade-banner')}>
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-2 items-start">
<div className="p-2 rounded-lg bg-brand-300 text-brand">
<Zap size={16} />
</div>
</div>
<div className="flex flex-col gap-y-1 mb-2">
<p className="text-sm font-medium">Free Micro compute upgrade available</p>
<p className="text-xs text-foreground-lighter text-balance">
Your Pro plan includes a free upgrade from Nano to Micro compute — double the memory at
no extra cost.
</p>
</div>
<div className="flex gap-2">
<Button asChild type="primary" size="tiny">
<Link href={`/project/${ref}/settings/compute-and-disk`}>Upgrade now</Link>
</Button>
</div>
</div>
</BannerCard>
)
}
5 changes: 5 additions & 0 deletions apps/studio/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions packages/common/constants/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,

Expand Down
Loading