r2np: new cost (#32632)

* add new cost estimate

* fix api types

* update api types

* restore api.d.ts

* fix types

* fix utils

* clean utils getComputeHourlyPrice

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
This commit is contained in:
Jordi EnricandIvan Vasilov authored and GitHub committed 2025-01-10 13:21:53 +01:00
1 parent 2781fb9285
commit 0e930cf940
6 files changed
+118 -34

No files matched your search

@@ -1,32 +1,10 @@
import { instanceSizeSpecs } from 'data/projects/new-project.constants'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
export const AdditionalMonthlySpend = () => {
const { project } = useProjectContext()
/**
* New project will have the same compute size and disk size as the original project
*/
function getAdditionalMonthlySpend() {
const currentProjectComputeSize = project?.infra_compute_size
if (!currentProjectComputeSize) {
return null
}
if (currentProjectComputeSize === 'nano') {
return null
}
const additionalMonthlySpend = instanceSizeSpecs[currentProjectComputeSize]
return additionalMonthlySpend
}
const additionalMonthlySpend = getAdditionalMonthlySpend()
if (!additionalMonthlySpend) {
return null
}
import { NewProjectPrice } from './RestoreToNewProject.utils'
export const AdditionalMonthlySpend = ({
additionalMonthlySpend,
}: {
additionalMonthlySpend: NewProjectPrice
}) => {
return (
<div className="text-sm text-foreground-lighter border-t p-5">
<p>
@@ -36,9 +14,21 @@ export const AdditionalMonthlySpend = () => {
Project Settings &gt; Compute and Disk
</span>
</p>
<div className="flex justify-between text-foreground mt-2">
<p>Additional Monthly Compute + Disk Cost</p>
<p className="font-mono text-right text-brand">${additionalMonthlySpend.priceMonthly}</p>
<div className="flex flex-col gap-2 text-foreground mt-4">
<div className="flex justify-between">
<p>Additional Monthly Compute</p>
<p className="font-mono text-right text-light">${additionalMonthlySpend.computePrice}</p>
</div>
<div className="flex justify-between">
<p>Additional Monthly Disk</p>
<p className="font-mono text-right text-light">${additionalMonthlySpend.diskPrice}</p>
</div>
<div className="flex justify-between border-t pt-2">
<p>Total</p>
<p className="font-mono text-right text-brand">
${additionalMonthlySpend.computePrice + additionalMonthlySpend.diskPrice}
</p>
</div>
</div>
</div>
)
@@ -11,17 +11,20 @@ import {
DialogTitle,
} from 'ui'
import { AdditionalMonthlySpend } from './AdditionalMonthlySpend'
import { NewProjectPrice } from './RestoreToNewProject.utils'
interface ConfirmRestoreDialogProps {
open: boolean
onOpenChange: (value: boolean) => void
onSelectContinue: () => void
additionalMonthlySpend: NewProjectPrice
}
export const ConfirmRestoreDialog = ({
open,
onOpenChange,
onSelectContinue,
additionalMonthlySpend,
}: ConfirmRestoreDialogProps) => {
const { project } = useProjectContext()
const organization = useSelectedOrganization()
@@ -64,7 +67,7 @@ export const ConfirmRestoreDialog = ({
</ul>
</ul>
</DialogSection>
<AdditionalMonthlySpend />
<AdditionalMonthlySpend additionalMonthlySpend={additionalMonthlySpend} />
<DialogFooter>
<Button type="outline" onClick={() => onOpenChange(false)}>
Cancel
@@ -30,6 +30,7 @@ import {
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { AdditionalMonthlySpend } from './AdditionalMonthlySpend'
import { NewProjectPrice } from './RestoreToNewProject.utils'
interface CreateNewProjectDialogProps {
open: boolean
@@ -37,6 +38,7 @@ interface CreateNewProjectDialogProps {
recoveryTimeTarget: number | null
onOpenChange: (value: boolean) => void
onCloneSuccess: () => void
additionalMonthlySpend: NewProjectPrice
}
export const CreateNewProjectDialog = ({
@@ -45,6 +47,7 @@ export const CreateNewProjectDialog = ({
recoveryTimeTarget,
onOpenChange,
onCloneSuccess,
additionalMonthlySpend,
}: CreateNewProjectDialogProps) => {
const { project } = useProjectContext()
const organization = useSelectedOrganization()
@@ -188,7 +191,7 @@ export const CreateNewProjectDialog = ({
)}
/>
</DialogSection>
<AdditionalMonthlySpend />
<AdditionalMonthlySpend additionalMonthlySpend={additionalMonthlySpend} />
<DialogFooter>
<Button htmlType="reset" type="outline" onClick={() => onOpenChange(false)}>
Cancel
@@ -0,0 +1,64 @@
import { InfraInstanceSize } from 'components/interfaces/DiskManagement/DiskManagement.types'
import {
calculateComputeSizePrice,
calculateDiskSizePrice,
} from 'components/interfaces/DiskManagement/DiskManagement.utils'
import { DiskType } from 'components/interfaces/DiskManagement/ui/DiskManagement.constants'
import { instanceSizeSpecs } from 'data/projects/new-project.constants'
import { PlanId } from 'data/subscriptions/types'
/**
* @description
* Calculates the monthly price for a new project based on the target volume size and compute size
*
* @param targetVolumeSizeGb - The target volume size in GB
* @param targetComputeSize - The target compute size
* @returns The disk price and compute price for the new project
*/
export type NewProjectPrice = {
diskPrice: number
computePrice: number
}
export function projectSpecToMonthlyPrice({
targetVolumeSizeGb,
targetComputeSize,
planId,
storageType,
}: {
targetVolumeSizeGb: number
targetComputeSize: InfraInstanceSize
planId: PlanId
storageType: DiskType
}): NewProjectPrice {
const diskPrice = calculateDiskSizePrice({
planId,
oldSize: 0,
oldStorageType: storageType,
newSize: targetVolumeSizeGb,
newStorageType: storageType,
numReplicas: 0,
})
const computePrice = calculateComputeSizePrice({
availableOptions: [
{ identifier: targetComputeSize, price: getComputeHourlyPrice(targetComputeSize) },
],
oldComputeSize: 'nano', // not used for r2np
newComputeSize: targetComputeSize,
plan: planId,
})
return {
diskPrice: Number(diskPrice.newPrice) || 0,
computePrice: Number(computePrice.newPrice) || 0,
}
}
function getComputeHourlyPrice(computeSize: InfraInstanceSize): number {
if (computeSize === 'nano') {
return 0
}
return instanceSizeSpecs[computeSize]?.priceHourly
}
+7 -1
View File
@@ -2,6 +2,7 @@ import { get, handleError } from 'data/fetchers'
import { projectKeys } from './keys'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { ResponseError } from 'types'
import { InfraInstanceSize } from 'components/interfaces/DiskManagement/DiskManagement.types'
export async function getCloneBackups(projectRef?: string) {
if (!projectRef) throw new Error('Project ref is required')
@@ -17,7 +18,12 @@ export async function getCloneBackups(projectRef?: string) {
export type CloneBackupsData = Awaited<ReturnType<typeof getCloneBackups>>
export type CloneBackupsError = ResponseError
export const useCloneBackupsQuery = <TData = CloneBackupsData>(
export const useCloneBackupsQuery = <
TData = CloneBackupsData & {
target_volume_size_gb: number
target_compute_size: InfraInstanceSize
},
>(
{ projectRef }: { projectRef?: string },
options: UseQueryOptions<CloneBackupsData, CloneBackupsError, TData> = {}
) => {
@@ -30,6 +30,10 @@ import type { NextPageWithLayout } from 'types'
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Badge, Button } from 'ui'
import { Admonition, TimestampInfo } from 'ui-patterns'
import Panel from 'components/ui/Panel'
import { projectSpecToMonthlyPrice } from 'components/interfaces/Database/Backups/RestoreToNewProject/RestoreToNewProject.utils'
import { useDiskAttributesQuery } from 'data/config/disk-attributes-query'
import { DiskType } from 'components/interfaces/DiskManagement/ui/DiskManagement.constants'
import { InfraInstanceSize } from 'components/interfaces/DiskManagement/DiskManagement.types'
const RestoreToNewProjectPage: NextPageWithLayout = () => {
return (
@@ -86,6 +90,11 @@ const RestoreToNewProject = () => {
const PHYSICAL_BACKUPS_ENABLED = project?.is_physical_backups_enabled
const dbVersion = getDatabaseMajorVersion(project?.dbVersion ?? '')
const IS_PG15_OR_ABOVE = dbVersion >= 15
const targetVolumeSizeGb = cloneBackups?.target_volume_size_gb
const targetComputeSize = cloneBackups?.target_compute_size
const planId = subscription?.plan?.id ?? 'free'
const { data } = useDiskAttributesQuery({ projectRef: project?.ref })
const storageType = data?.attributes?.type ?? 'gp3'
const {
data: cloneStatus,
@@ -310,6 +319,13 @@ const RestoreToNewProject = () => {
)
}
const additionalMonthlySpend = projectSpecToMonthlyPrice({
targetVolumeSizeGb: targetVolumeSizeGb ?? 0,
targetComputeSize: targetComputeSize ?? 'nano',
planId: planId ?? 'free',
storageType: storageType as DiskType,
})
return (
<div className="flex flex-col gap-4">
<ConfirmRestoreDialog
@@ -319,11 +335,13 @@ const RestoreToNewProject = () => {
setShowConfirmationDialog(false)
setShowNewProjectDialog(true)
}}
additionalMonthlySpend={additionalMonthlySpend}
/>
<CreateNewProjectDialog
open={showNewProjectDialog}
selectedBackupId={selectedBackupId}
recoveryTimeTarget={recoveryTimeTarget}
additionalMonthlySpend={additionalMonthlySpend}
onOpenChange={setShowNewProjectDialog}
onCloneSuccess={() => {
refetchCloneStatus()