Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 89 additions & 20 deletions components/buckets/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ export function BucketInfo({ bucketName }: BucketInfoProps) {
const [loading, setLoading] = React.useState(true)
const [loadedBucket, setLoadedBucket] = React.useState<string | null>(null)
const [readErrors, setReadErrors] = React.useState<Partial<Record<BucketSettingKey, string>>>({})
const [versionLoading, setVersionLoading] = React.useState(false)
const [pendingVersioning, setPendingVersioning] = React.useState<boolean | null>(null)
const [versioningError, setVersioningError] = React.useState("")
const versioningSwitchRef = React.useRef<HTMLButtonElement | null>(null)
const [mutation, setMutation] = React.useState<BucketMutation | null>(null)
const requestVersionRef = React.useRef(0)
const mutationRef = React.useRef<BucketMutation | null>(null)
Expand Down Expand Up @@ -300,6 +302,8 @@ export function BucketInfo({ bucketName }: BucketInfoProps) {
setShowQuotaModal(false)
setShowRetentionModal(false)
setDeleteTagIndex(null)
setPendingVersioning(null)
setVersioningError("")
mutationRef.current = null
setMutation(null)
fetchData()
Expand Down Expand Up @@ -572,21 +576,36 @@ export function BucketInfo({ bucketName }: BucketInfoProps) {
}
}

const handleVersionToggle = async (enabled: boolean) => {
if (!canEditVersioning || readErrors.versioning || readErrors.objectLock || loading) return
const versioningDisabled =
!canEditVersioning ||
Boolean(readErrors.versioning || readErrors.objectLock) ||
Boolean(objectLock) ||
loading ||
mutation !== null

const handleVersionToggle = (enabled: boolean) => {
if (versioningDisabled || enabled === (versioning === "Enabled")) return
setVersioningError("")
setPendingVersioning(enabled)
}

const confirmVersioning = async () => {
if (pendingVersioning === null || versioningDisabled) return
if (!beginMutation("versioning")) return
setVersionLoading(true)
const prev = versioning
const requestVersion = requestVersionRef.current
const next = pendingVersioning ? "Enabled" : "Suspended"
setVersioningError("")
try {
await bucketApi.putBucketVersioning(bucketName, enabled ? "Enabled" : "Suspended")
setVersioning(enabled ? "Enabled" : "Suspended")
await bucketApi.putBucketVersioning(bucketName, next)
if (requestVersion !== requestVersionRef.current) return
setVersioning(next)
setPendingVersioning(null)
message.success(t("Edit Success"))
} catch (err) {
setVersioning(prev)
message.error(`${t("Edit Failed")}: ${(err as Error).message}`)
if (requestVersion !== requestVersionRef.current) return
setVersioningError(`${t("Edit Failed")}: ${(err as Error).message}`)
} finally {
setVersionLoading(false)
finishMutation()
if (requestVersion === requestVersionRef.current) finishMutation()
}
}

Expand Down Expand Up @@ -841,26 +860,29 @@ export function BucketInfo({ bucketName }: BucketInfoProps) {
? t("Versioning stays enabled while Object Lock is active.")
: t("Keep previous object versions and recover from overwrites or deletes.")
}
status={readErrors.versioning ? t("Unavailable") : versioning === "Enabled" ? t("Enabled") : t("Disabled")}
status={
readErrors.versioning
? t("Unavailable")
: versioning === "Enabled"
? t("Enabled")
: versioning === "Suspended"
? t("Suspended")
: t("Not Configured")
}
statusVariant={readErrors.versioning ? "destructive" : versioning === "Enabled" ? "secondary" : "outline"}
action={
canEditVersioning ? (
<div className="flex min-h-9 items-center gap-2">
<Switch
id="bucket-info-versioning"
ref={versioningSwitchRef}
checked={versioning === "Enabled"}
disabled={
Boolean(readErrors.versioning || readErrors.objectLock) ||
(objectLock ?? false) ||
versionLoading ||
loading ||
mutation !== null
}
disabled={versioningDisabled}
onCheckedChange={handleVersionToggle}
aria-label={t("Versioning")}
aria-describedby="bucket-info-versioning-description"
/>
{versionLoading ? (
{mutation === "versioning" ? (
<Spinner className="size-4 text-muted-foreground" aria-label={t("Loading")} />
) : null}
</div>
Expand Down Expand Up @@ -1571,6 +1593,53 @@ export function BucketInfo({ bucketName }: BucketInfoProps) {
</DialogContent>
</Dialog>

<AlertDialog
open={pendingVersioning !== null}
onOpenChange={(open) => {
if (!open && !mutationRef.current) setPendingVersioning(null)
}}
>
<AlertDialogContent finalFocus={versioningSwitchRef} className="max-h-[calc(100dvh-2rem)] overflow-y-auto">
<AlertDialogHeader>
<AlertDialogTitle>{pendingVersioning ? t("Enable Versioning") : t("Suspend Versioning")}</AlertDialogTitle>
<AlertDialogDescription className="space-y-3" render={<div />}>
<p className="font-medium text-foreground [overflow-wrap:anywhere]">
{pendingVersioning
? t("Enable versioning for bucket {bucket}?", { bucket: bucketName })
: t("Suspend versioning for bucket {bucket}?", { bucket: bucketName })}
</p>
<p>
{pendingVersioning
? t(
"Once enabled, versioning cannot be disabled or reset to an unconfigured state. It can only be suspended. Existing versions are preserved.",
)
: t(
"Suspending versioning stops creating new versions for future writes. Existing version IDs are preserved, but future writes can overwrite objects with a null version ID.",
)}
</p>
</AlertDialogDescription>
</AlertDialogHeader>
{versioningError ? (
<p role="alert" className="text-sm text-destructive [overflow-wrap:anywhere]">
{versioningError}
</p>
) : null}
<AlertDialogFooter>
<AlertDialogCancel disabled={mutation === "versioning"} className="min-h-11 sm:min-h-10">
{t("Cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmVersioning}
disabled={versioningDisabled}
className="min-h-11 sm:min-h-10"
>
{mutation === "versioning" ? <Spinner className="size-4" aria-label={t("Loading")} /> : null}
{pendingVersioning ? t("Enable Versioning") : t("Suspend Versioning")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

{/* Delete Tag Confirm */}
<AlertDialog
open={deleteTagIndex !== null}
Expand Down
12 changes: 11 additions & 1 deletion components/buckets/new-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Field, FieldContent, FieldError, FieldLabel } from "@/components/ui/field"
import { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import {
Expand Down Expand Up @@ -180,6 +180,7 @@ export function BucketNewForm({ show, onShowChange }: BucketNewFormProps) {
<FieldContent className="flex-none">
<Switch
id="bucket-new-version"
aria-describedby="bucket-new-version-description"
checked={version}
onCheckedChange={(checked) => {
setVersion(checked)
Expand All @@ -189,11 +190,17 @@ export function BucketNewForm({ show, onShowChange }: BucketNewFormProps) {
/>
</FieldContent>
</Field>
<FieldDescription id="bucket-new-version-description">
{t(
"Once enabled, versioning cannot be disabled or reset to an unconfigured state. It can only be suspended. Existing versions are preserved.",
)}
</FieldDescription>
<Field orientation="horizontal" className="items-center justify-between">
<FieldLabel htmlFor="bucket-new-object-lock">{t("Object Lock")}</FieldLabel>
<FieldContent className="flex-none">
<Switch
id="bucket-new-object-lock"
aria-describedby="bucket-new-object-lock-description"
checked={objectLock}
onCheckedChange={(checked) => {
setObjectLock(checked)
Expand All @@ -203,6 +210,9 @@ export function BucketNewForm({ show, onShowChange }: BucketNewFormProps) {
/>
</FieldContent>
</Field>
<FieldDescription id="bucket-new-object-lock-description">
{t("Object Lock cannot be disabled after bucket creation and automatically enables versioning.")}
</FieldDescription>
{objectLock && (
<fieldset className="space-y-4 border-t pt-5">
<legend className="sr-only">{t("Object Lock")}</legend>
Expand Down
1 change: 1 addition & 0 deletions docs/ui-review/register.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ The 2026-07-11 follow-up reviewed 44 candidate files containing cards, dialogs,
| UI-053 | Upload selection | File pickers were detached from the drop target, fixed height left excessive empty space, and instructions competed with the destination. | The next action and upload scope were harder to scan. | Center file selection in the empty drop target, size the queue to its contents, retain one destination field and footer, and preserve picker focus across selection changes. | `upload-redesign-53` | Resolved |
| UI-054 | Object refresh | Refresh was hidden in a header menu even when it was the only item. | Reloading the object list required an unnecessary extra click. | Show Refresh beside Upload; keep pagination controls with the list. | `object-refresh-54` | Resolved |
| UI-055 | Bucket distribution | Similar blue segments had no matching color legend or percentages, and the remainder was unlabeled. | Bucket shares were unclear, especially when one bucket held almost all data. | Use distinct chart colors with matching names, sizes and percentages; label other buckets and preserve tiny nonzero shares in text. | `bucket-distribution-55` | Resolved |
| UI-056 | Bucket versioning | Toggling versioning submitted immediately and labeled suspended buckets as disabled. | An accidental click could permanently configure versioning without explaining later overwrite risks. | Confirm the bucket and consequences before enabling or suspending; preserve state on cancel and failure, distinguish all three states, and explain irreversible creation settings. | `bucket-versioning-confirmation-56` | Resolved |

## Deferred design debt

Expand Down
10 changes: 9 additions & 1 deletion i18n/locales/ar-MA.json
Original file line number Diff line number Diff line change
Expand Up @@ -2313,5 +2313,13 @@
"Object cleanup is pending for this tier. Background recovery continues automatically.": "تنظيف الكائنات لهذه الطبقة معلق. يستمر الاسترداد التلقائي في الخلفية.",
"A transition is still pending for this tier. Wait for recovery, then recheck.": "لا يزال هناك نقل معلق لهذه الطبقة. انتظر الاسترداد ثم أعد التحقق.",
"The server could not complete the deletion safety check. Recheck before retrying deletion.": "لم يتمكن الخادم من إكمال فحص أمان الحذف. أعد التحقق قبل إعادة محاولة الحذف.",
"The server reported an unrecognized blocker. The original deletion error still applies.": "أبلغ الخادم عن عائق غير معروف. لا يزال خطأ الحذف الأصلي سارياً."
"The server reported an unrecognized blocker. The original deletion error still applies.": "أبلغ الخادم عن عائق غير معروف. لا يزال خطأ الحذف الأصلي سارياً.",
"Enable Versioning": "تفعيل التحكم في الإصدارات",
"Suspend Versioning": "تعليق التحكم في الإصدارات",
"Suspended": "معلّق",
"Enable versioning for bucket {bucket}?": "هل تريد تفعيل التحكم في الإصدارات للحاوية {bucket}؟",
"Suspend versioning for bucket {bucket}?": "هل تريد تعليق التحكم في الإصدارات للحاوية {bucket}؟",
"Once enabled, versioning cannot be disabled or reset to an unconfigured state. It can only be suspended. Existing versions are preserved.": "بعد تفعيل التحكم في الإصدارات، لا يمكن تعطيله أو إعادته إلى حالة غير مهيأة. يمكن تعليقه فقط. يتم الاحتفاظ بالإصدارات الموجودة.",
"Suspending versioning stops creating new versions for future writes. Existing version IDs are preserved, but future writes can overwrite objects with a null version ID.": "يؤدي تعليق التحكم في الإصدارات إلى إيقاف إنشاء إصدارات جديدة لعمليات الكتابة المستقبلية. يتم الاحتفاظ بمعرّفات الإصدارات الموجودة، لكن عمليات الكتابة المستقبلية قد تستبدل الكائنات التي يكون معرّف إصدارها null.",
"Object Lock cannot be disabled after bucket creation and automatically enables versioning.": "لا يمكن تعطيل قفل الكائن بعد إنشاء الحاوية، وهو يفعّل التحكم في الإصدارات تلقائياً."
}
10 changes: 9 additions & 1 deletion i18n/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -2313,5 +2313,13 @@
"Object cleanup is pending for this tier. Background recovery continues automatically.": "Für dieses Tier steht eine Objektbereinigung aus. Die Hintergrundwiederherstellung läuft automatisch weiter.",
"A transition is still pending for this tier. Wait for recovery, then recheck.": "Für dieses Tier steht noch eine Auslagerung aus. Wiederherstellung abwarten und erneut prüfen.",
"The server could not complete the deletion safety check. Recheck before retrying deletion.": "Der Server konnte die Sicherheitsprüfung zur Löschung nicht abschließen. Vor dem erneuten Löschen erneut prüfen.",
"The server reported an unrecognized blocker. The original deletion error still applies.": "Der Server hat ein unbekanntes Hindernis gemeldet. Der ursprüngliche Löschfehler gilt weiterhin."
"The server reported an unrecognized blocker. The original deletion error still applies.": "Der Server hat ein unbekanntes Hindernis gemeldet. Der ursprüngliche Löschfehler gilt weiterhin.",
"Enable Versioning": "Versionierung aktivieren",
"Suspend Versioning": "Versionierung aussetzen",
"Suspended": "Ausgesetzt",
"Enable versioning for bucket {bucket}?": "Versionierung für Bucket {bucket} aktivieren?",
"Suspend versioning for bucket {bucket}?": "Versionierung für Bucket {bucket} aussetzen?",
"Once enabled, versioning cannot be disabled or reset to an unconfigured state. It can only be suspended. Existing versions are preserved.": "Nach der Aktivierung kann die Versionierung weder deaktiviert noch auf den nicht konfigurierten Zustand zurückgesetzt werden. Sie kann nur ausgesetzt werden. Vorhandene Versionen bleiben erhalten.",
"Suspending versioning stops creating new versions for future writes. Existing version IDs are preserved, but future writes can overwrite objects with a null version ID.": "Wenn die Versionierung ausgesetzt wird, werden bei künftigen Schreibvorgängen keine neuen Versionen erstellt. Vorhandene Versions-IDs bleiben erhalten, aber künftige Schreibvorgänge können Objekte mit der Versions-ID null überschreiben.",
"Object Lock cannot be disabled after bucket creation and automatically enables versioning.": "Die Objektsperre kann nach dem Erstellen des Buckets nicht deaktiviert werden und aktiviert automatisch die Versionierung."
}
10 changes: 9 additions & 1 deletion i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2313,5 +2313,13 @@
"Object cleanup is pending for this tier. Background recovery continues automatically.": "Object cleanup is pending for this tier. Background recovery continues automatically.",
"A transition is still pending for this tier. Wait for recovery, then recheck.": "A transition is still pending for this tier. Wait for recovery, then recheck.",
"The server could not complete the deletion safety check. Recheck before retrying deletion.": "The server could not complete the deletion safety check. Recheck before retrying deletion.",
"The server reported an unrecognized blocker. The original deletion error still applies.": "The server reported an unrecognized blocker. The original deletion error still applies."
"The server reported an unrecognized blocker. The original deletion error still applies.": "The server reported an unrecognized blocker. The original deletion error still applies.",
"Enable Versioning": "Enable Versioning",
"Suspend Versioning": "Suspend Versioning",
"Suspended": "Suspended",
"Enable versioning for bucket {bucket}?": "Enable versioning for bucket {bucket}?",
"Suspend versioning for bucket {bucket}?": "Suspend versioning for bucket {bucket}?",
"Once enabled, versioning cannot be disabled or reset to an unconfigured state. It can only be suspended. Existing versions are preserved.": "Once enabled, versioning cannot be disabled or reset to an unconfigured state. It can only be suspended. Existing versions are preserved.",
"Suspending versioning stops creating new versions for future writes. Existing version IDs are preserved, but future writes can overwrite objects with a null version ID.": "Suspending versioning stops creating new versions for future writes. Existing version IDs are preserved, but future writes can overwrite objects with a null version ID.",
"Object Lock cannot be disabled after bucket creation and automatically enables versioning.": "Object Lock cannot be disabled after bucket creation and automatically enables versioning."
}
Loading