Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/(dashboard)/browser/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion components/buckets/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions components/events/new-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,7 +68,7 @@ export function EventsNewForm({ open, onOpenChange, bucketName, onSuccess, disab
}, [getEventTargetArnList])

useEffect(() => {
queueMicrotask(() => loadArnList())
scheduleMicrotask(() => loadArnList())
}, [loadArnList])

const resetForm = useCallback(() => {
Expand All @@ -81,7 +82,7 @@ export function EventsNewForm({ open, onOpenChange, bucketName, onSuccess, disab

useEffect(() => {
if (open) {
queueMicrotask(() => resetForm())
scheduleMicrotask(() => resetForm())
}
}, [open, resetForm])

Expand Down
3 changes: 2 additions & 1 deletion components/object/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
<>
Expand Down
19 changes: 17 additions & 2 deletions components/object/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion components/object/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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">
Expand Down
5 changes: 3 additions & 2 deletions contexts/api-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) => {
Expand Down
5 changes: 3 additions & 2 deletions contexts/s3-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) => {
Expand Down
12 changes: 12 additions & 0 deletions lib/safe-date.ts
Original file line number Diff line number Diff line change
@@ -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()
}
13 changes: 13 additions & 0 deletions lib/schedule-microtask.ts
Original file line number Diff line number Diff line change
@@ -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)
}
20 changes: 20 additions & 0 deletions tests/lib/browser-safety-source.test.js
Original file line number Diff line number Diff line change
@@ -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)
})
19 changes: 19 additions & 0 deletions tests/lib/object-list-source.test.js
Original file line number Diff line number Diff line change
@@ -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)
})