Files
supabase/apps/studio/components/interfaces/ProjectHome/Home.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

168 lines
6.7 KiB
TypeScript

import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
import { IS_PLATFORM, useFlag, useParams } from 'common'
import { ProjectUsageSection as ProjectUsageSectionV1 } from 'components/interfaces/Home/ProjectUsageSection'
import { SortableSection } from 'components/interfaces/ProjectHome/SortableSection'
import { TopSection } from 'components/interfaces/ProjectHome/TopSection'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import dayjs from 'dayjs'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { usePHFlag } from 'hooks/ui/useFlag'
import { PROJECT_STATUS } from 'lib/constants'
import { useTrack } from 'lib/telemetry/track'
import { useEffect, useRef } from 'react'
import { useAppStateSnapshot } from 'state/app-state'
import { cn } from 'ui'
import { AdvisorSection } from './AdvisorSection'
import { ConnectSection } from './ConnectSection'
import type { ConnectSectionVariant } from './ConnectSection.config'
import { CustomReportSection } from './CustomReportSection'
import { type GettingStartedState } from './GettingStarted/GettingStarted.types'
import { GettingStartedSection } from './GettingStarted/GettingStartedSection'
import { DEFAULT_SECTION_ORDER, mergeSectionOrder, getSectionVisibility } from './Home.utils'
import { ProjectUsageSection as ProjectUsageSectionV2 } from './ProjectUsageSection'
export const ProjectHome = () => {
const { enableBranching } = useParams()
const snap = useAppStateSnapshot()
const { data: project } = useSelectedProjectQuery()
const track = useTrack()
const showHomepageUsageV2 = useFlag('newHomepageUsageV2')
const connectSectionVariant = usePHFlag<ConnectSectionVariant | false>('connectSection')
const isMatureProject = dayjs(project?.inserted_at).isBefore(dayjs().subtract(10, 'day'))
const hasShownEnableBranchingModalRef = useRef(false)
const isPaused = project?.status === PROJECT_STATUS.INACTIVE
const isComingUp = project?.status === PROJECT_STATUS.COMING_UP
const [sectionOrder, setSectionOrder] = useLocalStorage<string[]>(
`home-section-order-${project?.ref || 'default'}`,
DEFAULT_SECTION_ORDER
)
const [gettingStartedState, setGettingStartedState] = useLocalStorage<GettingStartedState>(
`home-getting-started-${project?.ref || 'default'}`,
'empty'
)
const UsageSection = showHomepageUsageV2 ? ProjectUsageSectionV2 : ProjectUsageSectionV1
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }))
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
setSectionOrder((items) => {
const oldIndex = items.indexOf(String(active.id))
const newIndex = items.indexOf(String(over.id))
if (oldIndex === -1 || newIndex === -1) return items
track('home_section_rows_moved', {
section_moved: String(active.id),
old_position: oldIndex,
new_position: newIndex,
})
return arrayMove(items, oldIndex, newIndex)
})
}
useEffect(() => {
if (enableBranching && !hasShownEnableBranchingModalRef.current) {
hasShownEnableBranchingModalRef.current = true
snap.setShowCreateBranchModal(true)
}
}, [enableBranching, snap])
useEffect(() => {
setSectionOrder(mergeSectionOrder)
}, [setSectionOrder])
const { showConnectSection, showGettingStarted } = getSectionVisibility({
connectSectionVariant,
isMatureProject,
hasProject: !!project,
gettingStartedState,
})
return (
<div className="w-full h-full">
<ScaffoldContainer size="large" className={cn(isPaused && 'h-full')}>
<ScaffoldSection
isFullWidth
className={cn(isPaused ? 'h-full flex justify-center !p-0' : 'pt-16 pb-0')}
>
<TopSection />
</ScaffoldSection>
</ScaffoldContainer>
{!isPaused && (
<ScaffoldContainer size="large">
<ScaffoldSection isFullWidth className="gap-16 pb-32">
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<SortableContext
items={sectionOrder.filter((id) => {
if (id === 'connect') return showConnectSection
if (id === 'getting-started') return showGettingStarted
return true
})}
strategy={verticalListSortingStrategy}
>
{sectionOrder.map((id) => {
if (IS_PLATFORM && id === 'usage') {
return (
<div key={id} className={cn(isComingUp && 'opacity-60 pointer-events-none')}>
<SortableSection id={id}>
<UsageSection />
</SortableSection>
</div>
)
}
if (id === 'connect' && showConnectSection) {
return (
<SortableSection key={id} id={id}>
<ConnectSection variant={connectSectionVariant as ConnectSectionVariant} />
</SortableSection>
)
}
if (id === 'getting-started' && showGettingStarted) {
return (
<SortableSection key={id} id={id}>
<GettingStartedSection
value={gettingStartedState}
onChange={setGettingStartedState}
/>
</SortableSection>
)
}
if (id === 'advisor') {
return (
<div key={id} className={cn(isComingUp && 'opacity-60 pointer-events-none')}>
<SortableSection id={id}>
<AdvisorSection showEmptyState={isComingUp} />
</SortableSection>
</div>
)
}
if (id === 'custom-report') {
return (
<div key={id} className={cn(isComingUp && 'opacity-60 pointer-events-none')}>
<SortableSection id={id}>
<CustomReportSection />
</SortableSection>
</div>
)
}
})}
</SortableContext>
</DndContext>
</ScaffoldSection>
</ScaffoldContainer>
)}
</div>
)
}