Home.tsx286 lines · main
| 1 | import { useParams } from 'common' |
| 2 | import dayjs from 'dayjs' |
| 3 | import Link from 'next/link' |
| 4 | import { useEffect, useRef } from 'react' |
| 5 | import { |
| 6 | Badge, |
| 7 | cn, |
| 8 | Tabs_Shadcn_, |
| 9 | TabsContent_Shadcn_, |
| 10 | TabsList_Shadcn_, |
| 11 | TabsTrigger_Shadcn_, |
| 12 | Tooltip, |
| 13 | TooltipContent, |
| 14 | TooltipTrigger, |
| 15 | } from 'ui' |
| 16 | import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' |
| 17 | |
| 18 | import { AdvisorWidget } from '@/components/interfaces/Home/AdvisorWidget' |
| 19 | import { ClientLibrary } from '@/components/interfaces/Home/ClientLibrary' |
| 20 | import { ExampleProject } from '@/components/interfaces/Home/ExampleProject' |
| 21 | import { EXAMPLE_PROJECTS } from '@/components/interfaces/Home/Home.constants' |
| 22 | import { NewProjectPanel } from '@/components/interfaces/Home/NewProjectPanel/NewProjectPanel' |
| 23 | import { ProjectUsageSection } from '@/components/interfaces/Home/ProjectUsageSection' |
| 24 | import { ServiceStatus } from '@/components/interfaces/Home/ServiceStatus' |
| 25 | import { ProjectPausedState } from '@/components/layouts/ProjectLayout/PausedState/ProjectPausedState' |
| 26 | import { ComputeBadgeWrapper } from '@/components/ui/ComputeBadgeWrapper' |
| 27 | import { InlineLink } from '@/components/ui/InlineLink' |
| 28 | import { ProjectUpgradeFailedBanner } from '@/components/ui/ProjectUpgradeFailedBanner' |
| 29 | import { useBranchesQuery } from '@/data/branches/branches-query' |
| 30 | import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query' |
| 31 | import { useProjectDetailQuery } from '@/data/projects/project-detail-query' |
| 32 | import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' |
| 33 | import { useTablesQuery } from '@/data/tables/tables-query' |
| 34 | import { useCustomContent } from '@/hooks/custom-content/useCustomContent' |
| 35 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 36 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 37 | import { useIsOrioleDb, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 38 | import { DOCS_URL, IS_PLATFORM, PROJECT_STATUS } from '@/lib/constants' |
| 39 | import { useAppStateSnapshot } from '@/state/app-state' |
| 40 | |
| 41 | export const Home = () => { |
| 42 | const { data: project } = useSelectedProjectQuery() |
| 43 | const { data: organization } = useSelectedOrganizationQuery() |
| 44 | const { data: parentProject } = useProjectDetailQuery({ ref: project?.parent_project_ref }) |
| 45 | const isOrioleDb = useIsOrioleDb() |
| 46 | const snap = useAppStateSnapshot() |
| 47 | const { ref, enableBranching } = useParams() |
| 48 | |
| 49 | const { projectHomepageExampleProjects, projectHomepageClientLibraries: clientLibraries } = |
| 50 | useCustomContent(['project_homepage:example_projects', 'project_homepage:client_libraries']) |
| 51 | |
| 52 | const { |
| 53 | projectHomepageShowInstanceSize: showInstanceSize, |
| 54 | projectHomepageShowExamples: showExamples, |
| 55 | } = useIsFeatureEnabled(['project_homepage:show_instance_size', 'project_homepage:show_examples']) |
| 56 | |
| 57 | const hasShownEnableBranchingModalRef = useRef(false) |
| 58 | const isPaused = project?.status === PROJECT_STATUS.INACTIVE |
| 59 | const isNewProject = dayjs(project?.inserted_at).isAfter(dayjs().subtract(2, 'day')) |
| 60 | |
| 61 | useEffect(() => { |
| 62 | if (enableBranching && !hasShownEnableBranchingModalRef.current) { |
| 63 | hasShownEnableBranchingModalRef.current = true |
| 64 | snap.setShowCreateBranchModal(true) |
| 65 | } |
| 66 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 67 | }, [enableBranching]) |
| 68 | |
| 69 | const { data: tablesData, isPending: isLoadingTables } = useTablesQuery({ |
| 70 | projectRef: project?.ref, |
| 71 | connectionString: project?.connectionString, |
| 72 | schema: 'public', |
| 73 | }) |
| 74 | const { data: functionsData, isPending: isLoadingFunctions } = useEdgeFunctionsQuery({ |
| 75 | projectRef: project?.ref, |
| 76 | }) |
| 77 | const { data: replicasData, isPending: isLoadingReplicas } = useReadReplicasQuery({ |
| 78 | projectRef: project?.ref, |
| 79 | }) |
| 80 | |
| 81 | const { data: branches } = useBranchesQuery({ |
| 82 | projectRef: project?.parent_project_ref ?? project?.ref, |
| 83 | }) |
| 84 | |
| 85 | const mainBranch = branches?.find((branch) => branch.is_default) |
| 86 | const currentBranch = branches?.find((branch) => branch.project_ref === project?.ref) |
| 87 | const isMainBranch = currentBranch?.name === mainBranch?.name |
| 88 | let projectName = 'Welcome to your project' |
| 89 | |
| 90 | if (currentBranch && !isMainBranch) { |
| 91 | projectName = currentBranch?.name |
| 92 | } else if (project?.name) { |
| 93 | projectName = project?.name |
| 94 | } |
| 95 | |
| 96 | const tablesCount = Math.max(0, tablesData?.length ?? 0) |
| 97 | const functionsCount = Math.max(0, functionsData?.length ?? 0) |
| 98 | // [Joshen] JFYI minus 1 as the replicas endpoint returns the primary DB minimally |
| 99 | const replicasCount = Math.max(0, (replicasData?.length ?? 1) - 1) |
| 100 | |
| 101 | if (isPaused) { |
| 102 | return ( |
| 103 | <div className="w-full h-full flex items-center justify-center px-4"> |
| 104 | <ProjectPausedState /> |
| 105 | </div> |
| 106 | ) |
| 107 | } |
| 108 | |
| 109 | return ( |
| 110 | <div className="w-full px-4"> |
| 111 | <div className={cn('py-16 ', !isPaused && 'border-b border-muted ')}> |
| 112 | <div className="mx-auto max-w-7xl flex flex-col gap-y-4"> |
| 113 | <div className="flex flex-col md:flex-row md:items-center gap-6 justify-between w-full"> |
| 114 | <div className="flex flex-col md:flex-row md:items-end gap-3 w-full"> |
| 115 | <div> |
| 116 | {!isMainBranch && ( |
| 117 | <Link |
| 118 | href={`/project/${parentProject?.ref}`} |
| 119 | className="text-sm text-foreground-light" |
| 120 | > |
| 121 | {parentProject?.name} |
| 122 | </Link> |
| 123 | )} |
| 124 | <h1 className="text-3xl">{projectName}</h1> |
| 125 | </div> |
| 126 | <div className="flex items-center gap-x-2 mb-1"> |
| 127 | {isOrioleDb && ( |
| 128 | <Tooltip> |
| 129 | <TooltipTrigger asChild> |
| 130 | <Badge variant="warning">OrioleDB</Badge> |
| 131 | </TooltipTrigger> |
| 132 | <TooltipContent side="bottom" align="start" className="max-w-80 text-center"> |
| 133 | This project is using Postgres with OrioleDB which is currently in preview and |
| 134 | not suitable for production workloads. View our{' '} |
| 135 | <InlineLink href={`${DOCS_URL}/guides/database/orioledb`}> |
| 136 | documentation |
| 137 | </InlineLink>{' '} |
| 138 | for all limitations. |
| 139 | </TooltipContent> |
| 140 | </Tooltip> |
| 141 | )} |
| 142 | {showInstanceSize && ( |
| 143 | <ComputeBadgeWrapper |
| 144 | projectRef={project?.ref} |
| 145 | slug={organization?.slug} |
| 146 | cloudProvider={project?.cloud_provider} |
| 147 | computeSize={project?.infra_compute_size} |
| 148 | /> |
| 149 | )} |
| 150 | </div> |
| 151 | </div> |
| 152 | <div className="flex items-center"> |
| 153 | {project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && ( |
| 154 | <div className="flex items-center gap-x-6"> |
| 155 | <div className="flex flex-col gap-y-1"> |
| 156 | <Link |
| 157 | href={`/project/${ref}/editor`} |
| 158 | className="transition text-foreground-light hover:text-foreground text-sm" |
| 159 | > |
| 160 | Tables |
| 161 | </Link> |
| 162 | |
| 163 | {isLoadingTables ? ( |
| 164 | <ShimmeringLoader className="w-full h-[32px] w-6 p-0" /> |
| 165 | ) : ( |
| 166 | <p className="text-2xl tabular-nums">{tablesCount}</p> |
| 167 | )} |
| 168 | </div> |
| 169 | |
| 170 | <div className="flex flex-col gap-y-1"> |
| 171 | <Link |
| 172 | href={`/project/${ref}/functions`} |
| 173 | className="transition text-foreground-light hover:text-foreground text-sm" |
| 174 | > |
| 175 | Functions |
| 176 | </Link> |
| 177 | {isLoadingFunctions ? ( |
| 178 | <ShimmeringLoader className="w-full h-[32px] w-6 p-0" /> |
| 179 | ) : ( |
| 180 | <p className="text-2xl tabular-nums">{functionsCount}</p> |
| 181 | )} |
| 182 | </div> |
| 183 | |
| 184 | {IS_PLATFORM && ( |
| 185 | <div className="flex flex-col gap-y-1"> |
| 186 | <Link |
| 187 | href={`/project/${ref}/settings/infrastructure`} |
| 188 | className="transition text-foreground-light hover:text-foreground text-sm" |
| 189 | > |
| 190 | Replicas |
| 191 | </Link> |
| 192 | {isLoadingReplicas ? ( |
| 193 | <ShimmeringLoader className="w-full h-[32px] w-6 p-0" /> |
| 194 | ) : ( |
| 195 | <p className="text-2xl tabular-nums">{replicasCount}</p> |
| 196 | )} |
| 197 | </div> |
| 198 | )} |
| 199 | </div> |
| 200 | )} |
| 201 | {IS_PLATFORM && project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && ( |
| 202 | <div className="ml-6 border-l flex items-center w-[145px] justify-end"> |
| 203 | <ServiceStatus /> |
| 204 | </div> |
| 205 | )} |
| 206 | </div> |
| 207 | </div> |
| 208 | <ProjectUpgradeFailedBanner /> |
| 209 | </div> |
| 210 | </div> |
| 211 | |
| 212 | <> |
| 213 | <div className="py-16 border-b border-muted"> |
| 214 | <div className="mx-auto max-w-7xl space-y-16 @container"> |
| 215 | {IS_PLATFORM && project?.status !== PROJECT_STATUS.INACTIVE && ( |
| 216 | <>{isNewProject ? <NewProjectPanel /> : <ProjectUsageSection />}</> |
| 217 | )} |
| 218 | {!isNewProject && project?.status !== PROJECT_STATUS.INACTIVE && <AdvisorWidget />} |
| 219 | </div> |
| 220 | </div> |
| 221 | |
| 222 | <div className="bg-surface-100/5 py-16"> |
| 223 | <div className="mx-auto max-w-7xl space-y-16 @container"> |
| 224 | {project?.status !== PROJECT_STATUS.INACTIVE && ( |
| 225 | <> |
| 226 | <div className="space-y-8"> |
| 227 | <h2 className="text-lg">Client libraries</h2> |
| 228 | <div className="grid grid-cols-2 gap-x-8 gap-y-8 md:gap-12 mb-12 md:grid-cols-3"> |
| 229 | {clientLibraries!.map((library) => ( |
| 230 | // [Alaister]: Looks like the useCustomContent has wonky types. I'll look at a fix later. |
| 231 | <ClientLibrary key={(library as any).language} {...(library as any)} /> |
| 232 | ))} |
| 233 | </div> |
| 234 | </div> |
| 235 | {showExamples && ( |
| 236 | <div className="flex flex-col gap-y-8"> |
| 237 | <h4 className="text-lg">Example projects</h4> |
| 238 | {!!projectHomepageExampleProjects ? ( |
| 239 | <div className="grid gap-2 md:gap-8 md:grid-cols-2 lg:grid-cols-3"> |
| 240 | {/* [Alaister]: Looks like the useCustomContent has wonky types. I'll look at a fix later. */} |
| 241 | {(projectHomepageExampleProjects as any) |
| 242 | .sort((a: any, b: any) => a.title.localeCompare(b.title)) |
| 243 | .map((project: any) => ( |
| 244 | <ExampleProject key={project.url} {...project} /> |
| 245 | ))} |
| 246 | </div> |
| 247 | ) : ( |
| 248 | <div className="flex justify-center"> |
| 249 | <Tabs_Shadcn_ defaultValue="app" className="w-full"> |
| 250 | <TabsList_Shadcn_ className="flex gap-4 mb-8"> |
| 251 | <TabsTrigger_Shadcn_ value="app">App Frameworks</TabsTrigger_Shadcn_> |
| 252 | <TabsTrigger_Shadcn_ value="mobile"> |
| 253 | Mobile Frameworks |
| 254 | </TabsTrigger_Shadcn_> |
| 255 | </TabsList_Shadcn_> |
| 256 | <TabsContent_Shadcn_ value="app"> |
| 257 | <div className="grid gap-2 md:gap-8 md:grid-cols-2 lg:grid-cols-3"> |
| 258 | {EXAMPLE_PROJECTS.filter((project) => project.type === 'app') |
| 259 | .sort((a, b) => a.title.localeCompare(b.title)) |
| 260 | .map((project) => ( |
| 261 | <ExampleProject key={project.url} {...project} /> |
| 262 | ))} |
| 263 | </div> |
| 264 | </TabsContent_Shadcn_> |
| 265 | <TabsContent_Shadcn_ value="mobile"> |
| 266 | <div className="grid gap-2 md:gap-8 md:grid-cols-2 lg:grid-cols-3"> |
| 267 | {EXAMPLE_PROJECTS.filter((project) => project.type === 'mobile') |
| 268 | .sort((a, b) => a.title.localeCompare(b.title)) |
| 269 | .map((project) => ( |
| 270 | <ExampleProject key={project.url} {...project} /> |
| 271 | ))} |
| 272 | </div> |
| 273 | </TabsContent_Shadcn_> |
| 274 | </Tabs_Shadcn_> |
| 275 | </div> |
| 276 | )} |
| 277 | </div> |
| 278 | )} |
| 279 | </> |
| 280 | )} |
| 281 | </div> |
| 282 | </div> |
| 283 | </> |
| 284 | </div> |
| 285 | ) |
| 286 | } |