Home.tsx286 lines · main
1import { useParams } from 'common'
2import dayjs from 'dayjs'
3import Link from 'next/link'
4import { useEffect, useRef } from 'react'
5import {
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'
16import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
17
18import { AdvisorWidget } from '@/components/interfaces/Home/AdvisorWidget'
19import { ClientLibrary } from '@/components/interfaces/Home/ClientLibrary'
20import { ExampleProject } from '@/components/interfaces/Home/ExampleProject'
21import { EXAMPLE_PROJECTS } from '@/components/interfaces/Home/Home.constants'
22import { NewProjectPanel } from '@/components/interfaces/Home/NewProjectPanel/NewProjectPanel'
23import { ProjectUsageSection } from '@/components/interfaces/Home/ProjectUsageSection'
24import { ServiceStatus } from '@/components/interfaces/Home/ServiceStatus'
25import { ProjectPausedState } from '@/components/layouts/ProjectLayout/PausedState/ProjectPausedState'
26import { ComputeBadgeWrapper } from '@/components/ui/ComputeBadgeWrapper'
27import { InlineLink } from '@/components/ui/InlineLink'
28import { ProjectUpgradeFailedBanner } from '@/components/ui/ProjectUpgradeFailedBanner'
29import { useBranchesQuery } from '@/data/branches/branches-query'
30import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
31import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
32import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
33import { useTablesQuery } from '@/data/tables/tables-query'
34import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
35import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
36import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
37import { useIsOrioleDb, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
38import { DOCS_URL, IS_PLATFORM, PROJECT_STATUS } from '@/lib/constants'
39import { useAppStateSnapshot } from '@/state/app-state'
40
41export 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}