Files
supabase/apps/studio/components/layouts/Navigation/LayoutHeader/LayoutHeader.tsx
T
Pamela ChiaandAlaister Young edacf2413d chore(studio): ship connect section, remove getting started and experiment plumbing (#44329)
## Summary

The `connectSection` A/B experiment concluded as a true null (no effect
on activation or any downstream metric after 13 days at 50/50, ~153K
mature orgs). Saxon decided to ship the Connect section as the permanent
experience. This PR removes the Getting Started control variant, the old
Connect modal, all experiment flag gating, and related telemetry types.

## Changes

- Delete `GettingStarted/` directory (5 files: section component, types,
utils, progress hook)
- Delete old `Connect.tsx` dialog modal (replaced by ConnectSheet)
- Remove `connectSection` PostHog flag reads from `Home.tsx` and
`LayoutHeader.tsx`
- Remove `getSectionVisibility()` experiment logic and
`ConnectSectionVariant` type
- Remove `getting-started` from `DEFAULT_SECTION_ORDER`
- Always render `<ConnectSheet />` in header (no more conditional with
old `<Connect />` modal)
- Remove `variant` prop from `ConnectSection` component
- Remove 4 getting-started telemetry event interfaces from
`telemetry-constants.ts`
- Update `mergeSectionOrder` tests to reflect new section order

## Testing

Tested on Vercel preview:
- [x] Project homepage shows Connect section for new projects (< 10 days
old)
- [x] Connect section hidden for mature projects (> 10 days old)
- [x] Header Connect button opens ConnectSheet (not old modal)
- [x] Connect tiles open ConnectSheet with correct tab
- [x] Section drag-and-drop still works without getting-started in the
order
- [x] Existing users with `getting-started` in localStorage order don't
break (mergeSectionOrder strips it)

## Linear

- fixes GROWTH-730

---------

Co-authored-by: Alaister Young <alaister@users.noreply.github.com>
2026-03-30 20:51:09 +08:00

293 lines
11 KiB
TypeScript

import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import {
useIsBranching2Enabled,
useIsFloatingMobileToolbarEnabled,
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ConnectButton } from 'components/interfaces/ConnectButton/ConnectButton'
import { ConnectSheet } from 'components/interfaces/ConnectSheet/ConnectSheet'
import { LocalDropdown } from 'components/interfaces/LocalDropdown'
import { UserDropdown } from 'components/interfaces/UserDropdown'
import { AdvisorButton } from 'components/layouts/AppLayout/AdvisorButton'
import { AssistantButton } from 'components/layouts/AppLayout/AssistantButton'
import { BranchDropdown } from 'components/layouts/AppLayout/BranchDropdown'
import { InlineEditorButton } from 'components/layouts/AppLayout/InlineEditorButton'
import { OrganizationDropdown } from 'components/layouts/AppLayout/OrganizationDropdown'
import { ProjectDropdown } from 'components/layouts/AppLayout/ProjectDropdown'
import { HelpButton } from 'components/ui/HelpPanel/HelpButton'
import { getResourcesExceededLimitsOrg } from 'components/ui/OveragesBanner/OveragesBanner.utils'
import { useOrgUsageQuery } from 'data/usage/org-usage-query'
import dayjs from 'dayjs'
import { DevToolbarTrigger } from 'dev-tools'
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 { IS_PLATFORM } from 'lib/constants'
import { ChevronLeft } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ReactNode, useMemo } from 'react'
import { Badge, cn } from 'ui'
import { CommandMenuTriggerInput } from 'ui-patterns'
import { BreadcrumbsView } from './BreadcrumbsView'
import { FeedbackDropdown } from './FeedbackDropdown/FeedbackDropdown'
import { HomeIcon } from './HomeIcon'
import { LocalVersionPopover } from './LocalVersionPopover'
import { MergeRequestButton } from './MergeRequestButton'
const LayoutHeaderDivider = ({ className, ...props }: React.HTMLProps<HTMLSpanElement>) => (
<span className={cn('text-border-stronger pr-2', className)} {...props}>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
aria-hidden={true}
>
<path d="M16 3.549L7.12 20.600" />
</svg>
</span>
)
interface LayoutHeaderProps {
customHeaderComponents?: ReactNode
breadcrumbs?: unknown[]
headerTitle?: string
backToDashboardURL?: string
}
export const LayoutHeader = ({
customHeaderComponents,
breadcrumbs = [],
headerTitle,
backToDashboardURL,
}: LayoutHeaderProps) => {
const router = useRouter()
const { ref: projectRef, slug } = useParams()
const { data: selectedProject } = useSelectedProjectQuery()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const gitlessBranching = useIsBranching2Enabled()
const showFloatingMobileToolbar = useIsFloatingMobileToolbarEnabled()
const [commandMenuEnabled] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.HOTKEY_COMMAND_MENU, true)
const isAccountPage = router.pathname.startsWith('/account')
// We only want to query the org usage and check for possible over-ages for plans without usage billing enabled (free or pro with spend cap)
const { data: orgUsage } = useOrgUsageQuery(
{ orgSlug: selectedOrganization?.slug },
{ enabled: selectedOrganization?.usage_billing_enabled === false }
)
const exceedingLimits = useMemo(() => {
if (orgUsage) {
return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
} else {
return false
}
}, [orgUsage])
const isNewProject =
selectedProject?.inserted_at !== undefined &&
dayjs(selectedProject.inserted_at).isAfter(dayjs().subtract(5, 'day'))
const connectButtonType = isNewProject ? 'primary' : 'default'
// show org selection if we are on a project page or on a explicit org route
const showOrgSelection = slug || (selectedOrganization && projectRef)
return (
<>
<header
className={cn(
'flex h-11 md:h-12 items-center flex-shrink-0 border-b',
showFloatingMobileToolbar && 'hidden md:flex'
)}
>
{backToDashboardURL && isAccountPage && (
<div className="flex items-center justify-center border-r flex-0 md:hidden h-full aspect-square">
<Link
href={backToDashboardURL}
className="flex items-center justify-center border-none !bg-transparent rounded-md min-w-[30px] w-[30px] h-[30px] text-foreground-lighter hover:text-foreground transition-colors"
>
<ChevronLeft strokeWidth={1.5} size={16} />
</Link>
</div>
)}
<div
className={cn(
'flex items-center justify-between h-full pr-3 flex-1 overflow-x-auto gap-x-8 pl-4'
)}
>
<div className="flex md:hidden items-center text-sm not-sr-only">
<AnimatePresence>
{headerTitle && (
<motion.div
className="flex items-center -ml-1"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.15,
ease: 'easeOut',
}}
>
<LayoutHeaderDivider />
<span className="text-foreground">{headerTitle}</span>
</motion.div>
)}
</AnimatePresence>
</div>
<div className="hidden md:flex items-center text-sm">
<HomeIcon />
<div className="flex items-center md:pl-2">
{showOrgSelection && IS_PLATFORM ? (
<>
<LayoutHeaderDivider className="hidden md:block" />
<OrganizationDropdown />
</>
) : null}
<AnimatePresence>
{projectRef && (
<motion.div
className="flex items-center"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.15,
ease: 'easeOut',
}}
>
{IS_PLATFORM && <LayoutHeaderDivider />}
<ProjectDropdown />
{exceedingLimits && (
<div className="ml-2">
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
<Badge variant="destructive">Exceeding usage limits</Badge>
</Link>
</div>
)}
{selectedProject && IS_PLATFORM && (
<>
<LayoutHeaderDivider />
<BranchDropdown />
</>
)}
</motion.div>
)}
</AnimatePresence>
</div>
<AnimatePresence>
{headerTitle && (
<motion.div
className="flex items-center"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.15,
ease: 'easeOut',
}}
>
<LayoutHeaderDivider />
<span className="text-foreground">{headerTitle}</span>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{projectRef && (
<motion.div
className="ml-3 items-center gap-x-2 flex"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.15,
ease: 'easeOut',
}}
>
{IS_PLATFORM && gitlessBranching && <MergeRequestButton />}
<ConnectButton buttonType={connectButtonType} />
</motion.div>
)}
</AnimatePresence>
<BreadcrumbsView defaultValue={breadcrumbs} />
</div>
<div className="flex items-center gap-x-2">
{customHeaderComponents && customHeaderComponents}
{IS_PLATFORM ? (
<>
<DevToolbarTrigger />
<FeedbackDropdown />
<div className="flex items-center gap-1 md:gap-2">
<CommandMenuTriggerInput
showShortcut={commandMenuEnabled}
placeholder="Search..."
className={cn(
'hidden md:flex md:min-w-32 xl:min-w-32 rounded-full bg-transparent',
'[&_.command-shortcut>div]:border-none',
'[&_.command-shortcut>div]:pr-2',
'[&_.command-shortcut>div]:bg-transparent',
'[&_.command-shortcut>div]:text-foreground-lighter'
)}
/>
<HelpButton />
<AdvisorButton projectRef={projectRef} />
<AnimatePresence initial={false}>
{!!projectRef && (
<>
<InlineEditorButton />
<AssistantButton />
</>
)}
</AnimatePresence>
</div>
<UserDropdown triggerClassName="hidden md:flex" />
</>
) : (
<>
<LocalVersionPopover />
<div className="flex items-center gap-1 md:gap-2">
<CommandMenuTriggerInput
placeholder="Search..."
className="hidden md:flex md:min-w-32 xl:min-w-32 rounded-full bg-transparent
[&_.command-shortcut>div]:border-none
[&_.command-shortcut>div]:pr-2
[&_.command-shortcut>div]:bg-transparent
[&_.command-shortcut>div]:text-foreground-lighter
"
/>
<HelpButton />
<AdvisorButton projectRef={projectRef} />
<AnimatePresence initial={false}>
{!!projectRef && (
<>
<InlineEditorButton />
<AssistantButton />
</>
)}
</AnimatePresence>
</div>
<LocalDropdown triggerClassName="hidden md:flex" />
</>
)}
</div>
</div>
</header>
<ConnectSheet />
</>
)
}