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
31 changes: 9 additions & 22 deletions components/object/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { DANGER_BUTTON_CLASS } from "@/components/danger-button"
import { Spinner } from "@/components/ui/spinner"
import { useDataTable } from "@/hooks/use-data-table"
import { useObject } from "@/hooks/use-object"
import { useObjectListPagination } from "@/hooks/use-object-list-pagination"
import { useBucket } from "@/hooks/use-bucket"
import { useLocalStorage } from "@/hooks/use-local-storage"
import { usePermissions } from "@/hooks/use-permissions"
Expand Down Expand Up @@ -433,14 +434,18 @@ export function ObjectList({
const filteredEmptyState = displayState === "filtered-partial" || displayState === "filtered-empty"
const emptyTitle = filteredEmptyState
? t(displayState === "filtered-partial" ? "No matches in loaded objects" : "No matching objects")
: t("No Objects")
: displayState === "partial"
? t("Load next objects")
: t("No Objects")
const emptyDescription = filteredEmptyState
? t(
displayState === "filtered-partial"
? "More objects have not been searched yet."
: "No loaded objects match this filter.",
)
: t("Upload files or create folders to populate this bucket.")
: displayState === "partial"
? t("More objects have not been searched yet.")
: t("Upload files or create folders to populate this bucket.")

const downloadFile = React.useCallback(
async (key: string) => {
Expand Down Expand Up @@ -850,25 +855,7 @@ export function ObjectList({
void fetchObjects({ token: nextToken, append: true })
}, [fetchObjects, nextToken])

React.useEffect(() => {
const node = loadMoreRef.current
if (!node || !nextToken || typeof IntersectionObserver === "undefined") return

const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
loadNextBatch()
}
},
{ rootMargin: "320px 0px" },
)

observer.observe(node)

return () => {
observer.disconnect()
}
}, [loadNextBatch, nextToken])
useObjectListPagination({ loadMoreRef, nextToken, loading, loadMoreError, loadNextBatch })

return (
<div className="space-y-6">
Expand Down Expand Up @@ -994,7 +981,7 @@ export function ObjectList({
</div>
) : null}

{data.length > 0 ? (
{data.length > 0 || nextToken ? (
<div
ref={loadMoreRef}
role={loadMoreError ? "alert" : undefined}
Expand Down
2 changes: 2 additions & 0 deletions docs/ui-review/register.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Use the resolved rows for the PR description:
| UI-030 | Bucket rule detail pages | Events, Replication, and Lifecycle rendered Add/Refresh in a second toolbar below the page header, separate from Back navigation. | Related page-level actions were split across two rows and displaced the table downward. | Added a tab header render seam and composed Back, Add, and Refresh together in the page header for all three scoped bucket pages. | `layout-followup-27` | Resolved |
| UI-031 | Top navigation account | The account artwork and its button trigger were both 32px high, so the circular avatar consumed the trigger's complete vertical space. | The avatar visually touched the trigger boundary and crowded adjacent top-navigation controls. | Reduced the artwork to 24px while retaining the 32px button target, leaving a visible 4px inset around the avatar. | `avatar-inset-28` | Resolved |
| UI-032 | Danger confirm buttons | `text-destructive-foreground` was never registered in `@theme inline`, so the solid `bg-destructive` confirm buttons in the SSE discard, stop-KMS, and force-delete dialogs emitted no CSS and inherited `text-popover-foreground` (black on red). Two other sites pushed `text-white` onto the tinted `destructive` Button variant, which removed its red label. | Destructive confirmations were unreadable in both themes: 4.2:1 in light mode and 2.8:1 in dark mode for the SSE dialogs, 1.2:1 for the shared `dialog.error` confirms and the object delete confirm. | Registered `--destructive-foreground` as a light label on the light-theme red and a dark label on the lightened dark-theme red (4.6:1 / 6.9:1), and routed every filled danger button through one shared `DANGER_BUTTON_CLASS` so the fill and its label can no longer drift apart. | `danger-button-32` pending | Resolved |
| UI-033 | Object browser pagination | An empty S3 page with a continuation token displayed an empty bucket and omitted automatic pagination and the retry control. | Visible objects on later pages could remain inaccessible through normal scrolling. | Retain pagination for empty pages, distinguish partial results from empty buckets, and pause automatic loading after append failures. | `object-pagination-33` | Resolved |
| UI-034 | Table bucket settings | Enabled table buckets had no Console action to disable an empty catalog. | Operators could not restore ordinary bucket behavior through the Console. | Expose a permission-checked action only when the server advertises support, confirm lifecycle consequences, and update state only after successful deletion. | `table-catalog-disable-34` | Resolved |

### Reconciled border rule
Expand Down Expand Up @@ -89,6 +90,7 @@ The 2026-07-11 follow-up reviewed 44 candidate files containing cards, dialogs,
- `avatar-inset-28` is a source-faithful static fixture for the exact 32px trigger and 32px-to-24px artwork change; source tests and component code remain authoritative.
- `danger-button-32` has no before/after capture yet. The implementation proof is `tests/lib/danger-button.test.ts` plus the compiled CSS, which emits no `.text-destructive-foreground` rule before the change and `color: var(--destructive-foreground)` after it. Capture light and dark dialogs for the SSE discard, stop-KMS, force-delete, object delete, and a `dialog.error` confirm before citing this key.
- `account-2fa-29` is a set of live runtime captures, not fixtures: a debug RustFS server with `RUSTFS_IAM_MASTER_KEY` set, this branch on `pnpm dev`, root identity, viewport 1585x1202 CSS px (the setup-dialog frame is 628 CSS px). It covers 2FA off/on, the setup QR, the recovery-code dismissal guard, the profile page, and the login second-factor step. It does **not** include a mobile viewport or a `main`-branch before-capture of the account menu; the responsive rules and the removed administrator gate are covered by source tests, which remain authoritative for those two.
- `object-pagination-33` captures the running Next.js Object Browser at 1440x1000 with mocked S3 responses: two empty pages followed by one visible object. The before capture uses `main`; the after capture uses the repaired component. This proves Console behavior with controlled responses; a live RustFS integration remains separate.
- `table-catalog-disable-34` captures the running Next.js S3 Tables page at 1440x1000 with mocked catalog responses and an enabled bucket. The before capture uses `main`; the after capture uses the capability-gated disable action. Browser checks cover old-server compatibility, permissions, cancellation, conflict retry, disable and re-enable, and mobile/dark confirmation. These are Console checks with controlled responses; backend safety is verified separately.
- The full on-disk mapping is maintained in the screenshot manifest next to the image archive.

Expand Down
37 changes: 37 additions & 0 deletions hooks/use-object-list-pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect, type RefObject } from "react"

interface ObjectListPaginationOptions {
loadMoreRef: RefObject<HTMLDivElement | null>
nextToken: string | undefined
loading: boolean
loadMoreError: boolean
loadNextBatch: () => void
}

export function useObjectListPagination({
loadMoreRef,
nextToken,
loading,
loadMoreError,
loadNextBatch,
}: ObjectListPaginationOptions) {
useEffect(() => {
const node = loadMoreRef.current
if (!node || !nextToken || loading || loadMoreError || typeof IntersectionObserver === "undefined") return

const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
loadNextBatch()
}
},
{ rootMargin: "320px 0px" },
)

observer.observe(node)

return () => {
observer.disconnect()
}
}, [loadMoreRef, loadNextBatch, nextToken, loading, loadMoreError])
}
4 changes: 3 additions & 1 deletion lib/object-list-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface ObjectListResponseGuardParams {
export type ObjectListDisplayState =
| "loading"
| "empty"
| "partial"
| "filtered-loading"
| "filtered-partial"
| "filtered-empty"
Expand Down Expand Up @@ -69,7 +70,8 @@ export function resolveObjectListDisplayState({

const isFiltering = searchTerm.trim().length > 0
if (!isFiltering) {
return loading && loadedCount === 0 ? "loading" : "empty"
if (loading && loadedCount === 0) return "loading"
return hasMore ? "partial" : "empty"
}

if (loading) return "filtered-loading"
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"start": "next start",
"lint": "eslint",
"lint:fix": "eslint --fix",
"test:run": "node --experimental-strip-types --import ./scripts/register-typescript-loader.mjs --test tests/lib/*.test.{js,ts}",
"test:run": "node --experimental-strip-types --import ./scripts/register-typescript-loader.mjs --test tests/lib/*.test.{js,ts} tests/hooks/*.test.ts",
"type-check": "node scripts/apply-theme-overrides.js && tsc --noEmit",
"prettier": "prettier",
"format": "prettier --write .",
Expand Down Expand Up @@ -61,6 +61,7 @@
"eslint": "^9.39.4",
"eslint-config-next": "16.3.0",
"eslint-config-prettier": "^10.1.8",
"happy-dom": "20.14.5",
"prettier": "^3.8.4",
"shadcn": "^4.16.2",
"tailwindcss": "^4.3.0",
Expand Down
66 changes: 66 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading