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