import { useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { AlertTriangle, Gauge, Info } from 'lucide-react'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useBuckets, useDeleteBucket, useUpdateBucketQuotas } from '@/hooks/useApi'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { EmptyState } from '@/components/ui/empty-state'; import { DangerousConfirmDialog } from '@/components/ui/dangerous-confirm-dialog'; import { Switch } from '@/components/ui/switch'; import { Input } from '@/components/ui/input'; import { Select, SelectOption } from '@/components/ui/select'; import { formatBytes } from '@/lib/file-utils'; import { formatDate as formatDateUtil } from '@/lib/utils'; import { bytesToQuotaValue, quotaValueToBytes, QUOTA_UNIT_BYTES, type QuotaUnit, } from '@/lib/quota-utils'; const formatBytesOrDash = (n?: number) => (n == null ? '—' : formatBytes(n)); const formatDateOrDash = (iso?: string) => (iso ? formatDateUtil(iso) : '—'); const quotaFormSchema = z .object({ maxSizeEnabled: z.boolean(), maxSizeValue: z.string(), maxSizeUnit: z.enum(['MB', 'GB', 'TB']), maxObjectsEnabled: z.boolean(), maxObjectsValue: z.string(), }) .superRefine((data, ctx) => { if (data.maxSizeEnabled) { const n = Number(data.maxSizeValue); if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['maxSizeValue'], message: 'Enter a positive whole number', }); } } if (data.maxObjectsEnabled) { const n = Number(data.maxObjectsValue); if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['maxObjectsValue'], message: 'Enter a positive whole number', }); } } }); type QuotaFormValues = z.infer; function deriveDefaults(quotas: { maxSize?: number; maxObjects?: number } | null | undefined): QuotaFormValues { const size = quotas?.maxSize; const objects = quotas?.maxObjects; if (size != null) { const { value, unit } = bytesToQuotaValue(size); return { maxSizeEnabled: true, maxSizeValue: String(value), maxSizeUnit: unit, maxObjectsEnabled: objects != null, maxObjectsValue: objects != null ? String(objects) : '', }; } return { maxSizeEnabled: false, maxSizeValue: '', maxSizeUnit: 'GB', maxObjectsEnabled: objects != null, maxObjectsValue: objects != null ? String(objects) : '', }; } export function BucketSettings() { const { bucketName = '' } = useParams<{ bucketName: string }>(); const navigate = useNavigate(); const { data: buckets = [], isLoading } = useBuckets(); const bucket = buckets.find((b) => b.name === bucketName); const deleteMutation = useDeleteBucket(); const updateQuotasMutation = useUpdateBucketQuotas(); const [deleteOpen, setDeleteOpen] = useState(false); const [deleting, setDeleting] = useState(false); const defaults = useMemo(() => deriveDefaults(bucket?.quotas), [bucket?.quotas]); const { control, register, handleSubmit, watch, reset, formState: { errors, isDirty, isSubmitting }, } = useForm({ resolver: zodResolver(quotaFormSchema), values: defaults, }); const watched = watch(); const currentSize = bucket?.size ?? 0; const currentObjects = bucket?.objectCount ?? 0; const newMaxSizeBytes = watched.maxSizeEnabled && watched.maxSizeValue !== '' && !Number.isNaN(Number(watched.maxSizeValue)) ? quotaValueToBytes(Number(watched.maxSizeValue), watched.maxSizeUnit) : null; const newMaxObjects = watched.maxObjectsEnabled && watched.maxObjectsValue !== '' && !Number.isNaN(Number(watched.maxObjectsValue)) ? Number(watched.maxObjectsValue) : null; const sizeBelowCurrent = newMaxSizeBytes !== null && bucket?.size != null && newMaxSizeBytes < currentSize; const objectsBelowCurrent = newMaxObjects !== null && bucket?.objectCount != null && newMaxObjects < currentObjects; if (isLoading) { return
Loading…
; } if (!bucket) { return (
} tone="neutral" title="Bucket not found" description="The bucket you're looking for doesn't exist or you don't have access." />
); } const confirmDelete = async () => { setDeleting(true); try { await deleteMutation.mutateAsync(bucket.name); navigate('/buckets'); } catch { setDeleting(false); } }; const onSubmit = handleSubmit(async (values) => { const maxSize = values.maxSizeEnabled ? quotaValueToBytes(Number(values.maxSizeValue), values.maxSizeUnit) : null; const maxObjects = values.maxObjectsEnabled ? Number(values.maxObjectsValue) : null; await updateQuotasMutation.mutateAsync({ bucketName: bucket.name, maxSize, maxObjects }); }); return (
{/* Info */}

Bucket info

{bucket.name}} /> {bucket.websiteAccess ? 'Enabled' : 'Disabled'} } />
{/* Quotas */}

Quotas

{/* Max size row */}
( )} /> ( )} />

Current: {formatBytesOrDash(bucket.size)}

{errors.maxSizeValue && (

{errors.maxSizeValue.message}

)} {sizeBelowCurrent && (

Current size ({formatBytes(currentSize)}) exceeds this limit. New writes will be rejected.

)}
{/* Max objects row */}
( )} />

Current: {bucket.objectCount != null ? bucket.objectCount.toLocaleString() : '—'}

{errors.maxObjectsValue && (

{errors.maxObjectsValue.message}

)} {objectsBelowCurrent && (

Current object count ({currentObjects.toLocaleString()}) exceeds this limit. New writes will be rejected.

)}
{/* Danger zone */}

Danger zone

Destructive actions for this bucket.

Delete bucket
All objects in this bucket will be permanently removed.
{ if (!o && !deleting) setDeleteOpen(false); }} title={`Delete bucket "${bucket.name}"?`} description="This action cannot be undone." confirmationText={bucket.name} confirmLabel="Delete bucket" loading={deleting} onConfirm={confirmDelete} />
); } function Field({ label, value }: { label: string; value: React.ReactNode }) { return (
{label}
{value}
); }