Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8b22304
feat(aurora-portal): adopt three-zone DataGrid header for Swift conta…
mark-karnaukh-extern-sap Jun 12, 2026
b27807c
fix(aurora-portal): fix bulk opearion selection logic (swift containers)
mark-karnaukh-extern-sap Jun 15, 2026
c5257c0
test(aurora-portal): update and fix unit tests (swift containers)
mark-karnaukh-extern-sap Jun 15, 2026
47a1cc2
feat(aurora-portal): adopt three-zone DataGrid header for Swift objects
mark-karnaukh-extern-sap Jun 15, 2026
3b8c7ff
fix(aurora-portal): make bulk modal titles singular/plural by selecti…
mark-karnaukh-extern-sap Jun 15, 2026
4573e44
chore(aurora-portal): update translations
mark-karnaukh-extern-sap Jun 15, 2026
ce0fab4
fix(aurora-portal): sync debounced search input with URL search param
mark-karnaukh-extern-sap Jun 15, 2026
f22b25f
docs(aurora-portal): drop left/right positioning wording from zone co…
mark-karnaukh-extern-sap Jun 15, 2026
f4995bf
Merge branch 'main' into mark-swift-data-grid-header-toolbar
mark-karnaukh-extern-sap Jun 15, 2026
c2a2419
Merge branch 'main' into mark-swift-data-grid-header-toolbar
mark-karnaukh-extern-sap Jun 16, 2026
2d07b3d
refactor(aurora-portal): use Lingui plural for bulk modal titles
mark-karnaukh-extern-sap Jun 16, 2026
b53f590
refactor(aurora-portal): use Lingui plural for bulk menu labels
mark-karnaukh-extern-sap Jun 16, 2026
a55a17f
chore(aurora-portal): update translations
mark-karnaukh-extern-sap Jun 16, 2026
d673314
Merge branch 'main' into mark-swift-data-grid-header-toolbar
TilmanHaupt Jun 16, 2026
e902051
Merge branch 'main' into mark-swift-data-grid-header-toolbar
mark-karnaukh-extern-sap Jun 16, 2026
1657442
chore(aurora-portal): create changeset
mark-karnaukh-extern-sap Jun 16, 2026
eb31b5e
Merge branch 'main' into mark-swift-data-grid-header-toolbar
TilmanHaupt Jun 17, 2026
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
5 changes: 5 additions & 0 deletions .changeset/plain-wings-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cobaltcore-dev/aurora": minor
---

implement three-zone DataGrid header for Object Storage (Swift)
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ interface ContainerTableViewProps {
onAclError: (containerName: string, errorMessage: string) => void
selectedContainers: string[]
setSelectedContainers: (containers: string[]) => void
// When false, the row-selection column (header checkbox + per-row checkboxes)
// is dropped entirely and the grid renders one fewer column. Defaults to true
// so existing callers that don't pass it keep the selectable layout.
hasAnyBulkAction?: boolean
}

export const ContainerTableView = ({
Expand All @@ -56,6 +60,7 @@ export const ContainerTableView = ({
onAclError,
selectedContainers,
setSelectedContainers,
hasAnyBulkAction = true,
}: ContainerTableViewProps) => {
const { projectId, provider } = useParams({
from: "/_auth/projects/$projectId/storage/$provider/containers/",
Expand Down Expand Up @@ -146,24 +151,31 @@ export const ContainerTableView = ({
)
}

// Define column template — 6 columns: checkbox, name, count, last modified, size, actions menu
const gridColumnTemplate = "40px minmax(200px, 2fr) minmax(100px, 1fr) minmax(180px, 2fr) minmax(100px, 1fr) 60px"
// Column count and template depend on whether the selection column is shown.
// With selection: checkbox, name, count, last modified, size, actions menu (6).
// Without selection: the leading 40px checkbox column is dropped (5).
const columnCount = hasAnyBulkAction ? 6 : 5
const gridColumnTemplate = hasAnyBulkAction
? "40px minmax(200px, 2fr) minmax(100px, 1fr) minmax(180px, 2fr) minmax(100px, 1fr) 60px"
: "minmax(200px, 2fr) minmax(100px, 1fr) minmax(180px, 2fr) minmax(100px, 1fr) 60px"

return (
<>
<div className="relative">
{/* Table Header with scrollbar padding */}
<div style={{ paddingRight: `${scrollbarWidth}px` }}>
<DataGrid
columns={6}
columns={columnCount}
gridColumnTemplate={gridColumnTemplate}
className="containers"
data-testid="containers-table-header"
>
<DataGridRow>
<DataGridHeadCell>
<Checkbox checked={allSelected} onChange={handleSelectAll} data-testid="select-all-containers" />
</DataGridHeadCell>
{hasAnyBulkAction && (
<DataGridHeadCell>
<Checkbox checked={allSelected} onChange={handleSelectAll} data-testid="select-all-containers" />
</DataGridHeadCell>
)}
<DataGridHeadCell>
<Trans>Container Name</Trans>
</DataGridHeadCell>
Expand Down Expand Up @@ -236,20 +248,22 @@ export const ContainerTableView = ({
}
}}
>
<DataGridCell
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation()
}
}}
>
<Checkbox
checked={isSelected}
onChange={() => handleSelectContainer(container.name)}
data-testid={`select-container-${container.name}`}
/>
</DataGridCell>
{hasAnyBulkAction && (
<DataGridCell
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation()
}
}}
>
<Checkbox
checked={isSelected}
onChange={() => handleSelectContainer(container.name)}
data-testid={`select-container-${container.name}`}
/>
</DataGridCell>
)}
<DataGridCell className="min-w-0 overflow-hidden">
<span className="block truncate" title={container.name}>
{container.name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,18 @@ describe("EmptyContainersModal", () => {

test("renders with a single container", () => {
renderModal({ containers: [makeContainer("single")] })
expect(screen.getByText("Empty Containers")).toBeInTheDocument()
expect(screen.getByText("Empty Container")).toBeInTheDocument()
expect(screen.getByText("single")).toBeInTheDocument()
})

test("title is singular for exactly one container, plural for two or more", () => {
const { unmount } = renderModal({ containers: [makeContainer("only")] })
expect(screen.getByText("Empty Container")).toBeInTheDocument()
expect(screen.queryByText("Empty Containers")).not.toBeInTheDocument()
unmount()
renderModal({ containers: [makeContainer("a"), makeContainer("b")] })
expect(screen.getByText("Empty Containers")).toBeInTheDocument()
})
})

describe("Content", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from "react"
import { Trans, useLingui } from "@lingui/react/macro"
import { Plural, Trans, useLingui } from "@lingui/react/macro"
import { trpcReact } from "@/client/trpcClient"
import { Modal, Spinner, Stack } from "@cloudoperators/juno-ui-components"
import { ContainerSummary } from "@/server/Storage/types/swift"
Expand Down Expand Up @@ -77,7 +77,7 @@ export const EmptyContainersModal = ({ isOpen, containers, onClose, onComplete }

return (
<Modal
title={t`Empty Containers`}
title={<Plural value={totalCount} one="Empty Container" other="Empty Containers" />}
open={isOpen}
onCancel={handleClose}
confirmButtonLabel={isPending ? t`Emptying...` : t`Empty`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,81 +452,91 @@ describe("SwiftContainers (List)", () => {
})
})

describe("Empty All button", () => {
test("renders the Empty All button", () => {
describe("Bulk actions menu", () => {
// The bulk Empty control is no longer a standalone "Empty All (N)" button.
// Selecting a row enables the Zone 3 "Actions" toggle; the Empty item lives
// inside that popup menu and is labeled singular/plural (no numeric count).
const selectViaCheckbox = async (user: ReturnType<typeof userEvent.setup>, name: string) => {
await user.click(screen.getByTestId(`select-container-${name}`).querySelector("input") as HTMLElement)
}
const openActionsMenu = async (user: ReturnType<typeof userEvent.setup>) => {
await user.click(screen.getByRole("button", { name: /Actions/i }))
}

test("renders the Actions button", () => {
renderList()
expect(screen.getByRole("button", { name: /Empty All/i })).toBeInTheDocument()
expect(screen.getByRole("button", { name: /Actions/i })).toBeInTheDocument()
})

test("Empty All button is disabled when no containers are selected", () => {
test("Actions button is disabled when no containers are selected", () => {
renderList()
expect(screen.getByRole("button", { name: /Empty All/i })).toBeDisabled()
expect(screen.getByRole("button", { name: /Actions/i })).toBeDisabled()
})

test("Empty All button shows no count when no containers are selected", () => {
test("no Empty item is reachable when no containers are selected", () => {
renderList()
expect(screen.getByRole("button", { name: "Empty All" })).toBeInTheDocument()
expect(screen.getByRole("button", { name: /Actions/i })).toBeDisabled()
expect(screen.queryByText(/^Empty Container/)).not.toBeInTheDocument()
})

test("Empty All button is enabled and shows count after selecting containers", async () => {
test("Actions button is enabled and exposes the Empty item after selecting a container", async () => {
const user = userEvent.setup()
renderList()
await user.click(screen.getByTestId("select-container-alpha").querySelector("input") as HTMLElement)
await waitFor(() => {
const btn = screen.getByRole("button", { name: /Empty All/i })
expect(btn).toBeEnabled()
expect(btn).toHaveTextContent("Empty All (1)")
})
await selectViaCheckbox(user, "alpha")
await waitFor(() => expect(screen.getByRole("button", { name: /Actions/i })).toBeEnabled())
await openActionsMenu(user)
expect(await screen.findByText("Empty Container")).toBeInTheDocument()
})

test("Empty All button count increments as more containers are selected", async () => {
test("Empty item label becomes plural as more containers are selected", async () => {
const user = userEvent.setup()
renderList()
await user.click(screen.getByTestId("select-container-alpha").querySelector("input") as HTMLElement)
await user.click(screen.getByTestId("select-container-beta").querySelector("input") as HTMLElement)
await waitFor(() => {
expect(screen.getByRole("button", { name: /Empty All \(2\)/i })).toBeEnabled()
})
await selectViaCheckbox(user, "alpha")
await selectViaCheckbox(user, "beta")
await waitFor(() => expect(screen.getByRole("button", { name: /Actions/i })).toBeEnabled())
await openActionsMenu(user)
expect(await screen.findByText("Empty Containers")).toBeInTheDocument()
})

test("Empty All button returns to disabled with no count after deselecting all", async () => {
test("Actions button returns to disabled after deselecting all", async () => {
const user = userEvent.setup()
renderList()
const alphaCheckbox = screen.getByTestId("select-container-alpha").querySelector("input") as HTMLElement
await user.click(alphaCheckbox)
await waitFor(() => expect(screen.getByRole("button", { name: /Empty All \(1\)/i })).toBeEnabled())
await waitFor(() => expect(screen.getByRole("button", { name: /Actions/i })).toBeEnabled())
await user.click(alphaCheckbox)
await waitFor(() => {
expect(screen.getByRole("button", { name: "Empty All" })).toBeDisabled()
})
await waitFor(() => expect(screen.getByRole("button", { name: /Actions/i })).toBeDisabled())
})

test("selecting all via header checkbox enables Empty All with full count", async () => {
test("selecting all via header checkbox enables Actions and shows the plural Empty item", async () => {
const user = userEvent.setup()
renderList()
await user.click(screen.getByTestId("select-all-containers").querySelector("input") as HTMLElement)
await waitFor(() => {
expect(screen.getByRole("button", { name: /Empty All \(3\)/i })).toBeEnabled()
})
await waitFor(() => expect(screen.getByRole("button", { name: /Actions/i })).toBeEnabled())
await openActionsMenu(user)
expect(await screen.findByText("Empty Containers")).toBeInTheDocument()
})
})

describe("Empty All modal", () => {
const selectAlpha = async (user: ReturnType<typeof userEvent.setup>) => {
describe("Bulk empty modal", () => {
// Select alpha, open the Zone 3 Actions menu, then click the Empty item to
// open the bulk-empty modal.
const selectAlphaAndOpenModal = async (user: ReturnType<typeof userEvent.setup>) => {
await user.click(screen.getByTestId("select-container-alpha").querySelector("input") as HTMLElement)
await waitFor(() => expect(screen.getByRole("button", { name: /Empty All \(1\)/i })).toBeEnabled())
await waitFor(() => expect(screen.getByRole("button", { name: /Actions/i })).toBeEnabled())
await user.click(screen.getByRole("button", { name: /Actions/i }))
await user.click(await screen.findByText("Empty Container"))
}

test("modal is not visible by default", () => {
renderList()
expect(screen.queryByTestId("empty-containers-modal")).not.toBeInTheDocument()
})

test("clicking Empty All opens the modal", async () => {
test("clicking the Empty item opens the modal", async () => {
const user = userEvent.setup()
renderList()
await selectAlpha(user)
await user.click(screen.getByRole("button", { name: /Empty All \(1\)/i }))
await selectAlphaAndOpenModal(user)
await waitFor(() => {
expect(screen.getByTestId("empty-containers-modal")).toBeInTheDocument()
})
Expand All @@ -535,8 +545,7 @@ describe("SwiftContainers (List)", () => {
test("modal receives the selected containers", async () => {
const user = userEvent.setup()
renderList()
await selectAlpha(user)
await user.click(screen.getByRole("button", { name: /Empty All \(1\)/i }))
await selectAlphaAndOpenModal(user)
await waitFor(() => {
expect(screen.getByTestId("empty-containers-modal")).toHaveAttribute("data-container-count", "1")
})
Expand All @@ -545,8 +554,7 @@ describe("SwiftContainers (List)", () => {
test("closing the modal hides it", async () => {
const user = userEvent.setup()
renderList()
await selectAlpha(user)
await user.click(screen.getByRole("button", { name: /Empty All \(1\)/i }))
await selectAlphaAndOpenModal(user)
await waitFor(() => expect(screen.getByTestId("empty-containers-modal")).toBeInTheDocument())
await user.click(screen.getByRole("button", { name: "CloseEmptyAll" }))
await waitFor(() => {
Expand All @@ -558,8 +566,7 @@ describe("SwiftContainers (List)", () => {
const { getContainersEmptyCompleteToast } = await import("./ContainerToastNotifications")
const user = userEvent.setup()
renderList()
await selectAlpha(user)
await user.click(screen.getByRole("button", { name: /Empty All \(1\)/i }))
await selectAlphaAndOpenModal(user)
await waitFor(() => expect(screen.getByTestId("empty-containers-modal")).toBeInTheDocument())
await user.click(screen.getByRole("button", { name: "SimulateEmptyAllSuccess" }))
await waitFor(() => {
Expand All @@ -569,16 +576,16 @@ describe("SwiftContainers (List)", () => {
[],
expect.objectContaining({ onDismiss: expect.any(Function) })
)
expect(screen.getByRole("button", { name: "Empty All" })).toBeDisabled()
// Selection cleared → the Actions toggle is disabled again.
expect(screen.getByRole("button", { name: /Actions/i })).toBeDisabled()
})
})

test("shows error toast when bulk empty fails", async () => {
const { getContainersEmptyCompleteToast } = await import("./ContainerToastNotifications")
const user = userEvent.setup()
renderList()
await selectAlpha(user)
await user.click(screen.getByRole("button", { name: /Empty All \(1\)/i }))
await selectAlphaAndOpenModal(user)
await waitFor(() => expect(screen.getByTestId("empty-containers-modal")).toBeInTheDocument())
await user.click(screen.getByRole("button", { name: "SimulateEmptyAllError" }))
await waitFor(() => {
Expand Down
Loading
Loading