Feat/preserve url path on refresh (#54)

This commit is contained in:
Soham
2026-02-19 12:04:39 +05:30
committed by GitHub
parent fd976013a8
commit 3134444268
6 changed files with 166 additions and 53 deletions
+49 -5
View File
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { useRouter } from "next/navigation"
import { useRouter, useSearchParams } from "next/navigation"
import { useTranslation } from "react-i18next"
import { Page } from "@/components/page"
import { PageHeader } from "@/components/page-header"
@@ -17,11 +17,14 @@ import { buildBucketPath } from "@/lib/bucket-path"
interface BrowserContentProps {
bucketName: string
keyPath?: string
preview?: boolean
previewKey?: string
}
export function BrowserContent({ bucketName, keyPath = "" }: BrowserContentProps) {
export function BrowserContent({ bucketName, keyPath = "", preview = false, previewKey = "" }: BrowserContentProps) {
const { t } = useTranslation()
const router = useRouter()
const searchParams = useSearchParams()
const message = useMessage()
const { headBucket } = useBucket()
@@ -30,18 +33,29 @@ export function BrowserContent({ bucketName, keyPath = "" }: BrowserContentProps
const [infoOpen, setInfoOpen] = React.useState(false)
const [infoKey, setInfoKey] = React.useState<string | null>(null)
const [autoPreview, setAutoPreview] = React.useState(false)
const [uploadPickerOpen, setUploadPickerOpen] = React.useState(false)
const [refreshTrigger, setRefreshTrigger] = React.useState(0)
// Handle initial preview params - set infoOpen and trigger auto-preview
React.useEffect(() => {
if (preview && previewKey) {
setInfoKey(previewKey)
setInfoOpen(true)
setAutoPreview(true)
}
}, [preview, previewKey])
React.useEffect(() => {
if (!bucketName) return
headBucket(bucketName)
.then(() => {})
.catch(() => {
message.error(t("Bucket not found"))
router.push("/browser")
const params = new URLSearchParams(searchParams.toString())
router.push(`/browser?${params.toString()}`)
})
}, [bucketName, headBucket, message, router, t])
}, [bucketName, headBucket, message, router, t, searchParams])
const bucketPath = React.useCallback((path?: string | string[]) => buildBucketPath(bucketName, path), [bucketName])
@@ -54,6 +68,34 @@ export function BrowserContent({ bucketName, keyPath = "" }: BrowserContentProps
setInfoOpen(true)
}
const updatePreviewParams = (showPreview: boolean, key?: string) => {
const params = new URLSearchParams(searchParams.toString())
if (showPreview && key) {
params.set("preview", "true")
params.set("previewKey", key)
} else {
params.delete("preview")
params.delete("previewKey")
}
router.replace(`/browser?${params.toString()}`)
}
const handleInfoOpenChange = (open: boolean) => {
setInfoOpen(open)
if (!open) {
setAutoPreview(false)
updatePreviewParams(false)
}
}
const handlePreviewChange = (showPreview: boolean) => {
if (showPreview && infoKey) {
updatePreviewParams(true, infoKey)
} else {
updatePreviewParams(false)
}
}
const handleRefresh = () => {
setRefreshTrigger((n) => n + 1)
}
@@ -99,8 +141,10 @@ export function BrowserContent({ bucketName, keyPath = "" }: BrowserContentProps
bucketName={bucketName}
objectKey={infoKey}
open={infoOpen}
onOpenChange={setInfoOpen}
onOpenChange={handleInfoOpenChange}
onRefresh={handleRefresh}
autoPreview={autoPreview}
onPreviewChange={handlePreviewChange}
/>
<ObjectUploadPicker
+3 -2
View File
@@ -118,7 +118,6 @@ function BrowserBucketsPage() {
fetchId,
buckets.map((bucket) => bucket.Name),
)
} catch (error) {
if (fetchId !== fetchIdRef.current) return
console.error("Failed to fetch buckets:", error)
@@ -296,9 +295,11 @@ export default function BrowserPage() {
const searchParams = useSearchParams()
const bucketName = searchParams.get("bucket") ?? ""
const keyPath = searchParams.get("key") ?? ""
const preview = searchParams.get("preview") === "true"
const previewKey = searchParams.get("previewKey") ?? ""
if (bucketName) {
return <BrowserContent bucketName={bucketName} keyPath={keyPath} />
return <BrowserContent bucketName={bucketName} keyPath={keyPath} preview={preview} previewKey={previewKey} />
}
return <BrowserBucketsPage />
+37 -37
View File
@@ -32,49 +32,49 @@ export default function BucketSettingsPage() {
</PageHeader>
<Tabs defaultValue="overview" className="w-full space-y-6">
<TabsList className="inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground">
<TabsTrigger value="overview" className="px-4">
{t("Overview")}
</TabsTrigger>
{canViewLifecycle && (
<TabsTrigger value="lifecycle" className="px-4">
{t("Lifecycle")}
</TabsTrigger>
)}
{canViewReplication && (
<TabsTrigger value="replication" className="px-4">
{t("Replication")}
</TabsTrigger>
)}
{canViewEvents && (
<TabsTrigger value="events" className="px-4">
{t("Events")}
</TabsTrigger>
)}
</TabsList>
<TabsContent value="overview" className="space-y-4 outline-none">
{bucketName ? <BucketInfo bucketName={bucketName} /> : null}
</TabsContent>
<TabsList className="inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground">
<TabsTrigger value="overview" className="px-4">
{t("Overview")}
</TabsTrigger>
{canViewLifecycle && (
<TabsContent value="lifecycle" className="space-y-4 outline-none">
{bucketName ? <BucketLifecycleTab bucketName={bucketName} /> : null}
</TabsContent>
<TabsTrigger value="lifecycle" className="px-4">
{t("Lifecycle")}
</TabsTrigger>
)}
{canViewReplication && (
<TabsContent value="replication" className="space-y-4 outline-none">
{bucketName ? <BucketReplicationTab bucketName={bucketName} /> : null}
</TabsContent>
<TabsTrigger value="replication" className="px-4">
{t("Replication")}
</TabsTrigger>
)}
{canViewEvents && (
<TabsContent value="events" className="space-y-4 outline-none">
{bucketName ? <BucketEventsTab bucketName={bucketName} /> : null}
</TabsContent>
<TabsTrigger value="events" className="px-4">
{t("Events")}
</TabsTrigger>
)}
</Tabs>
</TabsList>
<TabsContent value="overview" className="space-y-4 outline-none">
{bucketName ? <BucketInfo bucketName={bucketName} /> : null}
</TabsContent>
{canViewLifecycle && (
<TabsContent value="lifecycle" className="space-y-4 outline-none">
{bucketName ? <BucketLifecycleTab bucketName={bucketName} /> : null}
</TabsContent>
)}
{canViewReplication && (
<TabsContent value="replication" className="space-y-4 outline-none">
{bucketName ? <BucketReplicationTab bucketName={bucketName} /> : null}
</TabsContent>
)}
{canViewEvents && (
<TabsContent value="events" className="space-y-4 outline-none">
{bucketName ? <BucketEventsTab bucketName={bucketName} /> : null}
</TabsContent>
)}
</Tabs>
</Page>
)
}
+34 -2
View File
@@ -35,9 +35,19 @@ interface ObjectInfoProps {
open: boolean
onOpenChange: (open: boolean) => void
onRefresh?: () => void
autoPreview?: boolean
onPreviewChange?: (showPreview: boolean) => void
}
export function ObjectInfo({ bucketName, objectKey, open, onOpenChange }: ObjectInfoProps) {
export function ObjectInfo({
bucketName,
objectKey,
open,
onOpenChange,
onRefresh,
autoPreview = false,
onPreviewChange,
}: ObjectInfoProps) {
const { t } = useTranslation()
const message = useMessage()
const dialog = useDialog()
@@ -219,10 +229,25 @@ export function ObjectInfo({ bucketName, objectKey, open, onOpenChange }: Object
}
}
// Auto-open preview once when autoPreview is true and object data is first loaded
const hasAutoPreviewedRef = React.useRef(false)
React.useEffect(() => {
if (!open) {
hasAutoPreviewedRef.current = false
return
}
if (autoPreview && object && !hasAutoPreviewedRef.current) {
hasAutoPreviewedRef.current = true
setShowPreview(true)
}
}, [autoPreview, object, open])
// Open preview dialog
const openPreview = () => {
if (!object) return
setPreviewObject(object)
setShowPreview(true)
onPreviewChange?.(true)
}
const handlePreviewVersion = async (versionId: string) => {
@@ -648,7 +673,14 @@ export function ObjectInfo({ bucketName, objectKey, open, onOpenChange }: Object
</DialogContent>
</Dialog>
<ObjectPreviewModal show={showPreview} onShowChange={setShowPreview} object={previewObject ?? object} />
<ObjectPreviewModal
show={showPreview}
onShowChange={(show) => {
setShowPreview(show)
onPreviewChange?.(show)
}}
object={previewObject ?? object}
/>
<ObjectVersions
bucketName={bucketName}
objectKey={(object?.Key as string) ?? ""}
+16 -5
View File
@@ -12,6 +12,7 @@ import {
RiFileLine,
RiArrowLeftSLine,
RiArrowRightSLine,
RiEyeLine,
} from "@remixicon/react"
import {
AlertDialog,
@@ -263,10 +264,20 @@ export function ObjectList({
cell: ({ row }) => (
<div className="flex items-center gap-2">
{row.original.type === "object" ? (
<Button variant="outline" size="sm" onClick={() => downloadFile(row.original.Key)}>
<RiDownloadCloud2Line className="size-4" />
<span>{t("Download")}</span>
</Button>
<>
<Button variant="outline" size="sm" asChild>
<Link
href={`/browser?bucket=${encodeURIComponent(bucket)}&key=${encodeURIComponent(prefix)}&preview=true&previewKey=${encodeURIComponent(row.original.Key)}`}
>
<RiEyeLine className="size-4" />
<span>{t("Preview")}</span>
</Link>
</Button>
<Button variant="outline" size="sm" onClick={() => downloadFile(row.original.Key)}>
<RiDownloadCloud2Line className="size-4" />
<span>{t("Download")}</span>
</Button>
</>
) : null}
<Button variant="outline" size="sm" onClick={() => openDeleteDialog([row.original.Key])}>
<RiDeleteBin5Line className="size-4" />
@@ -276,7 +287,7 @@ export function ObjectList({
),
},
],
[t, bucket, bucketPath, onOpenInfo, displayKey, downloadFile],
[t, displayKey, bucketPath, onOpenInfo, bucket, prefix, downloadFile],
)
const { table, selectedRowIds } = useDataTable<ObjectRow>({
+27 -2
View File
@@ -4,6 +4,7 @@ import * as React from "react"
import { useTranslation } from "react-i18next"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Spinner } from "@/components/ui/spinner"
import { Button } from "@/components/ui/button"
const TEXT_MIMES = [
"application/json",
@@ -33,6 +34,7 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview
const { t } = useTranslation()
const [textContent, setTextContent] = React.useState("")
const [loading, setLoading] = React.useState(false)
const [isFormatted, setIsFormatted] = React.useState(true)
const contentType = object?.ContentType ?? ""
const previewUrl = object?.SignedUrl ?? ""
@@ -43,14 +45,26 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview
const isVideo = contentType.startsWith("video/")
const isAudio = contentType.startsWith("audio/")
const isPdf = contentType === "application/pdf" || objectKey.toLowerCase().endsWith(".pdf")
const isJson = contentType === "application/json" || objectKey.toLowerCase().endsWith(".json")
const isText =
objectSize <= ALLOWED_SIZE &&
(TEXT_MIMES.some((m) => contentType.startsWith(m)) ||
TEXT_EXTENSIONS.some((ext) => objectKey.toLowerCase().endsWith(ext)))
const getFormattedContent = () => {
if (!isJson || !isFormatted) return textContent
try {
const parsed = JSON.parse(textContent)
return JSON.stringify(parsed, null, 2)
} catch {
return textContent
}
}
React.useEffect(() => {
if (show && isText && previewUrl) {
setLoading(true)
setIsFormatted(true)
fetch(previewUrl)
.then((r) => r.text())
.then(setTextContent)
@@ -66,7 +80,7 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview
<Dialog open={show} onOpenChange={onShowChange}>
<DialogContent className="sm:max-w-4xl max-h-[85vh] overflow-auto z-[1000]">
<DialogHeader>
<DialogTitle>{t("Preview")}</DialogTitle>
<DialogTitle className="flex items-start justify-between mr-6">{t("Preview")}</DialogTitle>
</DialogHeader>
<div className="min-h-[300px] rounded-md border p-4 flex flex-col">
{loading ? (
@@ -81,7 +95,18 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview
)}
{isPdf && <iframe src={previewUrl} className="h-[70vh] w-full" frameBorder={0} title="PDF preview" />}
{isText && (
<pre className="max-h-[70vh] overflow-auto whitespace-pre-wrap break-words">{textContent}</pre>
<pre className="max-h-[70vh] relative overflow-auto whitespace-pre-wrap break-words">
{getFormattedContent()}
<div className="absolute right-0 top-0">
{isJson && (
<div className="flex justify-end">
<Button variant="outline" size="sm" onClick={() => setIsFormatted(!isFormatted)}>
{isFormatted ? t("Raw") : t("Formatted")}
</Button>
</div>
)}
</div>
</pre>
)}
{isVideo && (
<video controls className="w-full">