mirror of
https://github.com/rustfs/console.git
synced 2026-08-28 19:47:21 +08:00
fix: harden browser page webkit compatibility (#133)
This commit is contained in:
@@ -20,6 +20,7 @@ import { usePermissions } from "@/hooks/use-permissions"
|
||||
import { useDialog } from "@/lib/feedback/dialog"
|
||||
import { useMessage } from "@/lib/feedback/message"
|
||||
import { niceBytes } from "@/lib/functions"
|
||||
import { normalizeDateToIso } from "@/lib/safe-date"
|
||||
import { BrowserContent } from "./content"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import dayjs from "dayjs"
|
||||
@@ -154,7 +155,7 @@ function BrowserBucketsPage() {
|
||||
|
||||
const bucketRow: BucketRow = {
|
||||
Name: name,
|
||||
CreationDate: item?.CreationDate ? new Date(item.CreationDate).toISOString() : "",
|
||||
CreationDate: normalizeDateToIso(item?.CreationDate),
|
||||
}
|
||||
|
||||
return bucketRow
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useBucket } from "@/hooks/use-bucket"
|
||||
import { useSystem } from "@/hooks/use-system"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
import { niceBytes } from "@/lib/functions"
|
||||
import { normalizeDateToIso } from "@/lib/safe-date"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import dayjs from "dayjs"
|
||||
|
||||
@@ -132,7 +133,7 @@ export function BucketList({ title, emptyDescription, getBucketHref }: BucketLis
|
||||
|
||||
return {
|
||||
Name: name,
|
||||
CreationDate: item?.CreationDate ? new Date(item.CreationDate).toISOString() : "",
|
||||
CreationDate: normalizeDateToIso(item?.CreationDate),
|
||||
}
|
||||
})
|
||||
.filter((bucket): bucket is BucketListRow => bucket !== null)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { useBucket } from "@/hooks/use-bucket"
|
||||
import { useEventTarget } from "@/hooks/use-event-target"
|
||||
import { useMessage } from "@/lib/feedback/message"
|
||||
import { scheduleMicrotask } from "@/lib/schedule-microtask"
|
||||
|
||||
interface EventsNewFormProps {
|
||||
open: boolean
|
||||
@@ -67,7 +68,7 @@ export function EventsNewForm({ open, onOpenChange, bucketName, onSuccess, disab
|
||||
}, [getEventTargetArnList])
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => loadArnList())
|
||||
scheduleMicrotask(() => loadArnList())
|
||||
}, [loadArnList])
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
@@ -81,7 +82,7 @@ export function EventsNewForm({ open, onOpenChange, bucketName, onSuccess, disab
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
queueMicrotask(() => resetForm())
|
||||
scheduleMicrotask(() => resetForm())
|
||||
}
|
||||
}, [open, resetForm])
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useMessage } from "@/lib/feedback/message"
|
||||
import { useDialog } from "@/lib/feedback/dialog"
|
||||
import { exportFile } from "@/lib/export-file"
|
||||
import { getContentType } from "@/lib/mime-types"
|
||||
import { normalizeDateToIso } from "@/lib/safe-date"
|
||||
import {
|
||||
getDefaultObjectRetentionDate,
|
||||
getMinimumObjectRetentionDate,
|
||||
@@ -431,7 +432,7 @@ export function ObjectInfo({ bucketName, objectKey, open, onOpenChange, onPrevie
|
||||
}
|
||||
}
|
||||
|
||||
const lastModified = object?.LastModified ? new Date(object.LastModified as string | Date).toISOString() : ""
|
||||
const lastModified = normalizeDateToIso((object?.LastModified as string | Date | undefined) ?? undefined)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -40,6 +40,7 @@ import { useMessage } from "@/lib/feedback/message"
|
||||
import { exportFile } from "@/lib/export-file"
|
||||
import { getContentType } from "@/lib/mime-types"
|
||||
import { formatBytes } from "@/lib/functions"
|
||||
import { normalizeDateToIso } from "@/lib/safe-date"
|
||||
import { buildBucketPath } from "@/lib/bucket-path"
|
||||
import {
|
||||
createObjectListScope,
|
||||
@@ -189,10 +190,24 @@ export function ObjectList({
|
||||
Key: item.Key ?? "",
|
||||
type: "object" as const,
|
||||
Size: item.Size ?? 0,
|
||||
LastModified: item.LastModified ? item.LastModified.toISOString() : "",
|
||||
LastModified: normalizeDateToIso(item.LastModified),
|
||||
}))
|
||||
|
||||
setData([...prefixItems, ...objectItems])
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch objects:", error)
|
||||
message.error((error as Error)?.message ?? t("Failed to load objects"))
|
||||
if (
|
||||
shouldApplyObjectListResponse({
|
||||
requestId,
|
||||
activeRequestId: requestIdRef.current,
|
||||
requestScope,
|
||||
activeScope: activeScopeRef.current,
|
||||
})
|
||||
) {
|
||||
setNextToken(undefined)
|
||||
setData([])
|
||||
}
|
||||
} finally {
|
||||
window.setTimeout(() => {
|
||||
if (
|
||||
@@ -208,7 +223,7 @@ export function ObjectList({
|
||||
}, 200)
|
||||
}
|
||||
},
|
||||
[bucket, prefix, resolvedPageSize, continuationToken, showDeleted, listObject],
|
||||
[bucket, prefix, resolvedPageSize, continuationToken, showDeleted, listObject, message, t],
|
||||
)
|
||||
|
||||
const prevRefreshTriggerRef = React.useRef(refreshTrigger)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useObject } from "@/hooks/use-object"
|
||||
import { useMessage } from "@/lib/feedback/message"
|
||||
import { exportFile } from "@/lib/export-file"
|
||||
import { getContentType } from "@/lib/mime-types"
|
||||
import { normalizeDateToIso } from "@/lib/safe-date"
|
||||
|
||||
interface ObjectViewProps {
|
||||
bucketName: string
|
||||
@@ -49,7 +50,7 @@ export function ObjectView({ bucketName, objectKey }: ObjectViewProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const lastModified = object?.LastModified ? new Date(object.LastModified as string).toISOString() : ""
|
||||
const lastModified = normalizeDateToIso((object?.LastModified as string | Date | undefined) ?? undefined)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AwsClient } from "@/lib/aws4fetch"
|
||||
import { ApiErrorHandler } from "@/lib/api-error-handler"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import { configManager } from "@/lib/config"
|
||||
import { scheduleMicrotask } from "@/lib/schedule-microtask"
|
||||
|
||||
interface ApiContextValue {
|
||||
api: ApiClient | null
|
||||
@@ -24,14 +25,14 @@ export function ApiProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !credentials?.AccessKeyId) {
|
||||
queueMicrotask(() => {
|
||||
scheduleMicrotask(() => {
|
||||
setApiClient(null)
|
||||
setIsReady(true)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
queueMicrotask(() => setIsReady(false))
|
||||
scheduleMicrotask(() => setIsReady(false))
|
||||
let cancelled = false
|
||||
|
||||
configManager.loadConfig().then((siteConfig) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAuth } from "@/contexts/auth-context"
|
||||
import { addApiPrefixMiddleware } from "@/lib/api-prefix-middleware"
|
||||
import { configManager } from "@/lib/config"
|
||||
import { getServiceErrorMessage, getXmlErrorMessage } from "@/lib/error-handler"
|
||||
import { scheduleMicrotask } from "@/lib/schedule-microtask"
|
||||
import type { SiteConfig } from "@/types/config"
|
||||
|
||||
interface S3Response {
|
||||
@@ -79,14 +80,14 @@ export function S3Provider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !credentials?.AccessKeyId) {
|
||||
queueMicrotask(() => {
|
||||
scheduleMicrotask(() => {
|
||||
setS3Client(null)
|
||||
setIsReady(true)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
queueMicrotask(() => setIsReady(false))
|
||||
scheduleMicrotask(() => setIsReady(false))
|
||||
let cancelled = false
|
||||
|
||||
configManager.loadConfig().then((siteConfig: SiteConfig) => {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export function normalizeDateToIso(value: Date | string | undefined | null): string {
|
||||
if (!value) return ""
|
||||
|
||||
const date = value instanceof Date ? value : new Date(value)
|
||||
const time = date.getTime()
|
||||
|
||||
if (Number.isNaN(time)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return date.toISOString()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export function scheduleMicrotask(callback: () => void) {
|
||||
if (typeof queueMicrotask === "function") {
|
||||
queueMicrotask(callback)
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof Promise === "function") {
|
||||
void Promise.resolve().then(callback)
|
||||
return
|
||||
}
|
||||
|
||||
globalThis.setTimeout(callback, 0)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import test from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import fs from "node:fs"
|
||||
|
||||
test("safe date helper returns an empty string for invalid values instead of throwing", () => {
|
||||
const source = fs.readFileSync("lib/safe-date.ts", "utf8")
|
||||
|
||||
assert.match(source, /if \(!value\) return ""/)
|
||||
assert.match(source, /const date = value instanceof Date \? value : new Date\(value\)/)
|
||||
assert.match(source, /if \(Number\.isNaN\(time\)\) \{\s+return ""\s+\}/)
|
||||
assert.match(source, /return date\.toISOString\(\)/)
|
||||
})
|
||||
|
||||
test("scheduleMicrotask falls back when queueMicrotask is unavailable", () => {
|
||||
const source = fs.readFileSync("lib/schedule-microtask.ts", "utf8")
|
||||
|
||||
assert.equal(source.includes('if (typeof queueMicrotask === "function")'), true)
|
||||
assert.equal(source.includes('if (typeof Promise === "function")'), true)
|
||||
assert.equal(source.includes("globalThis.setTimeout(callback, 0)"), true)
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import test from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import fs from "node:fs"
|
||||
|
||||
test("object list normalizes LastModified through the safe date helper", () => {
|
||||
const source = fs.readFileSync("components/object/list.tsx", "utf8")
|
||||
|
||||
assert.equal(source.includes('import { normalizeDateToIso } from "@/lib/safe-date"'), true)
|
||||
assert.equal(source.includes("LastModified: normalizeDateToIso(item.LastModified)"), true)
|
||||
assert.equal(source.includes('item.LastModified ? item.LastModified.toISOString() : ""'), false)
|
||||
})
|
||||
|
||||
test("object list falls back to an empty table instead of crashing the page on fetch errors", () => {
|
||||
const source = fs.readFileSync("components/object/list.tsx", "utf8")
|
||||
|
||||
assert.equal(source.includes('console.error("Failed to fetch objects:", error)'), true)
|
||||
assert.equal(source.includes('message.error((error as Error)?.message ?? t("Failed to load objects"))'), true)
|
||||
assert.equal(source.includes("setData([])"), true)
|
||||
})
|
||||
Reference in New Issue
Block a user