| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- import { LOCAL_STORAGE_KEYS, useFlag } from 'common'
- import { useRouter } from 'next/router'
- import { UseFormReturn } from 'react-hook-form'
- import {
- Badge,
- Button,
- PopoverSeparator,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- } from 'ui'
- import { InfoTooltip } from 'ui-patterns/info-tooltip'
- import { CreateProjectForm } from './ProjectCreation.schema'
- import { instanceLabel, monthlyInstancePrice } from './ProjectCreation.utils'
- import { InlineLink } from '@/components/ui/InlineLink'
- import { DesiredInstanceSize, instanceSizeSpecs } from '@/data/projects/new-project.constants'
- import { OrgProject } from '@/data/projects/org-projects-infinite-query'
- import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { DOCS_URL } from '@/lib/constants'
- interface ProjectCreationFooterProps {
- form: UseFormReturn<CreateProjectForm>
- canCreateProject: boolean
- instanceSize?: string
- organizationProjects: OrgProject[]
- isCreatingNewProject: boolean
- isSuccessNewProject: boolean
- }
- export const ProjectCreationFooter = ({
- form,
- canCreateProject,
- instanceSize,
- organizationProjects,
- isCreatingNewProject,
- isSuccessNewProject,
- }: ProjectCreationFooterProps) => {
- const router = useRouter()
- const { data: currentOrg } = useSelectedOrganizationQuery()
- const isFreePlan = currentOrg?.plan?.id === 'free'
- const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
- const [lastVisitedOrganization] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
- ''
- )
- const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0
- const additionalMonthlySpend = isFreePlan
- ? 0
- : instanceSizeSpecs[instanceSize as DesiredInstanceSize]!.priceMonthly - availableComputeCredits
- // [kevin] This will eventually all be provided by a new API endpoint to preview and validate project creation, this is just for kaizen now
- const monthlyComputeCosts =
- // current project costs
- organizationProjects.reduce((prev, acc) => {
- const primaryDatabase = acc.databases.find((db) => db.identifier === acc.ref)
- const cost = !!primaryDatabase ? monthlyInstancePrice(primaryDatabase.infra_compute_size) : 0
- return prev + cost
- }, 0) +
- // selected compute size
- monthlyInstancePrice(instanceSize) -
- // compute credits
- 10
- return (
- <div key="panel-footer" className="grid grid-cols-12 w-full gap-4 items-center">
- <div className="col-span-4">
- {!isFreePlan &&
- !projectCreationDisabled &&
- canCreateProject &&
- additionalMonthlySpend > 0 && (
- <div className="flex justify-between text-sm">
- <span>Additional costs</span>
- <div className="text-brand flex gap-1 items-center font-mono font-medium">
- <span>${additionalMonthlySpend}/m</span>
- <InfoTooltip side="top" className="max-w-[450px] p-0">
- <div className="p-4 text-sm text-foreground-light space-y-1">
- <p>
- Each project includes a dedicated Postgres instance running on its own server.
- You are charged for the{' '}
- <InlineLink href={`${DOCS_URL}/guides/platform/billing-on-briven`}>
- Compute resource
- </InlineLink>{' '}
- of that server, independent of your database usage.
- </p>
- {monthlyComputeCosts > 0 && (
- <p>Compute costs are applied on top of your subscription plan costs.</p>
- )}
- </div>
- <Table className="mt-2">
- <TableHeader className="[&_th]:h-7">
- <TableRow className="py-2">
- <TableHead className="w-[170px]">Project</TableHead>
- <TableHead>Compute Size</TableHead>
- <TableHead className="text-right">Monthly Costs</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody className="[&_td]:py-2">
- {organizationProjects.map((project) => {
- const primaryDb = project.databases.find(
- (db) => db.identifier === project.ref
- )
- return (
- <TableRow key={project.ref} className="text-foreground-light">
- <TableCell className="w-[170px] truncate">{project.name}</TableCell>
- <TableCell className="text-center">
- {instanceLabel(primaryDb?.infra_compute_size)}
- </TableCell>
- <TableCell className="text-right">
- ${monthlyInstancePrice(primaryDb?.infra_compute_size)}
- </TableCell>
- </TableRow>
- )
- })}
- <TableRow>
- <TableCell className="w-[170px] flex gap-2">
- <span className="truncate">
- {form.getValues('projectName') || 'New project'}
- </span>
- <Badge variant="success">New</Badge>
- </TableCell>
- <TableCell className="text-center">{instanceLabel(instanceSize)}</TableCell>
- <TableCell className="text-right">
- ${monthlyInstancePrice(instanceSize)}
- </TableCell>
- </TableRow>
- </TableBody>
- </Table>
- <PopoverSeparator />
- <Table>
- <TableHeader className="[&_th]:h-7">
- <TableRow>
- <TableHead colSpan={2}>Compute Credits</TableHead>
- <TableHead colSpan={1} className="text-right">
- -$10
- </TableHead>
- </TableRow>
- </TableHeader>
- <TableBody className="[&_td]:py-2">
- <TableRow className="text-foreground">
- <TableCell colSpan={2}>
- Total Monthly Compute Costs
- {/**
- * API currently doesnt output replica information on the projects list endpoint. Until then, we cannot correctly calculate the costs including RRs.
- * Will be adjusted in the future [kevin]
- */}
- {organizationProjects.length > 0 && (
- <p className="text-xs text-foreground-lighter">
- Excluding Read replicas
- </p>
- )}
- </TableCell>
- <TableCell colSpan={1} className="text-right">
- ${monthlyComputeCosts}
- </TableCell>
- </TableRow>
- </TableBody>
- </Table>
- </InfoTooltip>
- </div>
- </div>
- )}
- </div>
- <div className="flex items-end col-span-8 space-x-2 ml-auto">
- <Button
- type="default"
- disabled={isCreatingNewProject || isSuccessNewProject}
- onClick={() => {
- if (!!lastVisitedOrganization) router.push(`/org/${lastVisitedOrganization}`)
- else router.push('/organizations')
- }}
- >
- Cancel
- </Button>
- <Button
- htmlType="submit"
- loading={isCreatingNewProject || isSuccessNewProject}
- disabled={!canCreateProject}
- >
- Create new project
- </Button>
- </div>
- </div>
- )
- }
|