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