From eeeae037da0bc4e62a982e028cd7688335bc364d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 14:40:45 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p --- .../StorageExplorer/VersionHistory.tsx | 7 -- .../protection/object-versions-query.ts | 15 ++- .../storage/protection/protection-mocks.ts | 94 ++++++++++++++----- 3 files changed, 82 insertions(+), 34 deletions(-) diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/VersionHistory.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/VersionHistory.tsx index 7c0739b8898..6efdeae5acc 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/VersionHistory.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/VersionHistory.tsx @@ -245,13 +245,6 @@ export const VersionHistory = ({ )} - - useQuery({ 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, }) diff --git a/apps/studio/data/storage/protection/protection-mocks.ts b/apps/studio/data/storage/protection/protection-mocks.ts index 36ea0994b59..47b349c4bb0 100644 --- a/apps/studio/data/storage/protection/protection-mocks.ts +++ b/apps/studio/data/storage/protection/protection-mocks.ts @@ -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