From 0b8ef6dad2573b57cc6ccb2be8786d63d8ceffc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 11:45:22 +0000 Subject: [PATCH] feat(storage): drive version history off the real endpoints The preview panel now addresses versions by the object's full path, which is what the list, move and delete endpoints take, rather than by the leaf name the explorer renders. `VersionHistory` keeps `objectName` for copy and takes `path` separately. Expiry countdowns read the bucket's stored lifecycle policy instead of an empty placeholder, so a row's fate reflects the policy that governs it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn --- .../Storage/StorageExplorer/PreviewPane.tsx | 43 ++++++++++++------- .../StorageExplorer/VersionHistory.tsx | 10 +++-- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx index b75d7aae782..38c192e436d 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx @@ -24,6 +24,7 @@ import { } from 'ui' import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' +import { fromLifecycleRules } from '../BucketVersioningFields.lifecycle' import { URL_EXPIRY_DURATION } from '../Storage.constants' import { StorageItem } from '../Storage.types' import { getBucketVersioningState } from '../StorageVersioning.constants' @@ -36,6 +37,7 @@ import { VersionCompareWidget } from './VersionCompareWidget' import { VersionHistory } from './VersionHistory' import { useIsStorageVersioningEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { bucketLifecycleQueryOptions } from '@/data/storage/bucket-lifecycle-query' import { useObjectPurgeMutation } from '@/data/storage/versioning/object-purge-mutation' import { useObjectVersionRestoreMutation } from '@/data/storage/versioning/object-version-restore-mutation' import { @@ -52,12 +54,6 @@ const PREVIEW_SIZE_LIMIT = 10 * 1024 * 1024 // 10MB const PANEL_WIDTH = 450 -/** - * TODO(storage-versioning): read the bucket's stored policy once the Storage API - * returns it. No condition is set until then, so no row shows an expiry. - */ -const EMPTY_LIFECYCLE_POLICY: LifecyclePolicy = { expiryDays: null, maxVersions: null } - const PreviewFile = ({ item }: { item: StorageItem }) => { const { projectRef, selectedBucket, openedFolders } = useStorageExplorerStateSnapshot() const folderPath = getPathAlongOpenedFolders({ openedFolders, selectedBucket }, false) @@ -338,6 +334,7 @@ export const PreviewPane = () => { projectRef, selectedBucket, selectedFilePreview: file, + openedFolders, setSelectedItemsToDelete, setSelectedFileCustomExpiry, downloadFile, @@ -359,14 +356,29 @@ export const PreviewPane = () => { const versioningState = getBucketVersioningState(selectedBucket) + // The version endpoints address an object by its full path in the bucket, not + // by the leaf name the explorer renders. + const folderPath = getPathAlongOpenedFolders({ openedFolders, selectedBucket }, false) + const filePath = file ? [folderPath, file.name].filter(Boolean).join('/') : undefined + + const { data: lifecycle } = useQuery({ + ...bucketLifecycleQueryOptions({ projectRef, bucketId: selectedBucket?.id }), + enabled: isStorageVersioningEnabled && !!projectRef && !!selectedBucket?.id, + }) + const storedPolicy = fromLifecycleRules(lifecycle) + const lifecyclePolicy: LifecyclePolicy = { + expiryDays: storedPolicy.versionExpiryDays, + maxVersions: storedPolicy.maxNoncurrentVersions, + } + const { data: versions } = useQuery({ ...objectVersionsQueryOptions({ projectRef, bucketId: selectedBucket?.id, - objectName: file?.name, - lifecyclePolicy: EMPTY_LIFECYCLE_POLICY, + path: filePath, + lifecyclePolicy, }), - enabled: isStorageVersioningEnabled && !!projectRef && !!selectedBucket?.id && !!file?.name, + enabled: isStorageVersioningEnabled && !!projectRef && !!selectedBucket?.id && !!filePath, }) const { mutate: restoreVersion, isPending: isRestoring } = useObjectVersionRestoreMutation({ @@ -395,18 +407,18 @@ export const PreviewPane = () => { const isComparing = previewedVersion !== undefined && !previewedVersion.isCurrent const handleRestore = () => { - if (!projectRef || !selectedBucket?.id || !previewedVersion) return + if (!projectRef || !selectedBucket?.id || !previewedVersion || !filePath) return restoreVersion({ projectRef, bucketId: selectedBucket.id, - objectName: file.name, + path: filePath, versionId: previewedVersion.versionId, }) } const handlePurge = () => { - if (!projectRef || !selectedBucket?.id) return - purgeObject({ projectRef, bucketId: selectedBucket.id, objectName: file.name }) + if (!projectRef || !selectedBucket?.id || !filePath) return + purgeObject({ projectRef, bucketId: selectedBucket.id, path: filePath }) } // The compare widget replaces the top of the panel, so scroll up to show it. @@ -469,9 +481,10 @@ export const PreviewPane = () => { projectRef={projectRef} bucketId={selectedBucket?.id} objectName={file.name} + path={filePath ?? file.name} versioningState={versioningState} - lifecyclePolicy={EMPTY_LIFECYCLE_POLICY} - expirationMode="and" + lifecyclePolicy={lifecyclePolicy} + expirationMode={storedPolicy.expirationMode} mimeType={mimeType} previewedVersionId={previewedVersion?.versionId} onPreview={handlePreviewVersion} diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/VersionHistory.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/VersionHistory.tsx index a740877a7df..b814803789e 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/VersionHistory.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/VersionHistory.tsx @@ -127,7 +127,10 @@ const VersionActionsMenu = ({ interface VersionHistoryProps { projectRef?: string bucketId?: string + /** Leaf name, for copy. */ objectName: string + /** Full path within the bucket, which is what the version endpoints address. */ + path: string versioningState: BucketVersioningState lifecyclePolicy: LifecyclePolicy expirationMode: ExpirationMode @@ -142,6 +145,7 @@ export const VersionHistory = ({ projectRef, bucketId, objectName, + path, versioningState, lifecyclePolicy, expirationMode, @@ -157,7 +161,7 @@ export const VersionHistory = ({ isError, error, isSuccess, - } = useQuery(objectVersionsQueryOptions({ projectRef, bucketId, objectName, lifecyclePolicy })) + } = useQuery(objectVersionsQueryOptions({ projectRef, bucketId, path, lifecyclePolicy })) const [versionToDelete, setVersionToDelete] = useState() @@ -197,7 +201,7 @@ export const VersionHistory = ({ const handleRestore = (version: ObjectVersion) => { if (!projectRef || !bucketId) return - restoreVersion({ projectRef, bucketId, objectName, versionId: version.versionId }) + restoreVersion({ projectRef, bucketId, path, versionId: version.versionId }) } if (isPending) return @@ -325,7 +329,7 @@ export const VersionHistory = ({ deleteVersion({ projectRef, bucketId, - objectName, + path, versionId: versionToDelete.versionId, }) }}