inline errors

This commit is contained in:
Jonathan Summers-Muir committed 2024-10-29 17:12:54 +08:00
1 parent cd2b4bc6e6
commit b85dda5e69
10 files changed
+166 -102

No files matched your search

@@ -1,5 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { MAX_WIDTH_CLASSES, PADDING_CLASSES, ScaffoldContainer } from 'components/layouts/Scaffold'
@@ -9,14 +10,15 @@ import {
} from 'data/config/disk-attributes-query'
import { useUpdateDiskAttributesMutation } from 'data/config/disk-attributes-update-mutation'
import { useDiskUtilizationQuery } from 'data/config/disk-utilization-query'
import { setProjectStatus } from 'data/projects/projects-query'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { AnimatePresence, motion } from 'framer-motion'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useFlag } from 'hooks/ui/useFlag'
import { PROJECT_STATUS } from 'lib/constants'
import { ChevronRight } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
@@ -32,7 +34,7 @@ import {
} from 'ui'
import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge'
import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagement.schema'
import { formatComputeName } from './DiskManagement.utils'
import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection'
import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog'
import { ComputeSizeField } from './fields/ComputeSizeField'
import { DiskSizeField } from './fields/DiskSizeField'
@@ -47,10 +49,6 @@ import {
} from './ui/DiskManagement.constants'
import { DiskManagementPlanUpgradeRequired } from './ui/DiskManagementPlanUpgradeRequired'
import { NoticeBar } from './ui/NoticeBar'
import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection'
import { useQueryClient } from '@tanstack/react-query'
import { PROJECT_STATUS } from 'lib/constants'
import { setProjectStatus } from 'data/projects/projects-query'
export function DiskManagementForm() {
const { project } = useProjectContext()
@@ -63,8 +61,12 @@ export function DiskManagementForm() {
message: string
type: 'error' | 'success'
} | null>(null)
const [showRestartPending, setShowRestartPendingState] = useState(false)
const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false)
/**
* Permissions
*/
const isPermissionsLoaded = usePermissionsLoaded()
const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', {
resource: {
project_id: project?.id,
@@ -208,6 +210,11 @@ export function DiskManagementForm() {
}
}, [isSuccess])
const isDirty = !!Object.keys(form.formState.dirtyFields).length
const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING
const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing
const noPermissions = isPermissionsLoaded && !canUpdateDiskConfiguration
const { mutateAsync: updateDiskConfiguration, isLoading: isUpdatingDisk } =
useUpdateDiskAttributesMutation({})
const { mutateAsync: updateSubscriptionAddon, isLoading: isUpdatingCompute } =
@@ -298,13 +305,9 @@ export function DiskManagementForm() {
// return <></>
const isDirty = !!Object.keys(form.formState.dirtyFields).length
const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING
const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing
return (
<>
{isProjectResizing || isProjectRequestingDiskChanges ? (
{isProjectResizing || isProjectRequestingDiskChanges || noPermissions ? (
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
<DiskMangementRestartRequiredSection
visible={isProjectResizing}
@@ -316,6 +319,11 @@ export function DiskManagementForm() {
title="Disk configuration changes have been requested"
description="The requested changes will be applied to your disk shortly"
/>
<NoticeBar
visible={noPermissions}
title="You do not have permission to update disk configuration"
description="Please contact your organization administrator to update your disk configuration"
/>
</ScaffoldContainer>
) : null}
<Form_Shadcn_ {...form}>
@@ -331,7 +339,8 @@ export function DiskManagementForm() {
<Collapsible_Shadcn_
// TO DO: wrap component into pattern
className="-space-y-px"
open={advancedSettingsOpen}
// open={advancedSettingsOpen}
defaultOpen
onOpenChange={() => setAdvancedSettingsOpenState((prev) => !prev)}
>
<CollapsibleTrigger_Shadcn_ className="px-8 py-3 w-full border flex items-center gap-6 rounded-t data-[state=closed]:rounded-b group justify-between">
@@ -382,6 +391,7 @@ export function DiskManagementForm() {
</Button>
<DiskManagementReviewAndSubmitDialog
loading={isUpdatingConfig}
disabled={noPermissions}
form={form}
numReplicas={readReplicas.length}
isDialogOpen={isDialogOpen}
@@ -686,13 +686,7 @@ export function DiskManagementPanelForm() {
{/* Additional content or information can go here */}
</div>
<div className="col-span-8 space-y-6 mt-6">
<DiskSpaceBar
form={form}
// showNewBar={form.formState.dirtyFields.totalSize !== undefined}
// totalSize={size_gb}
// usedSize={mainDiskUsed}
// newTotalSize={watchedTotalSize}
/>
<DiskSpaceBar form={form} />
<DiskManagementDiskSizeReadReplicas
isDirty={form.formState.dirtyFields.totalSize !== undefined}
totalSize={size_gb * 1.25}
@@ -12,6 +12,7 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import { calculateComputeSizePrice, getAvailableComputeOptions } from '../DiskManagement.utils'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import FormMessage from '../ui/FormMessage'
type ComputeOption = {
identifier: components['schemas']['AddonVariantId']
@@ -37,7 +38,6 @@ export function ComputeSizeField({ form }: ComputeSizeFieldProps) {
data: addons,
isLoading: isAddonsLoading,
error: addonsError,
isSuccess: isAddonsSuccess,
} = useProjectAddonsQuery({ projectRef: ref })
const availableAddons = useMemo(() => {
@@ -81,7 +81,7 @@ export function ComputeSizeField({ form }: ComputeSizeFieldProps) {
}
>
<RadioGroupCard
className="grid grid-cols-2 xl:grid-cols-3 flex-wrap gap-3"
className={!addonsError ? 'grid grid-cols-2 xl:grid-cols-3 flex-wrap gap-3' : ''}
onValueChange={(value: components['schemas']['AddonVariantId']) => {
setValue('computeSize', value, {
shouldDirty: true,
@@ -99,7 +99,7 @@ export function ComputeSizeField({ form }: ComputeSizeFieldProps) {
.fill(0)
.map((_, i) => <Skeleton key={i} className="w-full h-[110px] rounded-md" />)
) : addonsError ? (
<p>Error loading compute options</p>
<FormMessage message={addonsError?.message} type="error" />
) : (
availableOptions.map((compute: ComputeOption) => {
// @ts-expect-error
@@ -16,6 +16,7 @@ import DiskSpaceBar from '../ui/DiskSpaceBar'
import { InputPostTab } from '../ui/InputPostTab'
import { InputResetButton } from '../ui/InputResetButton'
import FormMessage from '../ui/FormMessage'
import { useDiskAttributesQuery } from 'data/config/disk-attributes-query'
type DiskSizeFieldProps = {
form: UseFormReturn<DiskStorageSchemaType>
@@ -27,10 +28,16 @@ export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) {
const { control, formState, setValue, trigger, getValues, resetField, watch } = form
const org = useSelectedOrganization()
const {
isLoading: isLoadingDiskAttributes,
error: diskAttributesError,
isError: isDiskAttributesError,
} = useDiskAttributesQuery({ projectRef })
const {
data: subscription,
isLoading: isSubscriptionLoading,
error: subscriptionError,
isError: isSubscriptionError,
} = useOrgSubscriptionQuery({
orgSlug: org?.slug,
})
@@ -38,12 +45,14 @@ export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) {
data: diskUtil,
isLoading: isDiskUtilizationLoading,
error: diskUtilError,
isError: isDiskUtilizationError,
} = useDiskUtilizationQuery({
projectRef: projectRef,
})
const isLoading = isSubscriptionLoading || isDiskUtilizationLoading
const error = subscriptionError || diskUtilError
const isLoading = isSubscriptionLoading || isDiskUtilizationLoading || isLoadingDiskAttributes
const error = subscriptionError || diskUtilError || diskAttributesError
const isError = isSubscriptionError || isDiskUtilizationError || isDiskAttributesError
const watchedStorageType = watch('storageType')
const watchedTotalSize = watch('totalSize')
@@ -73,7 +82,6 @@ export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) {
control={control}
render={({ field }) => (
<FormItemLayout label="Disk Size" layout="vertical">
{error && <FormMessage message={error?.message} type="error" />}
<div className="relative flex gap-2 items-center">
<InputPostTab label="GB">
{isLoading ? (
@@ -90,7 +98,7 @@ export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) {
<Input_Shadcn_
type="number"
{...field}
disabled={disableInput}
disabled={disableInput || isError}
className="w-32 font-mono rounded-r-none"
onWheel={(e) => e.currentTarget.blur()}
onChange={(e) => {
@@ -136,13 +144,8 @@ export function DiskSizeField({ form, disableInput }: DiskSizeFieldProps) {
</div>
</div>
<div className="col-span-8">
<DiskSpaceBar
form={form}
// showNewBar={formState.dirtyFields.totalSize !== undefined}
// totalSize={formState.defaultValues?.totalSize || 0}
// usedSize={mainDiskUsed}
// newTotalSize={watchedTotalSize}
/>
<DiskSpaceBar form={form} />
{error && <FormMessage message={error?.message} type="error" />}
<DiskManagementDiskSizeReadReplicas
isDirty={formState.dirtyFields.totalSize !== undefined}
totalSize={(formState.defaultValues?.totalSize || 0) * 1.25}
@@ -1,6 +1,6 @@
import { components } from 'api-types'
import { UseFormReturn } from 'react-hook-form'
import { Button, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_ } from 'ui'
import { Button, cn, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_, Skeleton } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import { ComputeSizeReccomendationSection } from '../ui/ComputeSizeReccomendationSection'
@@ -14,6 +14,10 @@ import { calculateComputeSizeRequiredForIops, calculateIOPSPrice } from '../Disk
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import { DiskManagementIOPSReadReplicas } from '../ui/DiskManagementReadReplicas'
import { InputPostTab } from '../ui/InputPostTab'
import { useDiskAttributesQuery } from 'data/config/disk-attributes-query'
import { useParams } from 'common'
import { InputVariants } from '@ui/components/shadcn/ui/input'
import FormMessage from '../ui/FormMessage'
type IOPSFieldProps = {
form: UseFormReturn<DiskStorageSchemaType>
@@ -21,12 +25,15 @@ type IOPSFieldProps = {
}
export function IOPSField({ form, disableInput }: IOPSFieldProps) {
const { ref: projectRef } = useParams()
const { control, formState, setValue, trigger, getValues, watch } = form
const watchedStorageType = watch('storageType')
const watchedComputeSize = watch('computeSize')
const watchedIOPS = watch('provisionedIOPS') ?? 0
const { isLoading, error, isError } = useDiskAttributesQuery({ projectRef })
const iopsPrice = calculateIOPSPrice({
oldStorageType: formState.defaultValues?.storageType as DiskType,
oldProvisionedIOPS: formState.defaultValues?.provisionedIOPS || 0,
@@ -47,8 +54,9 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
<FormItemLayout
layout="horizontal"
label="IOPS"
id={field.name}
description={
<div className="flex flex-col gap-y-2">
<span className="flex flex-col gap-y-2">
<ComputeSizeReccomendationSection
computeSize={watchedComputeSize}
iops={watchedIOPS}
@@ -76,7 +84,7 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
newStorageType={getValues('storageType') as DiskType}
/>
)}
</div>
</span>
}
labelOptional={
<>
@@ -98,29 +106,37 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
}
>
<InputPostTab label="IOPS">
<FormControl_Shadcn_>
<Input_Shadcn_
id="provisionedIOPS"
type="number"
className="flex-grow font-mono rounded-r-none max-w-32"
{...field}
value={
disableIopsInput
? COMPUTE_BASELINE_IOPS[
watchedComputeSize as keyof typeof COMPUTE_BASELINE_IOPS
]
: field.value
}
disabled={disableInput || disableIopsInput}
onChange={(e) => {
setValue('provisionedIOPS', e.target.valueAsNumber, {
shouldDirty: true,
shouldValidate: true,
})
}}
/>
</FormControl_Shadcn_>
{isLoading ? (
<div
className={cn(InputVariants({ size: 'small' }), 'w-32 font-mono rounded-r-none')}
>
<Skeleton className="w-10 h-4" />
</div>
) : (
<FormControl_Shadcn_>
<Input_Shadcn_
type="number"
className="flex-grow font-mono rounded-r-none max-w-32"
{...field}
value={
disableIopsInput
? COMPUTE_BASELINE_IOPS[
watchedComputeSize as keyof typeof COMPUTE_BASELINE_IOPS
]
: field.value
}
disabled={disableInput || disableIopsInput || isError}
onChange={(e) => {
setValue('provisionedIOPS', e.target.valueAsNumber, {
shouldDirty: true,
shouldValidate: true,
})
}}
/>
</FormControl_Shadcn_>
)}
</InputPostTab>
{error && <FormMessage type="error" message={error.message} />}
</FormItemLayout>
)
}}
@@ -1,16 +1,23 @@
import { FormField_Shadcn_, FormControl_Shadcn_ } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { useParams } from 'common'
import { useDiskAttributesQuery } from 'data/config/disk-attributes-query'
import { UseFormReturn } from 'react-hook-form'
import {
Badge,
FormControl_Shadcn_,
FormField_Shadcn_,
Select_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Skeleton,
buttonVariants,
cn,
} from 'ui'
import { Badge } from 'ui'
import { UseFormReturn } from 'react-hook-form'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import { DISK_TYPE_OPTIONS } from '../ui/DiskManagement.constants'
import FormMessage from '../ui/FormMessage'
type StorageTypeFieldProps = {
form: UseFormReturn<DiskStorageSchemaType>
@@ -18,8 +25,11 @@ type StorageTypeFieldProps = {
}
export function StorageTypeField({ form, disableInput }: StorageTypeFieldProps) {
const { ref: projectRef } = useParams()
const { control, trigger } = form
const { isLoading, error, isError } = useDiskAttributesQuery({ projectRef })
return (
<FormField_Shadcn_
name="storageType"
@@ -36,13 +46,22 @@ export function StorageTypeField({ form, disableInput }: StorageTypeFieldProps)
trigger('provisionedIOPS')
}}
defaultValue={field.value}
disabled={disableInput}
disabled={disableInput || isError}
>
<FormControl_Shadcn_>
<SelectTrigger_Shadcn_ className="h-13 max-w-[420px]">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
</FormControl_Shadcn_>
{isLoading ? (
<Skeleton
className={cn(
buttonVariants({ size: 'small' }),
'h-14 min-w-[420px] duration-[2s]'
)}
/>
) : (
<FormControl_Shadcn_>
<SelectTrigger_Shadcn_ className="h-14 max-w-[420px]">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
</FormControl_Shadcn_>
)}
<SelectContent_Shadcn_>
<>
{DISK_TYPE_OPTIONS.map((item) => (
@@ -66,6 +85,7 @@ export function StorageTypeField({ form, disableInput }: StorageTypeFieldProps)
</>
</SelectContent_Shadcn_>
</Select_Shadcn_>
{error && <FormMessage type="error" message={error.message} />}
</FormItemLayout>
)}
/>
@@ -1,6 +1,6 @@
import { AnimatePresence, motion } from 'framer-motion'
import { UseFormReturn } from 'react-hook-form'
import { FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_ } from 'ui'
import { cn, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_, Skeleton } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import {
@@ -14,6 +14,10 @@ import { DiskStorageSchemaType } from '../DiskManagement.schema'
import { DiskManagementThroughputReadReplicas } from '../ui/DiskManagementReadReplicas'
import { InputPostTab } from '../ui/InputPostTab'
import { useEffect } from 'react'
import FormMessage from '../ui/FormMessage'
import { useDiskAttributesQuery } from 'data/config/disk-attributes-query'
import { useParams } from 'common'
import { InputVariants } from '@ui/components/shadcn/ui/input'
type ThroughputFieldProps = {
form: UseFormReturn<DiskStorageSchemaType>
@@ -21,6 +25,8 @@ type ThroughputFieldProps = {
}
export function ThroughputField({ form, disableInput }: ThroughputFieldProps) {
const { ref: projectRef } = useParams()
const { control, formState, setValue, getValues, watch } = form
const watchedStorageType = watch('storageType')
@@ -28,6 +34,8 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) {
const watchedComputeSize = watch('computeSize')
const throughput_mbps = formState.defaultValues?.throughput
const { isLoading, error } = useDiskAttributesQuery({ projectRef })
const throughputPrice = calculateThroughputPrice({
storageType: form.getValues('storageType') as DiskType,
newThroughput: form.getValues('throughput') || 0,
@@ -98,28 +106,40 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) {
}
>
<InputPostTab label="MB/s">
<FormControl_Shadcn_>
<Input_Shadcn_
type="number"
{...field}
value={
disableIopsInput
? COMPUTE_BASELINE_THROUGHPUT[
watchedComputeSize as keyof typeof COMPUTE_BASELINE_THROUGHPUT
]
: field.value
}
onChange={(e) => {
setValue('throughput', e.target.valueAsNumber, {
shouldDirty: true,
shouldValidate: true,
})
}}
className="flex-grow font-mono rounded-r-none max-w-32"
disabled={disableInput || disableIopsInput || watchedStorageType === 'io2'}
/>
</FormControl_Shadcn_>
{isLoading ? (
<div
className={cn(
InputVariants({ size: 'small' }),
'w-32 font-mono rounded-r-none'
)}
>
<Skeleton className="w-10 h-4" />
</div>
) : (
<FormControl_Shadcn_>
<Input_Shadcn_
type="number"
{...field}
value={
disableIopsInput
? COMPUTE_BASELINE_THROUGHPUT[
watchedComputeSize as keyof typeof COMPUTE_BASELINE_THROUGHPUT
]
: field.value
}
onChange={(e) => {
setValue('throughput', e.target.valueAsNumber, {
shouldDirty: true,
shouldValidate: true,
})
}}
className="flex-grow font-mono rounded-r-none max-w-32"
disabled={disableInput || disableIopsInput || watchedStorageType === 'io2'}
/>
</FormControl_Shadcn_>
)}
</InputPostTab>
{error && <FormMessage type="error" message={error.message} />}
</FormItemLayout>
)}
/>
@@ -24,7 +24,7 @@ export function ComputeSizeReccomendationSection({
iops > maxIOPSforComputeSize && !RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(computeSize)
return (
<AnimatePresence>
<AnimatePresence initial={false}>
{isVisible && (
<motion.div
initial={{ opacity: 0, height: 0 }}
@@ -154,7 +154,7 @@ export const DiskManagementIOPSReadReplicas = ({
if (readReplicas.length === 0) return null
return (
<AnimatePresence>
<AnimatePresence initial={false}>
{isDirty && (
<motion.div
initial={{ opacity: 0, height: 0 }}
@@ -217,7 +217,7 @@ export const DiskManagementThroughputReadReplicas = ({
if (readReplicas.length === 0) return null
return (
<AnimatePresence>
<AnimatePresence initial={false}>
{isDirty && (
<motion.div
initial={{ opacity: 0, height: 0 }}
@@ -1,4 +1,4 @@
import { Alert_Shadcn_, AlertDescription_Shadcn_, InfoIcon, WarningIcon } from 'ui'
import { Admonition } from 'ui-patterns'
interface FormMessageProps {
message: string
@@ -7,10 +7,11 @@ interface FormMessageProps {
function FormMessage({ message, type }: FormMessageProps) {
return (
<Alert_Shadcn_ variant={type === 'error' ? 'destructive' : 'default'} className="mt-2">
{type === 'error' ? <WarningIcon /> : <InfoIcon className="h-4 w-4" />}
<AlertDescription_Shadcn_>{message}</AlertDescription_Shadcn_>
</Alert_Shadcn_>
<Admonition
type={type === 'error' ? 'destructive' : 'default'}
className="mt-2"
title={message}
/>
)
}