ProjectCreationFooter.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { LOCAL_STORAGE_KEYS, useFlag } from 'common'
  2. import { useRouter } from 'next/router'
  3. import { UseFormReturn } from 'react-hook-form'
  4. import {
  5. Badge,
  6. Button,
  7. PopoverSeparator,
  8. Table,
  9. TableBody,
  10. TableCell,
  11. TableHead,
  12. TableHeader,
  13. TableRow,
  14. } from 'ui'
  15. import { InfoTooltip } from 'ui-patterns/info-tooltip'
  16. import { CreateProjectForm } from './ProjectCreation.schema'
  17. import { instanceLabel, monthlyInstancePrice } from './ProjectCreation.utils'
  18. import { InlineLink } from '@/components/ui/InlineLink'
  19. import { DesiredInstanceSize, instanceSizeSpecs } from '@/data/projects/new-project.constants'
  20. import { OrgProject } from '@/data/projects/org-projects-infinite-query'
  21. import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
  22. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  23. import { DOCS_URL } from '@/lib/constants'
  24. interface ProjectCreationFooterProps {
  25. form: UseFormReturn<CreateProjectForm>
  26. canCreateProject: boolean
  27. instanceSize?: string
  28. organizationProjects: OrgProject[]
  29. isCreatingNewProject: boolean
  30. isSuccessNewProject: boolean
  31. }
  32. export const ProjectCreationFooter = ({
  33. form,
  34. canCreateProject,
  35. instanceSize,
  36. organizationProjects,
  37. isCreatingNewProject,
  38. isSuccessNewProject,
  39. }: ProjectCreationFooterProps) => {
  40. const router = useRouter()
  41. const { data: currentOrg } = useSelectedOrganizationQuery()
  42. const isFreePlan = currentOrg?.plan?.id === 'free'
  43. const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
  44. const [lastVisitedOrganization] = useLocalStorageQuery(
  45. LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
  46. ''
  47. )
  48. const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0
  49. const additionalMonthlySpend = isFreePlan
  50. ? 0
  51. : instanceSizeSpecs[instanceSize as DesiredInstanceSize]!.priceMonthly - availableComputeCredits
  52. // [kevin] This will eventually all be provided by a new API endpoint to preview and validate project creation, this is just for kaizen now
  53. const monthlyComputeCosts =
  54. // current project costs
  55. organizationProjects.reduce((prev, acc) => {
  56. const primaryDatabase = acc.databases.find((db) => db.identifier === acc.ref)
  57. const cost = !!primaryDatabase ? monthlyInstancePrice(primaryDatabase.infra_compute_size) : 0
  58. return prev + cost
  59. }, 0) +
  60. // selected compute size
  61. monthlyInstancePrice(instanceSize) -
  62. // compute credits
  63. 10
  64. return (
  65. <div key="panel-footer" className="grid grid-cols-12 w-full gap-4 items-center">
  66. <div className="col-span-4">
  67. {!isFreePlan &&
  68. !projectCreationDisabled &&
  69. canCreateProject &&
  70. additionalMonthlySpend > 0 && (
  71. <div className="flex justify-between text-sm">
  72. <span>Additional costs</span>
  73. <div className="text-brand flex gap-1 items-center font-mono font-medium">
  74. <span>${additionalMonthlySpend}/m</span>
  75. <InfoTooltip side="top" className="max-w-[450px] p-0">
  76. <div className="p-4 text-sm text-foreground-light space-y-1">
  77. <p>
  78. Each project includes a dedicated Postgres instance running on its own server.
  79. You are charged for the{' '}
  80. <InlineLink href={`${DOCS_URL}/guides/platform/billing-on-briven`}>
  81. Compute resource
  82. </InlineLink>{' '}
  83. of that server, independent of your database usage.
  84. </p>
  85. {monthlyComputeCosts > 0 && (
  86. <p>Compute costs are applied on top of your subscription plan costs.</p>
  87. )}
  88. </div>
  89. <Table className="mt-2">
  90. <TableHeader className="[&_th]:h-7">
  91. <TableRow className="py-2">
  92. <TableHead className="w-[170px]">Project</TableHead>
  93. <TableHead>Compute Size</TableHead>
  94. <TableHead className="text-right">Monthly Costs</TableHead>
  95. </TableRow>
  96. </TableHeader>
  97. <TableBody className="[&_td]:py-2">
  98. {organizationProjects.map((project) => {
  99. const primaryDb = project.databases.find(
  100. (db) => db.identifier === project.ref
  101. )
  102. return (
  103. <TableRow key={project.ref} className="text-foreground-light">
  104. <TableCell className="w-[170px] truncate">{project.name}</TableCell>
  105. <TableCell className="text-center">
  106. {instanceLabel(primaryDb?.infra_compute_size)}
  107. </TableCell>
  108. <TableCell className="text-right">
  109. ${monthlyInstancePrice(primaryDb?.infra_compute_size)}
  110. </TableCell>
  111. </TableRow>
  112. )
  113. })}
  114. <TableRow>
  115. <TableCell className="w-[170px] flex gap-2">
  116. <span className="truncate">
  117. {form.getValues('projectName') || 'New project'}
  118. </span>
  119. <Badge variant="success">New</Badge>
  120. </TableCell>
  121. <TableCell className="text-center">{instanceLabel(instanceSize)}</TableCell>
  122. <TableCell className="text-right">
  123. ${monthlyInstancePrice(instanceSize)}
  124. </TableCell>
  125. </TableRow>
  126. </TableBody>
  127. </Table>
  128. <PopoverSeparator />
  129. <Table>
  130. <TableHeader className="[&_th]:h-7">
  131. <TableRow>
  132. <TableHead colSpan={2}>Compute Credits</TableHead>
  133. <TableHead colSpan={1} className="text-right">
  134. -$10
  135. </TableHead>
  136. </TableRow>
  137. </TableHeader>
  138. <TableBody className="[&_td]:py-2">
  139. <TableRow className="text-foreground">
  140. <TableCell colSpan={2}>
  141. Total Monthly Compute Costs
  142. {/**
  143. * API currently doesnt output replica information on the projects list endpoint. Until then, we cannot correctly calculate the costs including RRs.
  144. * Will be adjusted in the future [kevin]
  145. */}
  146. {organizationProjects.length > 0 && (
  147. <p className="text-xs text-foreground-lighter">
  148. Excluding Read replicas
  149. </p>
  150. )}
  151. </TableCell>
  152. <TableCell colSpan={1} className="text-right">
  153. ${monthlyComputeCosts}
  154. </TableCell>
  155. </TableRow>
  156. </TableBody>
  157. </Table>
  158. </InfoTooltip>
  159. </div>
  160. </div>
  161. )}
  162. </div>
  163. <div className="flex items-end col-span-8 space-x-2 ml-auto">
  164. <Button
  165. type="default"
  166. disabled={isCreatingNewProject || isSuccessNewProject}
  167. onClick={() => {
  168. if (!!lastVisitedOrganization) router.push(`/org/${lastVisitedOrganization}`)
  169. else router.push('/organizations')
  170. }}
  171. >
  172. Cancel
  173. </Button>
  174. <Button
  175. htmlType="submit"
  176. loading={isCreatingNewProject || isSuccessNewProject}
  177. disabled={!canCreateProject}
  178. >
  179. Create new project
  180. </Button>
  181. </div>
  182. </div>
  183. )
  184. }