Home.tsx 13 KB

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