Skip to content

Commit 16a5a52

Browse files
authored
fix(ui): fix Ceph findings for Q2 go-live (#992)
* refactor(ui): ceph objects header badges and actions * refactor(ui): empty bucket, delete bucket * refactor(ui): improve ceph move/rename and copy modals * fix(ui): right position for actions column in edit metadata * refactor(ui): change delete object title * fix(aurora): unit tests * refactor(ui): change em dash * refactor(ui): move breadcrumbs * fix(aurora): fixed tests * refactor(ui): metadata * fix(aurora): empty backet issue * fix(aurora): showing deleted folders if we have deleted file * fix(aurora): go to the root after tab changed * fix(aurora): improve delete bucket modal logic for versioned buckets Fixed several issues with bucket deletion validation: 1. Increased maxKeys from 1 to 100 to get sufficient data for accurate bucket state determination (with maxKeys=1, delete markers could hide actual files) 2. Fixed detection of current objects in versioned buckets - when showVersions=true, BFF returns all data in "versions" array, not "objects", so we now count current objects from versions with isLatest=true && !isDeleteMarker 3. Improved delete marker handling - any delete marker (current or old) now correctly triggers "Delete all versions" message, since they require explicit deletion and prevent bucket deletion Now correctly shows both messages when bucket has: - Current objects (isLatest=true && !isDeleteMarker) → "Empty the bucket" - Old versions or delete markers → "Delete all versions and delete markers" * fix(aurora): fixed current/versions state * feat(aurora): added changeset * fix(aurora): object browser view tests * refactor(ui): added two different buttons for empty/versioning * refactor(ui): bucket policy title * fix(ui): long name issue
1 parent 4f25b53 commit 16a5a52

41 files changed

Lines changed: 1831 additions & 397 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/small-planets-battle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cobaltcore-dev/aurora": minor
3+
---
4+
5+
fix Ceph findings for Q2 go-live

packages/aurora/src/client/routes/_auth/projects/$projectId/storage/$provider/$storageType/$containerName/objects/index.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, expect, vi, beforeEach } from "vitest"
22
import { redirect } from "@tanstack/react-router"
33
import { getServiceIndex } from "@/server/Authentication/helpers"
4-
import { checkServiceAvailability } from "./"
4+
import { checkServiceAvailability } from "../../../../-components/utils/serviceAvailability"
55

66
// Mock the dependencies
77
vi.mock("@tanstack/react-router", async () => {

packages/aurora/src/client/routes/_auth/projects/$projectId/storage/$provider/$storageType/$containerName/objects/index.tsx

Lines changed: 10 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,12 @@
1-
import { createFileRoute, redirect, useParams } from "@tanstack/react-router"
2-
import { getServiceIndex } from "@/server/Authentication/helpers"
1+
import { createFileRoute, useParams } from "@tanstack/react-router"
2+
import { checkServiceAvailability } from "../../../../-components/utils/serviceAvailability"
33
import { ErrorBoundary } from "react-error-boundary"
44
import { Trans } from "@lingui/react/macro"
55
import { SwiftObjects } from "../../../../-components/Swift/Objects"
66
import { CephObjects } from "../../../../-components/Ceph/Objects"
77
import { z } from "zod"
88
import type { RouteInfo } from "@/client/routes/routeInfo"
9-
import { ContentHeader } from "@/client/components/ContentHeader/ContentHeader"
10-
11-
export const checkServiceAvailability = (
12-
availableServices: {
13-
type: string
14-
name: string
15-
}[],
16-
params: {
17-
projectId: string
18-
provider: string
19-
storageType: string
20-
containerName: string
21-
}
22-
) => {
23-
const { provider, projectId, storageType, containerName } = params
24-
25-
const serviceIndex = getServiceIndex(availableServices)
26-
27-
// Redirect to the "Projects Overview" page if no storage services available
28-
if (!serviceIndex["object-store"]) {
29-
throw redirect({
30-
to: "/projects/$projectId",
31-
params: { projectId },
32-
})
33-
}
34-
35-
// Check provider availability
36-
const hasSwift = Boolean(serviceIndex["object-store"]["swift"])
37-
const hasCeph = Boolean(serviceIndex["object-store"]["ceph"])
38-
39-
// TEMPORARY: Allow Ceph access even if not in catalog (relies on env config)
40-
// TODO: Properly register Ceph in OpenStack service catalog
41-
const cephFallbackEnabled = true // Set to false once Ceph is in catalog
42-
43-
// Effective availability includes fallback flag for Ceph
44-
const hasEffectiveCeph = hasCeph || cephFallbackEnabled
45-
const fallbackProvider = hasSwift ? "swift" : hasEffectiveCeph ? "ceph" : null
46-
const fallbackStorageType = hasSwift ? "containers" : hasEffectiveCeph ? "buckets" : null
47-
48-
if (provider !== "swift" && provider !== "ceph") {
49-
if (!fallbackProvider || !fallbackStorageType) {
50-
throw redirect({
51-
to: "/projects/$projectId",
52-
params: { projectId },
53-
})
54-
}
55-
throw redirect({
56-
to: "/projects/$projectId/storage/$provider/$storageType/$containerName/objects",
57-
params: { projectId, provider: fallbackProvider, storageType: fallbackStorageType, containerName },
58-
})
59-
}
60-
61-
if (provider === "swift" && !hasSwift) {
62-
if (!hasEffectiveCeph) {
63-
throw redirect({
64-
to: "/projects/$projectId",
65-
params: { projectId },
66-
})
67-
}
68-
69-
throw redirect({
70-
to: "/projects/$projectId/storage/$provider/$storageType/$containerName/objects",
71-
params: { projectId, provider: "ceph", storageType: "buckets", containerName },
72-
})
73-
}
74-
75-
if (provider === "ceph" && !hasEffectiveCeph) {
76-
if (!hasSwift) {
77-
throw redirect({
78-
to: "/projects/$projectId",
79-
params: { projectId },
80-
})
81-
}
82-
83-
throw redirect({
84-
to: "/projects/$projectId/storage/$provider/$storageType/$containerName/objects",
85-
params: { projectId, provider: "swift", storageType: "containers", containerName },
86-
})
87-
}
88-
89-
// Canonicalize the URL terminology for the resolved provider. Availability is
90-
// already settled above, so by this point provider is a valid, available
91-
// swift|ceph. The storageType segment is user-controllable and the router never
92-
// validates it, so a mismatched noun (e.g. ceph + "containers", swift + "buckets")
93-
// must redirect to the canonical path to keep URLs normalized.
94-
if (provider === "swift" || provider === "ceph") {
95-
const expectedStorageType = provider === "swift" ? "containers" : "buckets"
96-
if (storageType !== expectedStorageType) {
97-
throw redirect({
98-
to: "/projects/$projectId/storage/$provider/$storageType/$containerName/objects",
99-
params: { projectId, provider, storageType: expectedStorageType, containerName },
100-
})
101-
}
102-
}
103-
}
9+
import { BucketHeader } from "../../../../-components/Ceph/Buckets/BucketHeader"
10410

10511
// Search params schema
10612
// - prefix: base64-encoded current folder path, safe to carry "/" chars in the URL
@@ -156,29 +62,28 @@ export const Route = createFileRoute(
15662
})
15763

15864
function ObjectsDashboard() {
159-
const { project, provider, containerName } = useParams({
65+
const { projectId, provider, containerName } = useParams({
16066
from: "/_auth/projects/$projectId/storage/$provider/$storageType/$containerName/objects/",
16167
select: (params) => ({
162-
project: params.projectId,
68+
projectId: params.projectId,
16369
provider: params.provider,
16470
containerName: params.containerName,
165-
storageType: params.storageType,
16671
}),
16772
})
16873

16974
const { prefix, sortBy, sortDirection, search } = Route.useSearch()
17075

171-
// For Ceph buckets, we need to show ContentHeader
76+
// For Ceph buckets, we show ContentHeader with badges and actions
17277
// For Swift containers, the component handles its own header
17378
const showContentHeader = provider === "ceph"
17479

17580
return (
17681
<>
177-
{showContentHeader && <ContentHeader title={containerName} projectId={project} />}
82+
{showContentHeader && <BucketHeader bucketName={containerName} />}
17883
<div>
179-
{project ? (
84+
{projectId ? (
18085
<ErrorBoundary
181-
resetKeys={[project, provider, containerName, prefix, sortBy, sortDirection, search]}
86+
resetKeys={[projectId, provider, containerName, prefix, sortBy, sortDirection, search]}
18287
fallback={
18388
<div className="p-4 text-center">
18489
<Trans>Error loading component</Trans>
@@ -194,7 +99,7 @@ function ObjectsDashboard() {
19499
default:
195100
return (
196101
<div className="p-4">
197-
<Trans>Objects {containerName}</Trans>
102+
<Trans>Objects: {containerName}</Trans>
198103
</div>
199104
)
200105
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useState } from "react"
2+
import { Trans } from "@lingui/react/macro"
3+
import { useParams } from "@tanstack/react-router"
4+
import { Badge } from "@cloudoperators/juno-ui-components"
5+
import { ContentHeader } from "@/client/components/ContentHeader/ContentHeader"
6+
import { useBucketInfo } from "../hooks/useBucketInfo"
7+
import { BucketHeaderActions } from "./BucketHeaderActions"
8+
import { BucketModals, type ModalType } from "./BucketModals"
9+
10+
interface BucketHeaderProps {
11+
bucketName: string
12+
}
13+
14+
/**
15+
* Bucket header component with badges, actions, and modals
16+
*
17+
* Displays:
18+
* - Bucket name as title
19+
* - Badges for versioning status and policy
20+
* - Action buttons
21+
* - All bucket management modals
22+
*/
23+
export const BucketHeader = ({ bucketName }: BucketHeaderProps) => {
24+
const { projectId, provider, storageType } = useParams({
25+
from: "/_auth/projects/$projectId/storage/$provider/$storageType/$containerName/objects/",
26+
})
27+
28+
const [activeModal, setActiveModal] = useState<ModalType | null>(null)
29+
30+
// Fetch bucket information
31+
const { versioningStatus, policyData, hasOldVersionsOrDeleteMarkers, isBucketEmpty } = useBucketInfo({
32+
bucketName,
33+
enabled: true,
34+
})
35+
36+
const openModal = (modal: ModalType) => setActiveModal(modal)
37+
const closeModal = () => setActiveModal(null)
38+
39+
// Build badges
40+
const badges = (
41+
<>
42+
{versioningStatus && versioningStatus.status === "Enabled" && (
43+
<Badge variant="success">
44+
<Trans>Versioning Enabled</Trans>
45+
</Badge>
46+
)}
47+
{versioningStatus && versioningStatus.status === "Suspended" && (
48+
<Badge variant="warning">
49+
<Trans>Versioning Suspended</Trans>
50+
</Badge>
51+
)}
52+
{policyData?.policy && (
53+
<Badge variant="info">
54+
<Trans>Bucket Policy</Trans>
55+
</Badge>
56+
)}
57+
</>
58+
)
59+
60+
// Build actions
61+
const actions = (
62+
<BucketHeaderActions
63+
versioningStatus={versioningStatus}
64+
hasPolicy={Boolean(policyData?.policy)}
65+
hasOldVersionsOrDeleteMarkers={hasOldVersionsOrDeleteMarkers}
66+
isBucketEmpty={isBucketEmpty}
67+
onOpenModal={openModal}
68+
/>
69+
)
70+
71+
return (
72+
<>
73+
<ContentHeader title={bucketName} projectId={projectId} badges={badges} actions={actions} />
74+
75+
<BucketModals
76+
bucketName={bucketName}
77+
provider={provider}
78+
storageType={storageType}
79+
activeModal={activeModal}
80+
onClose={closeModal}
81+
/>
82+
</>
83+
)
84+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Trans, useLingui } from "@lingui/react/macro"
2+
import { Button, PopupMenu, PopupMenuItem, PopupMenuOptions, PopupMenuToggle } from "@cloudoperators/juno-ui-components"
3+
import type { ModalType } from "./BucketModals"
4+
5+
interface BucketHeaderActionsProps {
6+
versioningStatus?: {
7+
status: "Enabled" | "Suspended" | "Unversioned"
8+
}
9+
hasPolicy: boolean
10+
hasOldVersionsOrDeleteMarkers: boolean
11+
isBucketEmpty: boolean
12+
onOpenModal: (modal: ModalType) => void
13+
}
14+
15+
/**
16+
* Bucket header actions component
17+
*
18+
* Displays:
19+
* - Policy button (primary or subdued based on whether policy exists)
20+
* - Actions dropdown with versioning, policy, and bucket management options
21+
*/
22+
export const BucketHeaderActions = ({
23+
versioningStatus,
24+
hasPolicy,
25+
hasOldVersionsOrDeleteMarkers,
26+
isBucketEmpty,
27+
onOpenModal,
28+
}: BucketHeaderActionsProps) => {
29+
const { t } = useLingui()
30+
31+
return (
32+
<>
33+
<Button variant="subdued" className="whitespace-nowrap" onClick={() => onOpenModal("policy")}>
34+
{hasPolicy ? <Trans>Edit/View Policy</Trans> : <Trans>Add Policy</Trans>}
35+
</Button>
36+
<PopupMenu>
37+
<PopupMenuToggle as="div">
38+
<Button icon="moreVert" />
39+
</PopupMenuToggle>
40+
<PopupMenuOptions>
41+
{versioningStatus &&
42+
(versioningStatus.status === "Unversioned" || versioningStatus.status === "Suspended") && (
43+
<PopupMenuItem label={t`Enable Versioning`} onClick={() => onOpenModal("enableVersioning")} />
44+
)}
45+
{versioningStatus && versioningStatus.status === "Enabled" && (
46+
<PopupMenuItem label={t`Suspend Versioning`} onClick={() => onOpenModal("suspendVersioning")} />
47+
)}
48+
{hasPolicy && <PopupMenuItem label={t`Delete Policy`} onClick={() => onOpenModal("deletePolicy")} />}
49+
{!isBucketEmpty && <PopupMenuItem label={t`Empty Bucket`} onClick={() => onOpenModal("emptyBucket")} />}
50+
{hasOldVersionsOrDeleteMarkers && (
51+
<PopupMenuItem label={t`Delete Versions`} onClick={() => onOpenModal("deleteVersions")} />
52+
)}
53+
<PopupMenuItem label={t`Delete Bucket`} onClick={() => onOpenModal("deleteBucket")} />
54+
</PopupMenuOptions>
55+
</PopupMenu>
56+
</>
57+
)
58+
}

0 commit comments

Comments
 (0)