Files
supabase/apps/studio/components/interfaces/ProjectHome/ConnectSection.tsx
T
Pamela ChiaandAli Waseem 578a73f966 feat(studio): connectSection experiment — replace Getting Started with Connect section (#43629)
## Summary

Re-ports PR #43119 against the current `ProjectHome` codebase (the
original PR targeted `HomeNew/` which was removed during the `homeNew`
graduation in #43437). Also unifies the `connectSheet` and
`connectSection` feature flags into a single `connectSection` flag — the
`connectSheet` flag is removed entirely, so both the ConnectSheet
(header) and ConnectSection (homepage row) are controlled by one
experiment.

- Adds `connectSection` PostHog experiment flag that controls two
things:
1. Swaps the Getting Started section for a Connect section on the
project homepage for new projects (< 10 days old)
2. Swaps the legacy Connect dialog for the new ConnectSheet panel in the
header
- **Control** (`getting-started`): existing Getting Started section +
legacy Connect dialog in header
- **Treatment** (`connect`): new 4-tile Connect section + ConnectSheet
in header
- `undefined` (loading): neither section renders, avoiding flash
- Tiles filtered by the same `useIsFeatureEnabled` flags as ConnectSheet
(`show_app_frameworks`, `show_mobile_frameworks`, `show_orms`)
- Connect tile clicks tracked via `home_connect_action_clicked`; section
render tracked via `home_connect_section_exposed`; sheet opens tracked
via `connect_sheet_opened` with source attribution (`header_button` or
`connect_section`)

## Changes

- `packages/common/telemetry-constants.ts` — new
`home_connect_section_exposed`, `home_connect_action_clicked`, and
`connect_sheet_opened` event types
- `ConnectSheet/ConnectSheet.tsx` — read `connectTab` query param and
sync to active mode on open; `handleModeChange` keeps param in sync on
tab switch; fire `connect_sheet_opened` event with source attribution on
open
- `ConnectButton/ConnectButton.tsx` — set `connectSource=header_button`
query param on click
- `ProjectHome/ConnectSection.tsx` — new component (4-tile connect
card); set `connectSource=connect_section` on tile click
- `ProjectHome/Home.tsx` — experiment flag wiring
- `LayoutHeader/LayoutHeader.tsx` — read `connectSection` flag instead
of `connectSheet` to toggle ConnectSheet vs legacy Connect dialog

## Test plan

To test on the Vercel preview, set `connectSection=connect` to 100% in
PostHog (or override via cookie `ph_override_connectSection=connect`).
No separate `connectSheet` flag is needed — `connectSection` controls
both features.
- [x] Treatment (`connect` variant) — "Get connected" section renders on
new project, Getting Started hidden
- [x] Control (`getting-started`) — Getting Started renders, Connect
section hidden
- [x] Mature project (> 10 days) — neither section regardless of flag
- [x] Clicking each tile opens ConnectSheet on the correct tab
(Framework / Direct / ORM / MCP)
- [x] Switching tabs inside sheet updates `connectTab` URL param
- [x] Closing sheet clears `connectTab` param
- [x] Direct URL deep-link (`?showConnect=true&connectTab=orm`) opens
sheet on correct tab
- [x] Dark mode — background gradient renders correctly
- [x] Light mode — background switches to light gradient
- [x] Responsive layout — 4 cols (xl), 2×2 (md), stacked (mobile)
- [x] Telemetry — `home_connect_section_exposed` fires once on load;
`home_connect_action_clicked` fires with correct `mode`
- [x] Treatment — header Connect button opens ConnectSheet (not legacy
Connect dialog)
- [x] Control — header Connect button opens legacy Connect dialog (not
ConnectSheet)
- [x] Telemetry — clicking a ConnectSection tile fires
`connect_sheet_opened` with `source: 'connect_section'`
- [x] Telemetry — clicking header Connect button fires
`connect_sheet_opened` with `source: 'header_button'`

---------

Co-authored-by: Ali Waseem <waseema393@gmail.com>
2026-03-13 11:50:07 +09:00

98 lines
3.8 KiB
TypeScript

import { IS_PLATFORM } from 'common'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { BASE_PATH, PROJECT_STATUS } from 'lib/constants'
import { useTrack } from 'lib/telemetry/track'
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
import { useEffect, useRef } from 'react'
import { Card, CardContent, cn } from 'ui'
import type { ConnectMode } from '../ConnectSheet/Connect.types'
import { useAvailableConnectModes } from '../ConnectSheet/useAvailableConnectModes'
import { useAppStateSnapshot } from '@/state/app-state'
import { CONNECT_ACTIONS, type ConnectSectionVariant } from './ConnectSection.config'
interface ConnectSectionProps {
variant: ConnectSectionVariant
}
export const ConnectSection = ({ variant }: ConnectSectionProps) => {
const { data: selectedProject } = useSelectedProjectQuery()
const { setConnectSheetSource } = useAppStateSnapshot()
const track = useTrack()
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const [, setConnectTab] = useQueryState('connectTab', parseAsString)
const isActiveHealthy = selectedProject?.status === PROJECT_STATUS.ACTIVE_HEALTHY
const availableModeIds = useAvailableConnectModes()
const availableActions = CONNECT_ACTIONS.filter((action) =>
availableModeIds.includes(action.mode)
)
const hasTrackedExposure = useRef(false)
useEffect(() => {
if (!IS_PLATFORM) return
if (hasTrackedExposure.current) return
hasTrackedExposure.current = true
track('home_connect_section_exposed', { variant })
}, [variant, track])
const handleConnectClick = (mode: ConnectMode) => {
track('home_connect_action_clicked', { mode })
setConnectTab(mode)
setConnectSheetSource('connect_section')
setShowConnect(true)
}
return (
<section className="w-full">
<div className="mb-6">
<h3 className="heading-section">Get connected</h3>
</div>
<Card className="bg-background/25 border-dashed relative overflow-hidden">
<div className="absolute -inset-16 z-0 opacity-50">
<img
src={`${BASE_PATH}/img/reports/bg-grafana-dark.svg`}
alt=""
className="w-full h-full object-cover object-right hidden dark:block"
/>
<img
src={`${BASE_PATH}/img/reports/bg-grafana-light.svg`}
alt=""
className="w-full h-full object-cover object-right dark:hidden"
/>
<div className="absolute inset-0 bg-gradient-to-r from-background-alternative to-transparent" />
</div>
<CardContent className="relative z-10 p-0">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 divide-y md:divide-y-0 md:divide-x border-muted">
{availableActions.map((action) => (
<button
key={action.mode}
type="button"
disabled={!isActiveHealthy}
onClick={() => handleConnectClick(action.mode)}
className={cn(
'group flex flex-col items-center justify-center gap-3 p-6 text-center transition-colors min-h-32',
'hover:bg-surface-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand',
!isActiveHealthy && 'cursor-not-allowed opacity-50'
)}
>
<span className="text-foreground-light group-hover:text-foreground">
{action.icon}
</span>
<div className="flex flex-col gap-1">
<p className="text-sm">{action.heading}</p>
<p className="text-sm text-foreground-lighter">{action.subheading}</p>
</div>
</button>
))}
</div>
</CardContent>
</Card>
</section>
)
}