feat(storage): make version list mocks adapt to bucket policy

- Remove the 'Restoring is non-destructive' admonition from the
  bottom of the Versions tab
- Refactor getMockObjectVersions to accept the bucket's current
  cap and expiryDays and adapt its output so version-list labels
  are always consistent with the policy:
    * Never returns a version older than the retention window
      (no 'Past Nd limit' surprises when the user set fewer days)
    * Never returns more noncurrent versions than the cap
      (no '#3 of 2' when the user tightened the cap)
- Anchor all version timestamps to the real clock via
  daysAgoFromNow so the mock stays fresh regardless of when the
  prototype is opened, instead of drifting away from the fixed
  BASE_DATE
- Wire useObjectVersionsQuery to look up the bucket's protection
  and pass it into the mock on every fetch, so policy edits from
  the bucket modal reflect in the panel next time it opens

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
This commit is contained in:
Claude committed 2026-08-11 14:40:45 +00:00
1 parent b3d683628e
commit eeeae037da
3 files changed
+82 -34

No files matched your search

@@ -245,13 +245,6 @@ export const VersionHistory = ({
</ol>
)}
<Admonition
showIcon={false}
type="default"
title="Restoring is non-destructive"
description="Restoring makes an older version the new current version. The previous current version becomes a noncurrent version you can still recover."
/>
<ConfirmationModal
variant="destructive"
visible={versionToDelete !== undefined}
@@ -1,6 +1,7 @@
import { useMutation, UseMutationOptions, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { getMockBucketProtection } from '@/components/interfaces/Storage/StorageProtection.constants'
import { storageKeys } from '../keys'
import { getMockObjectVersions, mockDelay, type ObjectVersion } from './protection-mocks'
@@ -17,7 +18,19 @@ export const useObjectVersionsQuery = ({
}: ObjectVersionsVariables) =>
useQuery<ObjectVersion[], Error>({
queryKey: storageKeys.objectVersions(projectRef, bucketId, objectName),
queryFn: () => mockDelay(getMockObjectVersions(objectName ?? '')),
// The mock adapts the returned versions to the bucket's current lifecycle
// policy so the indicator labels (age countdowns, cap position) never
// contradict the settings. Reading protection inside queryFn keeps it
// fresh across policy edits without needing to include it in the key.
queryFn: () => {
const protection = getMockBucketProtection(bucketId)
return mockDelay(
getMockObjectVersions(objectName ?? '', {
cap: protection.maxNoncurrentVersions,
expiryDays: protection.versionExpiryDays,
})
)
},
enabled: !!projectRef && !!bucketId && !!objectName,
})
@@ -71,36 +71,78 @@ const daysAgoFromNow = (days: number) => {
return date.toISOString()
}
export const getMockObjectVersions = (_objectName: string): ObjectVersion[] => [
{
/**
* Options that let the mock adapt its version list to the bucket's current
* lifecycle policy, so the labels rendered against it always make sense in
* context — e.g. never returning a version older than the retention window
* (which would render as "Past Nd limit") and never returning more than the
* configured cap (which would render as "#3 of 2").
*
* `null`/omitted means "no limit for that condition" — matches the shape of
* `BucketProtection.versionExpiryDays` / `maxNoncurrentVersions`.
*/
export interface MockObjectVersionsOptions {
cap?: number | null
expiryDays?: number | null
}
/**
* Noncurrent version templates, newest first. Ages are picked to spread across
* common retention windows so at least one shows a near-expiry countdown under
* typical (30d) settings, while tighter policies still leave the freshest
* couple retained.
*/
const NONCURRENT_VERSION_TEMPLATES: Array<{
versionId: string
size: number
daysAgo: number
action: ObjectVersionAction
}> = [
{ versionId: '2b7d9153aa9e', size: 790 * KB, daysAgo: 1, action: 'overwrite' },
{ versionId: 'a19c04f7de40', size: 760 * KB, daysAgo: 3, action: 'overwrite' },
{ versionId: '5c0278b3ac7a', size: 744 * KB, daysAgo: 8, action: 'overwrite' },
{ versionId: '9f4e1a2b8c3d', size: 720 * KB, daysAgo: 20, action: 'overwrite' },
{ versionId: '6a1b8d2f5c47', size: 705 * KB, daysAgo: 27, action: 'overwrite' },
{ versionId: '3e7c2b91da85', size: 680 * KB, daysAgo: 60, action: 'initial upload' },
]
export const getMockObjectVersions = (
_objectName: string,
options: MockObjectVersionsOptions = {}
): ObjectVersion[] => {
const cap = options.cap ?? null
const expiryDays = options.expiryDays ?? null
const hasCap = cap !== null && cap > 0
const hasExpiryDays = expiryDays !== null && expiryDays > 0
const current: ObjectVersion = {
versionId: '8f3a2c9b41c1',
size: 812 * KB,
createdAt: BASE_DATE,
createdAt: daysAgoFromNow(0),
isCurrent: true,
action: 'overwrite',
},
{
versionId: '2b7d9153aa9e',
size: 790 * KB,
createdAt: daysAgo(4, '18:02:00'),
isCurrent: false,
action: 'overwrite',
},
{
versionId: 'a19c04f7de40',
size: 760 * KB,
createdAt: daysAgo(10, '11:40:00'),
isCurrent: false,
action: 'overwrite',
},
{
versionId: '5c0278b3ac7a',
size: 744 * KB,
createdAt: daysAgo(22, '08:20:00'),
isCurrent: false,
action: 'initial upload',
},
]
}
let noncurrent = NONCURRENT_VERSION_TEMPLATES
// Retention: drop templates whose age would render as "Past Nd limit". Keep
// a 1-day buffer so time-of-day rounding in the indicator can't push a
// surviving version over the edge.
if (hasExpiryDays) noncurrent = noncurrent.filter((v) => v.daysAgo < expiryDays - 1)
// Cap: never return more than the configured maximum so the "#N of cap"
// label is always valid.
if (hasCap) noncurrent = noncurrent.slice(0, cap)
return [
current,
...noncurrent.map((v) => ({
versionId: v.versionId,
size: v.size,
createdAt: daysAgoFromNow(v.daysAgo),
isCurrent: false,
action: v.action,
})),
]
}
/**
* Mutable in-memory "deleted files" store. Shared across buckets, mirroring