mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 15:49:09 +03:00
moved things around. billing badges updated. compute added
This commit is contained in:
1 parent
9e02c81f6b
commit
c5a32fb534
8 files changed
+342
-178
No files matched your search
@@ -8,6 +8,7 @@ interface BillingChangeBadgeProps {
|
||||
afterPrice?: number
|
||||
show: boolean | undefined
|
||||
tooltip?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const BillingChangeBadge = ({
|
||||
@@ -15,17 +16,21 @@ const BillingChangeBadge = ({
|
||||
afterPrice,
|
||||
show,
|
||||
tooltip,
|
||||
className,
|
||||
}: BillingChangeBadgeProps) => {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{beforePrice !== undefined && afterPrice !== undefined && show && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -4 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -4 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, x: -4, height: 0 }}
|
||||
animate={{ opacity: 1, x: 0, height: 'auto' }}
|
||||
exit={{ opacity: 0, x: -4, height: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 800, damping: 40, duration: 0.3 }}
|
||||
// key={afterPrice} // This key will change whenever any form value changes
|
||||
// whileHover={{ scale: 1.05 }}
|
||||
// whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Badge variant="default" className="bg-alternative bg-opacity-100">
|
||||
<Badge variant="default" className={`bg-alternative bg-opacity-100 ${className}`}>
|
||||
<Tooltip_Shadcn_>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -33,9 +38,14 @@ const BillingChangeBadge = ({
|
||||
{formatCurrency(beforePrice)}
|
||||
</span>
|
||||
<ChevronRight size={12} strokeWidth={2} className="text-foreground-muted" />
|
||||
<span className="text-xs font-mono text-foreground">
|
||||
<motion.span
|
||||
key={afterPrice} // This key will change whenever any form value changes
|
||||
className="text-xs font-mono text-foreground"
|
||||
animate={{ scale: [1, 1.1, 1] }}
|
||||
transition={{ duration: 0.12 }}
|
||||
>
|
||||
{formatCurrency(afterPrice)}
|
||||
</span>
|
||||
</motion.span>
|
||||
</div>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
{tooltip !== undefined && (
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { components } from 'api-types'
|
||||
import { DISK_LIMITS, DISK_PRICING, DiskType, PLAN_DETAILS } from './DiskManagement.constants'
|
||||
import { INSTANCE_MICRO_SPECS } from 'lib/constants'
|
||||
import { ProjectAddonVariantMeta } from 'data/subscriptions/types'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
// Included disk size only applies to primary, not replicas
|
||||
export const calculateDiskSizePrice = ({
|
||||
@@ -31,6 +35,29 @@ export const calculateDiskSizePrice = ({
|
||||
}
|
||||
}
|
||||
|
||||
export const calculateComputeSizePrice = ({
|
||||
availableOptions,
|
||||
oldComputeSize,
|
||||
newComputeSize,
|
||||
}: {
|
||||
availableOptions: {
|
||||
identifier: string
|
||||
price: number
|
||||
}[]
|
||||
oldComputeSize: string
|
||||
newComputeSize: string
|
||||
}) => {
|
||||
console.log('newComputeSize', newComputeSize)
|
||||
|
||||
const oldPrice = availableOptions.find((x) => x.identifier === oldComputeSize)?.price * 720
|
||||
const newPrice = availableOptions.find((x) => x.identifier === newComputeSize)?.price * 720
|
||||
|
||||
return {
|
||||
oldPrice,
|
||||
newPrice,
|
||||
}
|
||||
}
|
||||
|
||||
// Included IOPS applies to both primary and replicas
|
||||
export const calculateIOPSPrice = ({
|
||||
oldStorageType,
|
||||
@@ -109,3 +136,51 @@ export const calculateThroughputPrice = ({
|
||||
}
|
||||
return { oldPrice: '0.00', newPrice: '0.00' }
|
||||
}
|
||||
|
||||
export function getAvailableComputeOptions(availableAddons: any[], projectCloudProvider?: string) {
|
||||
const computeOptions =
|
||||
availableAddons
|
||||
.find((addon) => addon.type === 'compute_instance')
|
||||
?.variants.filter((option) => {
|
||||
if (!projectCloudProvider) {
|
||||
return true
|
||||
}
|
||||
|
||||
const meta = option.meta as ProjectAddonVariantMeta
|
||||
|
||||
return (
|
||||
!meta.supported_cloud_providers ||
|
||||
meta.supported_cloud_providers.includes(projectCloudProvider)
|
||||
)
|
||||
}) ?? []
|
||||
|
||||
function hasMicroOptionFromApi() {
|
||||
return (
|
||||
availableAddons.find((addon) => addon.type === 'compute_instance')?.variants ?? []
|
||||
).some((variant: any) => variant.identifier === 'ci_micro')
|
||||
}
|
||||
|
||||
// Backwards comp until API is deployed
|
||||
if (!hasMicroOptionFromApi) {
|
||||
// Unshift to push to start of array
|
||||
computeOptions.unshift({
|
||||
identifier: 'ci_micro',
|
||||
name: 'Micro',
|
||||
price_description: '$0.01344/hour (~$10/month)',
|
||||
price: 0.01344,
|
||||
price_interval: 'hourly',
|
||||
price_type: 'usage',
|
||||
meta: {
|
||||
cpu_cores: INSTANCE_MICRO_SPECS.cpu_cores,
|
||||
cpu_dedicated: INSTANCE_MICRO_SPECS.cpu_dedicated,
|
||||
memory_gb: INSTANCE_MICRO_SPECS.memory_gb,
|
||||
baseline_disk_io_mbs: INSTANCE_MICRO_SPECS.baseline_disk_io_mbs,
|
||||
max_disk_io_mbs: INSTANCE_MICRO_SPECS.max_disk_io_mbs,
|
||||
connections_direct: INSTANCE_MICRO_SPECS.connections_direct,
|
||||
connections_pooler: INSTANCE_MICRO_SPECS.connections_pooler,
|
||||
} as ProjectAddonVariantMeta,
|
||||
})
|
||||
}
|
||||
|
||||
return computeOptions
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { GB } from 'lib/constants'
|
||||
import { GB, INSTANCE_MICRO_SPECS } from 'lib/constants'
|
||||
import {
|
||||
Alert_Shadcn_,
|
||||
AlertDescription_Shadcn_,
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
cn,
|
||||
Form_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
@@ -63,9 +64,11 @@ import {
|
||||
THROUGHPUT_RANGE,
|
||||
} from './DiskManagement.constants'
|
||||
import {
|
||||
calculateComputeSizePrice,
|
||||
calculateDiskSizePrice,
|
||||
calculateIOPSPrice,
|
||||
calculateThroughputPrice,
|
||||
getAvailableComputeOptions,
|
||||
} from './DiskManagement.utils'
|
||||
import { DiskStorageSchema, DiskStorageSchemaType } from './DiskManagementPanelSchema'
|
||||
import { DiskManagementPlanUpgradeRequired } from './DiskManagementPlanUpgradeRequired'
|
||||
@@ -80,6 +83,7 @@ import { ProjectAddonVariantMeta } from 'data/subscriptions/types'
|
||||
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
|
||||
import { ComputeBadge } from 'ui-patterns'
|
||||
import { components } from 'api-types'
|
||||
import { MAX_WIDTH_CLASSES, PADDING_CLASSES, ScaffoldContainer } from 'components/layouts/Scaffold'
|
||||
|
||||
export function DiskManagementForm() {
|
||||
const { project } = useProjectContext()
|
||||
@@ -170,54 +174,12 @@ export function DiskManagementForm() {
|
||||
return addons?.available_addons ?? []
|
||||
}, [addons])
|
||||
|
||||
const hasMicroOptionFromApi = useMemo(() => {
|
||||
return (
|
||||
availableAddons.find((addon) => addon.type === 'compute_instance')?.variants ?? []
|
||||
).some((variant) => variant.identifier === 'ci_micro')
|
||||
}, [availableAddons])
|
||||
const selectedAddons = addons?.selected_addons ?? []
|
||||
const subscriptionCompute = selectedAddons.find((addon) => addon.type === 'compute_instance')
|
||||
|
||||
const availableOptions = useMemo(() => {
|
||||
const computeOptions =
|
||||
availableAddons
|
||||
.find((addon) => addon.type === 'compute_instance')
|
||||
?.variants.filter((option) => {
|
||||
if (!project?.cloud_provider) {
|
||||
return true
|
||||
}
|
||||
|
||||
const meta = option.meta as ProjectAddonVariantMeta
|
||||
|
||||
return (
|
||||
!meta.supported_cloud_providers ||
|
||||
meta.supported_cloud_providers.includes(project.cloud_provider)
|
||||
)
|
||||
}) ?? []
|
||||
|
||||
// Backwards comp until API is deployed
|
||||
// if (!hasMicroOptionFromApi) {
|
||||
// // Unshift to push to start of array
|
||||
// computeOptions.unshift({
|
||||
// identifier: 'ci_micro',
|
||||
// name: 'Micro',
|
||||
// price_description: '$0.01344/hour (~$10/month)',
|
||||
// price: 0.01344,
|
||||
// price_interval: 'hourly',
|
||||
// price_type: 'usage',
|
||||
// // @ts-ignore API types it as Record<string, never>
|
||||
// meta: {
|
||||
// cpu_cores: INSTANCE_MICRO_SPECS.cpu_cores,
|
||||
// cpu_dedicated: INSTANCE_MICRO_SPECS.cpu_dedicated,
|
||||
// memory_gb: INSTANCE_MICRO_SPECS.memory_gb,
|
||||
// baseline_disk_io_mbs: INSTANCE_MICRO_SPECS.baseline_disk_io_mbs,
|
||||
// max_disk_io_mbs: INSTANCE_MICRO_SPECS.max_disk_io_mbs,
|
||||
// connections_direct: INSTANCE_MICRO_SPECS.connections_direct,
|
||||
// connections_pooler: INSTANCE_MICRO_SPECS.connections_pooler,
|
||||
// } as ProjectAddonVariantMeta,
|
||||
// })
|
||||
// }
|
||||
|
||||
return computeOptions
|
||||
}, [availableAddons, hasMicroOptionFromApi])
|
||||
return getAvailableComputeOptions(availableAddons, project?.cloud_provider)
|
||||
}, [availableAddons, project?.cloud_provider])
|
||||
|
||||
/**
|
||||
* Handle default values
|
||||
@@ -229,10 +191,9 @@ export function DiskManagementForm() {
|
||||
provisionedIOPS: iops,
|
||||
throughput: throughput_mbps,
|
||||
totalSize: size_gb,
|
||||
computeSize: subscriptionCompute?.variant.identifier ?? 'ci_micro',
|
||||
}
|
||||
|
||||
console.log('type', type)
|
||||
|
||||
const form = useForm<DiskStorageSchemaType>({
|
||||
resolver: zodResolver(DiskStorageSchema),
|
||||
defaultValues,
|
||||
@@ -311,6 +272,12 @@ export function DiskManagementForm() {
|
||||
/**
|
||||
* Price calculations
|
||||
*/
|
||||
|
||||
const computeSizePrice = calculateComputeSizePrice({
|
||||
availableOptions: availableOptions,
|
||||
oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro',
|
||||
newComputeSize: form.getValues('computeSize'),
|
||||
})
|
||||
const diskSizePrice = calculateDiskSizePrice({
|
||||
planId,
|
||||
oldSize: form.formState.defaultValues?.totalSize || 0,
|
||||
@@ -423,71 +390,105 @@ export function DiskManagementForm() {
|
||||
|
||||
// return <></>
|
||||
|
||||
const isDirty = !!Object.keys(form.formState.dirtyFields).length
|
||||
|
||||
return (
|
||||
<div id="disk-management">
|
||||
{/* <FormHeader
|
||||
title="Disk Management"
|
||||
docsUrl="https://supabase.com/docs/guides/platform/database-size#disk-management"
|
||||
/> */}
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-8">
|
||||
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
|
||||
{/* {showNewDiskManagementUI ? <DiskManagementForm /> : null} */}
|
||||
<Separator />
|
||||
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-8">
|
||||
<RadioGroupCard className="flex flex-wrap gap-3">
|
||||
{availableOptions.map((compute) => {
|
||||
const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider)
|
||||
<FormField_Shadcn_
|
||||
name="computeSize"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label={'Compute size'}
|
||||
labelOptional={
|
||||
<>
|
||||
<BillingChangeBadge
|
||||
className={'mb-2'}
|
||||
show={
|
||||
formState.isDirty &&
|
||||
formState.dirtyFields.computeSize &&
|
||||
!formState.errors.computeSize
|
||||
}
|
||||
beforePrice={Number(computeSizePrice.oldPrice)}
|
||||
afterPrice={Number(computeSizePrice.newPrice)}
|
||||
/>
|
||||
<p>hardware resources allocated to your postgres database</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<RadioGroupCard
|
||||
className="grid grid-cols-3 flex-wrap gap-3"
|
||||
onValueChange={(value) => {
|
||||
setValue('computeSize', value, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
{availableOptions.map((compute) => {
|
||||
const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider)
|
||||
|
||||
return (
|
||||
<RadioGroupCardItem
|
||||
// label={compute.name}
|
||||
disabled={disableInput}
|
||||
showIndicator={false}
|
||||
value={compute.identifier}
|
||||
className="text-sm text-left flex flex-col gap-0 px-0 py-3 overflow-hidden [&_label]:w-full group] min-w-[250px]"
|
||||
// @ts-ignore
|
||||
label={
|
||||
<div className="w-full flex flex-col gap-3">
|
||||
<div className="px-3 opacity-50 group-data-[state=checked]:opacity-100 flex justify-between">
|
||||
<ComputeBadge
|
||||
className="inline-flex font-semibold"
|
||||
infraComputeSize={compute.name as components['schemas']['DbInstanceSize']}
|
||||
/>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-foreground text-sm font-semibold">
|
||||
{/* Price needs to be exact here */}${compute.price}
|
||||
</span>
|
||||
<span className="text-foreground-light translate-y-[1px]">
|
||||
{' '}
|
||||
/ {compute.price_interval === 'monthly' ? 'month' : 'hour'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* <Separator className="bg-border group-data-[state=checked]:bg-foreground-muted" /> */}
|
||||
<div className="w-full">
|
||||
<div className="px-3 text-sm flex flex-col gap-1">
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<Microchip
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>{compute.meta?.memory_gb ?? 0} GB memory</span>
|
||||
</div>
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<CpuIcon
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>
|
||||
{compute.meta?.cpu_cores ?? 0}-core {cpuArchitecture} CPU
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="px-2">
|
||||
return (
|
||||
<RadioGroupCardItem
|
||||
showIndicator={false}
|
||||
value={compute.identifier}
|
||||
className="text-sm text-left flex flex-col gap-0 px-0 py-3 overflow-hidden [&_label]:w-full group] w-full"
|
||||
// @ts-ignore
|
||||
label={
|
||||
<div className="w-full flex flex-col gap-3">
|
||||
<div className="px-3 opacity-50 group-data-[state=checked]:opacity-100 flex justify-between">
|
||||
<ComputeBadge
|
||||
className="inline-flex font-semibold"
|
||||
infraComputeSize={
|
||||
compute.name as components['schemas']['DbInstanceSize']
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-foreground text-sm font-semibold">
|
||||
{/* Price needs to be exact here */}${compute.price}
|
||||
</span>
|
||||
<span className="text-foreground-light translate-y-[1px]">
|
||||
{' '}
|
||||
/ {compute.price_interval === 'monthly' ? 'month' : 'hour'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* <Separator className="bg-border group-data-[state=checked]:bg-foreground-muted" /> */}
|
||||
<div className="w-full">
|
||||
<div className="px-3 text-sm flex flex-col gap-1">
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<Microchip
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>{compute.meta?.memory_gb ?? 0} GB memory</span>
|
||||
</div>
|
||||
<div className="text-foreground-light flex gap-2 items-center">
|
||||
<CpuIcon
|
||||
strokeWidth={1}
|
||||
size={14}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
<span>
|
||||
{compute.meta?.cpu_cores ?? 0}-core {cpuArchitecture} CPU
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="px-2">
|
||||
<span>{compute.meta?.cpu_dedicated ? 'Dedicated' : 'Shared'}</span>
|
||||
</div> */}
|
||||
|
||||
{/* <div className="px-3 py-1">
|
||||
{/* <div className="px-3 py-1">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-foreground text-sm">
|
||||
${compute.price}
|
||||
@@ -498,13 +499,17 @@ export function DiskManagementForm() {
|
||||
</span>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
></RadioGroupCardItem>
|
||||
)
|
||||
})}
|
||||
</RadioGroupCard>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
></RadioGroupCardItem>
|
||||
)
|
||||
})}
|
||||
</RadioGroupCard>
|
||||
</FormItemLayout>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{/* <Card className="bg-surface-100 rounded-b-none">
|
||||
<CardContent className="transition-all duration-500 ease-in-out py-10 flex flex-col gap-10 px-8"> */}
|
||||
|
||||
@@ -661,7 +666,24 @@ export function DiskManagementForm() {
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
labelOptional="Input/output operations per second. Higher IOPS is suitable for applications requiring high throughput."
|
||||
labelOptional={
|
||||
<>
|
||||
<BillingChangeBadge
|
||||
show={
|
||||
(watchedStorageType !== type ||
|
||||
(watchedStorageType === 'gp3' && field.value !== iops)) &&
|
||||
!formState.errors.provisionedIOPS
|
||||
}
|
||||
beforePrice={Number(iopsPrice.oldPrice)}
|
||||
afterPrice={Number(iopsPrice.newPrice)}
|
||||
className="mb-2"
|
||||
/>
|
||||
<p>
|
||||
Input/output operations per second. Higher IOPS is suitable for applications
|
||||
requiring high throughput.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex -space-x-px">
|
||||
@@ -684,15 +706,6 @@ export function DiskManagementForm() {
|
||||
<span className="text-foreground-lighter text-xs font-mono">IOPS</span>
|
||||
</div>
|
||||
</div>
|
||||
<BillingChangeBadge
|
||||
show={
|
||||
(watchedStorageType !== type ||
|
||||
(watchedStorageType === 'gp3' && field.value !== iops)) &&
|
||||
!formState.errors.provisionedIOPS
|
||||
}
|
||||
beforePrice={Number(iopsPrice.oldPrice)}
|
||||
afterPrice={Number(iopsPrice.newPrice)}
|
||||
/>
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
@@ -758,6 +771,25 @@ export function DiskManagementForm() {
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
labelOptional={
|
||||
<>
|
||||
<BillingChangeBadge
|
||||
show={
|
||||
formState.isDirty &&
|
||||
formState.dirtyFields.throughput &&
|
||||
!formState.errors.throughput
|
||||
}
|
||||
beforePrice={Number(throughputPrice.oldPrice)}
|
||||
afterPrice={Number(throughputPrice.newPrice)}
|
||||
className="mb-2"
|
||||
/>
|
||||
<p>
|
||||
Throughput is the amount of data that can be read or written to the disk
|
||||
per second. Higher throughput is suitable for applications requiring
|
||||
high throughput.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex -space-x-px">
|
||||
@@ -779,15 +811,6 @@ export function DiskManagementForm() {
|
||||
<span className="text-foreground-lighter text-xs font-mono">MB/s</span>
|
||||
</div>
|
||||
</div>
|
||||
<BillingChangeBadge
|
||||
show={
|
||||
formState.isDirty &&
|
||||
formState.dirtyFields.throughput &&
|
||||
!formState.errors.throughput
|
||||
}
|
||||
beforePrice={Number(throughputPrice.oldPrice)}
|
||||
afterPrice={Number(throughputPrice.newPrice)}
|
||||
/>
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
@@ -910,28 +933,48 @@ export function DiskManagementForm() {
|
||||
)}
|
||||
|
||||
{isPlanUpgradeRequired && <DiskManagementPlanUpgradeRequired />}
|
||||
</ScaffoldContainer>
|
||||
|
||||
{/* <Card className="bg-surface-100 rounded-t-none">
|
||||
{/* <Card className="bg-surface-100 rounded-t-none">
|
||||
<CardContent className="flex items-center pb-0 py-3 px-8 gap-3 justify-end"> */}
|
||||
<FormFooterChangeBadge formState={formState} />
|
||||
<div className="flex gap-2">
|
||||
<Button type="default" onClick={() => form.reset()} disabled={!form.formState.isDirty}>
|
||||
Cancel
|
||||
</Button>
|
||||
<DiskManagementReviewAndSubmitDialog
|
||||
loading={isUpdatingDiskConfiguration}
|
||||
form={form}
|
||||
numReplicas={readReplicas.length}
|
||||
isDialogOpen={isDialogOpen}
|
||||
isWithinCooldown={disableInput}
|
||||
onSubmit={onSubmit}
|
||||
setIsDialogOpen={setIsDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
{/* </CardContent> */}
|
||||
{/* </Card> */}
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isDirty ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.1, delay: 0.4 }}
|
||||
className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
MAX_WIDTH_CLASSES,
|
||||
PADDING_CLASSES,
|
||||
'flex items-center gap-3 justify-end'
|
||||
)}
|
||||
>
|
||||
<FormFooterChangeBadge formState={formState} />
|
||||
<Button type="default" onClick={() => form.reset()} disabled={!isDirty}>
|
||||
Cancel
|
||||
</Button>
|
||||
<DiskManagementReviewAndSubmitDialog
|
||||
loading={isUpdatingDiskConfiguration}
|
||||
form={form}
|
||||
numReplicas={readReplicas.length}
|
||||
isDialogOpen={isDialogOpen}
|
||||
isWithinCooldown={disableInput}
|
||||
onSubmit={onSubmit}
|
||||
setIsDialogOpen={setIsDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* </CardContent> */}
|
||||
{/* </Card> */}
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)
|
||||
}
|
||||
@@ -9,10 +9,11 @@ const baseSchema = z.object({
|
||||
.describe('Allocated disk size in GB'),
|
||||
provisionedIOPS: z.number().describe('Provisioned IOPS for storage type'),
|
||||
throughput: z.number().optional().describe('Throughput in MB/s for gp3'),
|
||||
computeSize: z.string().describe('Compute size'),
|
||||
})
|
||||
|
||||
export const DiskStorageSchema = baseSchema.superRefine((data, ctx) => {
|
||||
const { storageType, totalSize, provisionedIOPS, throughput } = data
|
||||
const { storageType, totalSize, provisionedIOPS, throughput, computeSize } = data
|
||||
|
||||
if (storageType === 'io2') {
|
||||
// Validation rules for io2
|
||||
|
||||
+41
-3
@@ -28,13 +28,17 @@ import {
|
||||
import BillingChangeBadge from './BillingChangeBadge'
|
||||
import { DiskType } from './DiskManagement.constants'
|
||||
import {
|
||||
calculateComputeSizePrice,
|
||||
calculateDiskSizePrice,
|
||||
calculateIOPSPrice,
|
||||
calculateThroughputPrice,
|
||||
getAvailableComputeOptions,
|
||||
} from './DiskManagement.utils'
|
||||
import { DiskMangementCoolDownSection } from './DiskManagementCoolDownSection'
|
||||
import { DiskStorageSchemaType } from './DiskManagementPanelSchema'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useMemo } from 'react'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
|
||||
const TableHeaderRow = () => (
|
||||
<TableRow>
|
||||
@@ -127,7 +131,17 @@ export const DiskManagementReviewAndSubmitDialog = ({
|
||||
}: DiskSizeMeterProps) => {
|
||||
const { project } = useProjectContext()
|
||||
const org = useSelectedOrganization()
|
||||
|
||||
/**
|
||||
* Queries
|
||||
* */
|
||||
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: org?.slug })
|
||||
const {
|
||||
data: addons,
|
||||
isLoading: isAddonsLoading,
|
||||
error: addonsError,
|
||||
isSuccess: isAddonsSuccess,
|
||||
} = useProjectAddonsQuery({ projectRef: project?.ref })
|
||||
|
||||
const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', {
|
||||
resource: {
|
||||
@@ -136,10 +150,26 @@ export const DiskManagementReviewAndSubmitDialog = ({
|
||||
})
|
||||
|
||||
const planId = subscription?.plan.id ?? ''
|
||||
const isDirty = Object.keys(form.formState.dirtyFields).length > 0
|
||||
|
||||
const isDirty = !!Object.keys(form.formState.dirtyFields).length
|
||||
const replicaTooltipText = `Price change includes primary database and ${numReplicas} replica${numReplicas > 1 ? 's' : ''}`
|
||||
|
||||
/**
|
||||
* Handle compute instances
|
||||
*/
|
||||
const availableAddons = useMemo(() => {
|
||||
return addons?.available_addons ?? []
|
||||
}, [addons])
|
||||
|
||||
const availableOptions = useMemo(() => {
|
||||
return getAvailableComputeOptions(availableAddons, project?.cloud_provider)
|
||||
}, [availableAddons, project?.cloud_provider])
|
||||
|
||||
const computeSizePrice = calculateComputeSizePrice({
|
||||
availableOptions,
|
||||
oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro',
|
||||
newComputeSize: form.getValues('computeSize'),
|
||||
})
|
||||
|
||||
const diskSizePrice = calculateDiskSizePrice({
|
||||
planId,
|
||||
oldSize: form.formState.defaultValues?.totalSize || 0,
|
||||
@@ -190,7 +220,7 @@ export const DiskManagementReviewAndSubmitDialog = ({
|
||||
Review changes
|
||||
</ButtonTooltip>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="large">
|
||||
<DialogContent className="min-w-[620px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Review changes</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -204,6 +234,14 @@ export const DiskManagementReviewAndSubmitDialog = ({
|
||||
<TableHeaderRow />
|
||||
</TableHeader>
|
||||
<TableBody className="[&_td]:py-0 [&_tr]:h-[50px] [&_tr]:border-dotted">
|
||||
<TableDataRow
|
||||
attribute="Compute size"
|
||||
defaultValue={form.formState.defaultValues?.computeSize ?? ''}
|
||||
newValue={form.getValues('computeSize')}
|
||||
unit="-"
|
||||
beforePrice={computeSizePrice.oldPrice}
|
||||
afterPrice={computeSizePrice.newPrice}
|
||||
/>
|
||||
<TableDataRow
|
||||
hidePrice
|
||||
attribute="Storage Type"
|
||||
|
||||
@@ -149,7 +149,7 @@ const ProjectLayout = ({
|
||||
/>
|
||||
<ResizablePanel id="panel-right" className="h-full flex flex-col">
|
||||
{!navLayoutV2 && !hideHeader && IS_PLATFORM && <LayoutHeader />}
|
||||
<main className="h-full flex flex-col flex-1 w-full overflow-x-hidden">
|
||||
<main className="h-screen flex flex-col flex-1 w-full overflow-x-hidden">
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
const maxWidthClasses = 'mx-auto w-full max-w-[1200px]'
|
||||
const paddingClasses = 'px-6 lg:px-14 xl:px-24 2xl:px-32'
|
||||
const maxWidthClassesColumn = 'min-w-[420px]'
|
||||
export const MAX_WIDTH_CLASSES = 'mx-auto w-full max-w-[1200px]'
|
||||
export const PADDING_CLASSES = 'px-6 lg:px-14 xl:px-24 2xl:px-32'
|
||||
export const MAX_WIDTH_CLASSES_COLUMN = 'min-w-[420px]'
|
||||
|
||||
const ScaffoldHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
@@ -39,7 +39,7 @@ const ScaffoldContainer = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cn(maxWidthClasses, paddingClasses, bottomPadding && 'pb-16', className)}
|
||||
className={cn(MAX_WIDTH_CLASSES, PADDING_CLASSES, bottomPadding && 'pb-16', className)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -68,7 +68,7 @@ const ScaffoldColumn = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTM
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cn('flex flex-col gap-3', maxWidthClassesColumn, className)}
|
||||
className={cn('flex flex-col gap-3', MAX_WIDTH_CLASSES_COLUMN, className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -131,7 +131,7 @@ const ScaffoldContainerLegacy = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cn(maxWidthClasses, paddingClasses, 'my-8 flex flex-col gap-8', className)}
|
||||
className={cn(MAX_WIDTH_CLASSES, PADDING_CLASSES, 'my-8 flex flex-col gap-8', className)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -35,11 +35,8 @@ const AuthSettings: NextPageWithLayout = () => {
|
||||
</ScaffoldDescription>
|
||||
</ScaffoldHeader>
|
||||
</ScaffoldContainer>
|
||||
<ScaffoldContainer className="flex flex-col gap-10" bottomPadding>
|
||||
{/* {showNewDiskManagementUI ? <DiskManagementForm /> : null} */}
|
||||
<Separator />
|
||||
<DiskManagementForm />
|
||||
</ScaffoldContainer>
|
||||
|
||||
<DiskManagementForm />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user