diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/DeleteContainerModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/DeleteContainerModal.tsx index 8e4d624b9..013289c31 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/DeleteContainerModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/DeleteContainerModal.tsx @@ -8,7 +8,6 @@ import { ButtonRow, TextInput, Stack, - Message, Spinner, Checkbox, } from "@cloudoperators/juno-ui-components" @@ -161,22 +160,22 @@ export const DeleteContainerModal = ({ isOpen, container, onClose, onSuccess, on } > {(objectsError || metaError) && ( - + {objectsError && ( - +

{(() => { const errorMessage = objectsError.message return Failed to load container objects: {errorMessage} })()} - +

)} {metaError && ( - +

{(() => { const errorMessage = metaError.message return Failed to load container properties: {errorMessage} })()} - +

)}
)} diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EditContainerMetadataModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EditContainerMetadataModal.tsx index b47951c64..9b4a0d8ce 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EditContainerMetadataModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EditContainerMetadataModal.tsx @@ -5,7 +5,6 @@ import { Modal, TextInput, Stack, - Message, Spinner, DataGrid, DataGridRow, @@ -435,20 +434,26 @@ export const EditContainerMetadataModal = ({ size="large" disableConfirmButton={isBusy || isAddingNew || hasEditing || isUnchanged || isMetaFailed} > + {updateMutation.isError && ( +

+ {(() => { + const errorMessage = updateMutation.error.message + return Failed to update container: {errorMessage} + })()} +

+ )} {isLoading ? ( Loading container properties... ) : isMetaFailed ? ( - - - {(() => { - const errorMessage = metaError?.message ?? "Unknown error" - return Failed to load container properties: {errorMessage} - })()} - - +

+ {(() => { + const errorMessage = metaError?.message ?? "Unknown error" + return Failed to load container properties: {errorMessage} + })()} +

) : (
@@ -812,17 +817,6 @@ export const EditContainerMetadataModal = ({ )}
- - {/* Mutation error */} - {updateMutation.isError && - (() => { - const errorMessage = updateMutation.error.message - return ( - - Failed to update container: {errorMessage} - - ) - })()}
)} diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainerModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainerModal.tsx index c1fff97f6..7d476758f 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainerModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainerModal.tsx @@ -9,7 +9,6 @@ import { ButtonRow, TextInput, Stack, - Message, Spinner, DataGrid, DataGridRow, @@ -162,12 +161,12 @@ export const EmptyContainerModal = ({ isOpen, container, onClose, onSuccess, onE } > {objectsError && ( - +

{(() => { const errorMessage = objectsError.message return Failed to load container objects: {errorMessage} })()} - +

)} {isLoadingObjects ? ( diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainersModal.test.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainersModal.test.tsx index a3a15c771..d31bdcec1 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainersModal.test.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainersModal.test.tsx @@ -121,7 +121,6 @@ describe("EmptyContainersModal", () => { describe("Content", () => { test("renders warning message", () => { renderModal() - expect(screen.getByText(/Are you sure/i)).toBeInTheDocument() expect(screen.getByText(/cannot be undone/i)).toBeInTheDocument() }) diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainersModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainersModal.tsx index 9443e6c53..26c62f567 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainersModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/EmptyContainersModal.tsx @@ -1,7 +1,7 @@ import { useState } from "react" import { Trans, useLingui } from "@lingui/react/macro" import { trpcReact } from "@/client/trpcClient" -import { Modal, Message, Spinner, Stack } from "@cloudoperators/juno-ui-components" +import { Modal, Spinner, Stack } from "@cloudoperators/juno-ui-components" import { ContainerSummary } from "@/server/Storage/types/swift" import { useProjectId } from "@/client/hooks/useProjectId" @@ -81,6 +81,7 @@ export const EmptyContainersModal = ({ isOpen, containers, onClose, onComplete } open={isOpen} onCancel={handleClose} confirmButtonLabel={isPending ? t`Emptying...` : t`Empty`} + confirmButtonVariant="primary-danger" cancelButtonLabel={t`Cancel`} onConfirm={handleConfirm} disableConfirmButton={isPending} @@ -101,17 +102,14 @@ export const EmptyContainersModal = ({ isOpen, containers, onClose, onComplete } ) : (
- - - Are you sure? All objects in the selected containers will be permanently deleted. This - cannot be undone. - +

+ All objects in the selected containers will be permanently deleted. This cannot be undone.
Please note: for dynamic and static large objects only the manifests are deleted. The related segments are not deleted. - +

diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/ManageContainerAccessModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/ManageContainerAccessModal.tsx index d1b24686d..986bf56ff 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/ManageContainerAccessModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Containers/ManageContainerAccessModal.tsx @@ -7,7 +7,6 @@ import { Modal, Textarea, Stack, - Message, Spinner, Checkbox, Badge, @@ -332,6 +331,14 @@ export const ManageContainerAccessModal = ({ size="xl" disableConfirmButton={isBusy || isMetaError} > + {updateMutation.isError && ( +

+ {(() => { + const errorMessage = updateMutation.error.message + return Failed to update ACLs: {errorMessage} + })()} +

+ )}
{/* ── Info message ─────────────────────────────────────────────────── */}
@@ -355,12 +362,12 @@ export const ManageContainerAccessModal = ({ Loading ACLs... ) : isMetaError ? ( - +

{(() => { const errorMessage = metaError?.message ?? "" return Failed to load container ACLs: {errorMessage} })()} - +

) : ( <>
@@ -522,16 +529,6 @@ export const ManageContainerAccessModal = ({

- - {/* Mutation error */} - {updateMutation.isError && ( - - {(() => { - const errorMessage = updateMutation.error.message - return Failed to update ACLs: {errorMessage} - })()} - - )} )}
diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CopyObjectModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CopyObjectModal.tsx index 917c6b768..704ecf847 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CopyObjectModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CopyObjectModal.tsx @@ -2,16 +2,7 @@ import { useState, useRef, useEffect, useCallback } from "react" import { Trans, useLingui } from "@lingui/react/macro" import { trpcReact } from "@/client/trpcClient" import { useProjectId } from "@/client/hooks/useProjectId" -import { - Modal, - Stack, - Spinner, - ComboBox, - ComboBoxOption, - TextInput, - Message, - Button, -} from "@cloudoperators/juno-ui-components" +import { Modal, Stack, Spinner, ComboBox, ComboBoxOption, TextInput, Button } from "@cloudoperators/juno-ui-components" import { useParams } from "@tanstack/react-router" import { MdFolder, MdDescription, MdCreateNewFolder, MdArrowBack } from "react-icons/md" import { useVirtualizer } from "@tanstack/react-virtual" @@ -319,6 +310,16 @@ export const CopyObjectModal = ({ isOpen, object, onClose, onSuccess, onError }: ) : ( + {copyMutation.isError && + (() => { + const errorMessage = copyMutation.error.message + return ( +

+ Failed to copy object: {errorMessage} +

+ ) + })()} + {/* Target container — ComboBox with debounced search to handle large lists */} {/* Read-only target path */} - +
+

+ The object will be copied to this path. Navigate folders above to change the destination. +

+ +
{/* Copy metadata checkbox */} - - {copyMutation.isError && - (() => { - const errorMessage = copyMutation.error.message - return ( - - Failed to copy object: {errorMessage} - - ) - })()}
)} diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CreateFolderModal.test.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CreateFolderModal.test.tsx index e7c56ee0d..4d25d8bcb 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CreateFolderModal.test.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CreateFolderModal.test.tsx @@ -150,7 +150,7 @@ describe("CreateFolderModal", () => { expect(screen.getByRole("button", { name: /Cancel/i })).toBeInTheDocument() }) - test("renders info message about virtual folders", () => { + test("renders info text about virtual folders", () => { renderModal() expect(screen.getByText(/zero-byte placeholder objects/i)).toBeInTheDocument() }) diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CreateFolderModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CreateFolderModal.tsx index 31e9a1e8c..f12b99657 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CreateFolderModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/CreateFolderModal.tsx @@ -2,7 +2,7 @@ import { useState, useRef } from "react" import { Trans, useLingui } from "@lingui/react/macro" import { trpcReact } from "@/client/trpcClient" import { useProjectId } from "@/client/hooks/useProjectId" -import { Modal, TextInput, Stack, Message } from "@cloudoperators/juno-ui-components" +import { Modal, TextInput, Stack } from "@cloudoperators/juno-ui-components" import { useParams } from "@tanstack/react-router" interface CreateFolderModalProps { @@ -110,12 +110,12 @@ export const CreateFolderModal = ({ isOpen, currentPrefix, onClose, onSuccess, o disableConfirmButton={createFolderMutation.isPending || !folderName.trim()} > - +

Folders in object storage are virtual — they are created as zero-byte placeholder objects with a trailing slash. The folder will appear once created. - +

{ expect(screen.getByRole("button", { name: /Cancel/i })).toBeInTheDocument() }) - test("renders warning message with folder name", () => { + test("renders plain text warning with folder name", () => { renderModal() - expect(screen.getByText(/Are you sure\?/i)).toBeInTheDocument() - // The folder name appears in the warning body as "documents" inside a expect(screen.getByText(/permanently deleted/i)).toBeInTheDocument() expect(screen.getByText(/"documents"/)).toBeInTheDocument() - expect(screen.getByText(/permanently deleted/i)).toBeInTheDocument() }) - test("renders SLO/DLO segments info message", () => { + test("renders SLO/DLO segments plain text note", () => { renderModal() expect(screen.getByText(/static and dynamic large objects/i)).toBeInTheDocument() expect(screen.getByText(/only the manifests are deleted/i)).toBeInTheDocument() diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteFolderModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteFolderModal.tsx index 096f2b13b..95b7f47f4 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteFolderModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteFolderModal.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from "react" import { Trans, useLingui } from "@lingui/react/macro" import { trpcReact } from "@/client/trpcClient" import { useProjectId } from "@/client/hooks/useProjectId" -import { Modal, Message, Stack, Spinner } from "@cloudoperators/juno-ui-components" +import { Modal, Stack, Spinner } from "@cloudoperators/juno-ui-components" import { useParams } from "@tanstack/react-router" import { FolderRow } from "./" @@ -81,6 +81,7 @@ export const DeleteFolderModal = ({ isOpen, folder, onClose, onSuccess, onError open={isOpen} onCancel={handleClose} confirmButtonLabel={deleteFolderMutation.isPending ? t`Deleting...` : t`Delete`} + confirmButtonVariant="primary-danger" onConfirm={handleConfirm} cancelButtonLabel={t`Cancel`} size="small" @@ -93,19 +94,18 @@ export const DeleteFolderModal = ({ isOpen, folder, onClose, onSuccess, onError
) : ( - +

- Are you sure? Folder{" "} - "{folderDisplayName}" and all objects within it will be - permanently deleted. This cannot be undone. + Folder "{folderDisplayName}" and all objects within it + will be permanently deleted. This cannot be undone. - - +

+

Note: for static and dynamic large objects only the manifests are deleted — their segments outside this folder prefix are not affected. - +

)} diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectModal.test.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectModal.test.tsx index 0f0edcfbe..d22a60f8f 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectModal.test.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectModal.test.tsx @@ -167,6 +167,13 @@ describe("DeleteObjectModal", () => { expect(screen.getByText(/Failed to load object metadata/i)).toBeInTheDocument() expect(screen.getByText(/Forbidden/i)).toBeInTheDocument() }) + + it("disables confirm button when metadata fetch fails", () => { + mockMetadataError = { message: "Forbidden" } + mockMetadata = null + renderModal() + expect(screen.getByRole("button", { name: /^Delete$/i })).toBeDisabled() + }) }) // ── Regular object (delete variant) ────────────────────────────────────── @@ -182,9 +189,10 @@ describe("DeleteObjectModal", () => { expect(screen.getByTitle("report.pdf")).toBeInTheDocument() }) - it("shows warning about permanent deletion", () => { + it("shows plain text warning about permanent deletion", () => { renderModal() expect(screen.getByText(/will be permanently deleted/i)).toBeInTheDocument() + expect(screen.queryByText(/Are you sure/i)).not.toBeInTheDocument() }) it("does not show SLO or DLO info notes for regular objects", () => { diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectModal.tsx index 6043b8ea9..b18140b65 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectModal.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react" import { Trans, useLingui } from "@lingui/react/macro" import { trpcReact } from "@/client/trpcClient" import { useProjectId } from "@/client/hooks/useProjectId" -import { Modal, Message, Stack, Spinner } from "@cloudoperators/juno-ui-components" +import { Modal, Stack, Spinner, Checkbox } from "@cloudoperators/juno-ui-components" import { useParams } from "@tanstack/react-router" import { ObjectRow } from "./" @@ -111,11 +111,17 @@ export const DeleteObjectModal = ({ isOpen, object, onClose, onSuccess, onError open={isOpen} onCancel={handleClose} confirmButtonLabel={isPending ? t`Deleting...` : t`Delete`} + confirmButtonVariant="primary-danger" onConfirm={handleConfirm} cancelButtonLabel={t`Cancel`} size="small" - disableConfirmButton={isLoading || isPending} + disableConfirmButton={isLoading || isPending || !!metadataError} > + {metadataError && ( +

+ Failed to load object metadata: {metadataErrorMessage} +

+ )} {isPending ? ( @@ -126,50 +132,40 @@ export const DeleteObjectModal = ({ isOpen, object, onClose, onSuccess, onError Loading object info... - ) : metadataError ? ( - - Failed to load object metadata: {metadataErrorMessage} - - ) : ( + ) : !metadataError ? ( - +

- Are you sure? Object "{displayName}"{" "} - will be permanently deleted. This cannot be undone. + Object "{displayName}" will be permanently deleted. This + cannot be undone. - +

{isSLO && ( <> - +

This is a static large object. By default, all associated segment objects will also be permanently deleted. - - +

+ ) => setKeepSegments(e.target.checked)} + /> )} {isDLO && ( - +

This is a dynamic large object. Only the manifest will be deleted — its segment objects (stored under the manifest prefix) are not automatically removed and must be deleted separately. - +

)}
- )} + ) : null} ) } diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectsModal.test.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectsModal.test.tsx index f3ec992fa..f7badbfc1 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectsModal.test.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectsModal.test.tsx @@ -151,7 +151,6 @@ describe("DeleteObjectsModal", () => { describe("Content", () => { test("renders danger warning message", () => { renderModal() - expect(screen.getByText(/Are you sure/i)).toBeInTheDocument() expect(screen.getByText(/cannot be undone/i)).toBeInTheDocument() }) diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectsModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectsModal.tsx index efb062fc2..55776d7ae 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectsModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/DeleteObjectsModal.tsx @@ -1,6 +1,6 @@ import { Trans, useLingui } from "@lingui/react/macro" import { trpcReact } from "@/client/trpcClient" -import { Modal, Message, Spinner, Stack } from "@cloudoperators/juno-ui-components" +import { Modal, Spinner, Stack } from "@cloudoperators/juno-ui-components" import { useProjectId } from "@/client/hooks/useProjectId" // Max number of object names shown in the list before truncating @@ -94,6 +94,7 @@ export const DeleteObjectsModal = ({ open={isOpen} onCancel={handleClose} confirmButtonLabel={isPending ? t`Deleting...` : t`Delete`} + confirmButtonVariant="primary-danger" cancelButtonLabel={t`Cancel`} onConfirm={handleConfirm} disableConfirmButton={isPending} @@ -107,11 +108,9 @@ export const DeleteObjectsModal = ({ ) : (
- - - Are you sure? The selected objects will be permanently deleted. This cannot be undone. - - +

+ The selected objects will be permanently deleted. This cannot be undone. +

diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/EditObjectMetadataModal.test.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/EditObjectMetadataModal.test.tsx index a6ee38796..8738d171f 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/EditObjectMetadataModal.test.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/EditObjectMetadataModal.test.tsx @@ -180,19 +180,19 @@ describe("EditObjectMetadataModal", () => { describe("Visibility", () => { test("renders nothing when isOpen is false", () => { renderModal({ isOpen: false }) - expect(screen.queryByText(/Properties of/i)).not.toBeInTheDocument() + expect(screen.queryByText(/Edit metadata:/i)).not.toBeInTheDocument() }) test("renders nothing when object is null", () => { renderModal({ object: null }) - expect(screen.queryByText(/Properties of/i)).not.toBeInTheDocument() + expect(screen.queryByText(/Edit metadata:/i)).not.toBeInTheDocument() }) test("renders modal title with object display name", async () => { mockObjectMetadata = makeObjectMetadata() renderModal() await flushEffects() - expect(screen.getByText("Properties of")).toBeInTheDocument() + expect(screen.getByText("Edit metadata:")).toBeInTheDocument() expect(screen.getByText("sample.txt")).toBeInTheDocument() }) }) @@ -324,27 +324,23 @@ describe("EditObjectMetadataModal", () => { await user.type(screen.getByLabelText(/Expires at/i), "not-a-date") act(() => vi.advanceTimersByTime(700)) await waitFor(() => { - expect(screen.getByText(/Expected format: YYYY-MM-DD HH:MM:SS/i)).toBeInTheDocument() + expect(screen.getByText(/Expected format: YYYY-MM-DD HH:mm:ss/i)).toBeInTheDocument() }) vi.useRealTimers() }, 10000) - test("shows helptext when user has typed something", async () => { + test("always shows helper text above expires at input", async () => { mockObjectMetadata = makeObjectMetadata() - const user = userEvent.setup() renderModal() await flushEffects() - await user.type(screen.getByLabelText(/Expires at/i), "2026") - await waitFor(() => { - expect(screen.getByText(/Enter a timestamp like/i)).toBeInTheDocument() - }) + expect(screen.getByText(/Enter a timestamp like/i)).toBeInTheDocument() }) - test("does not show helptext when field is empty", async () => { + test("shows helper text even when field is empty", async () => { mockObjectMetadata = makeObjectMetadata({ deleteAt: undefined }) renderModal() await flushEffects() - expect(screen.queryByText(/Enter a timestamp like/i)).not.toBeInTheDocument() + expect(screen.getByText(/Enter a timestamp like/i)).toBeInTheDocument() }) test("blocks submission when expires at format is invalid", async () => { diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/EditObjectMetadataModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/EditObjectMetadataModal.tsx index e0afd615b..a7d87d4be 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/EditObjectMetadataModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/EditObjectMetadataModal.tsx @@ -6,7 +6,6 @@ import { Modal, TextInput, Stack, - Message, Spinner, DataGrid, DataGridRow, @@ -43,7 +42,7 @@ interface EditObjectMetadataModalProps { // Using a positive allowlist avoids the ESLint no-control-regex rule. const VALID_KEY_RE = /^[a-zA-Z0-9!#$%&'*+\-.^_`|~]+$/ -// Expected format: "YYYY-MM-DD HH:MM:SS" (UTC). +// Expected format: "YYYY-MM-DD HH:mm:ss" (UTC). // Checks shape first, then rejects semantically invalid dates (e.g. month 13, day 30 in Feb) // by parsing as UTC and verifying the parts round-trip back to the same values. const TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/ @@ -87,7 +86,7 @@ const formatDate = (iso: string): string => { }) } -// Converts a Unix timestamp (seconds) to the "YYYY-MM-DD HH:MM:SS" field format (UTC) +// Converts a Unix timestamp (seconds) to the "YYYY-MM-DD HH:mm:ss" field format (UTC) const formatUnixToTimestamp = (unix: number): string => { const d = new Date(unix * 1000) const pad = (n: number) => String(n).padStart(2, "0") @@ -357,7 +356,7 @@ export const EditObjectMetadataModal = ({ title={ - Properties of{" "} + Edit metadata: {" "} {displayName} @@ -369,7 +368,7 @@ export const EditObjectMetadataModal = ({ confirmButtonLabel={isPending ? t`Saving...` : t`Update object`} onConfirm={handleSubmit} cancelButtonLabel={t`Cancel`} - size="large" + size={isAddingNew ? "xl" : "large"} disableConfirmButton={isBusy || !hasChanges || hasEditing || isAddingNew} > {isLoading ? ( @@ -378,27 +377,34 @@ export const EditObjectMetadataModal = ({ Loading object properties... ) : isMetaError ? ( - +

Failed to load object metadata: {metadataErrorMessage} - +

) : ( + {/* Mutation error */} + {updateMutation.isError && ( +

+ Failed to update object: {mutationErrorMessage} +

+ )} + {/* ── Large object notices ──────────────────────────────────────── */} {isSLO && ( - +

This is a static large object (SLO) manifest. Metadata changes apply to the manifest only — segment objects are not affected. - +

)} {isDLO && ( - +

This is a dynamic large object (DLO) manifest. Metadata changes apply to the manifest only — segment objects are not affected. - +

)} {/* ── Read-only properties ──────────────────────────────────────── */} @@ -436,35 +442,35 @@ export const EditObjectMetadataModal = ({

{/* ── Expires at ───────────────────────────────────────────────── */} - ) => { - const value = e.target.value - setExpiresAt(value) - if (expiresAtDebounceTimer.current) clearTimeout(expiresAtDebounceTimer.current) - expiresAtDebounceTimer.current = setTimeout(() => { - if (value.trim() && !isValidTimestamp(value.trim())) { - setExpiresAtError("invalid") - } else { - setExpiresAtError(null) - } - }, 600) - }} - invalid={!!expiresAtError} - errortext={expiresAtError ? t`Expected format: YYYY-MM-DD HH:MM:SS` : undefined} - placeholder={t`Enter a timestamp like "YYYY-MM-DD HH:mm:ss" to schedule automatic deletion`} - helptext={ - expiresAt.trim() - ? t`Enter a timestamp like "YYYY-MM-DD HH:mm:ss" to schedule automatic deletion` - : undefined - } - disabled={isBusy} - /> +
+

+ Enter a timestamp like "YYYY-MM-DD HH:mm:ss" to schedule automatic deletion. +

+ ) => { + const value = e.target.value + setExpiresAt(value) + if (expiresAtDebounceTimer.current) clearTimeout(expiresAtDebounceTimer.current) + expiresAtDebounceTimer.current = setTimeout(() => { + if (value.trim() && !isValidTimestamp(value.trim())) { + setExpiresAtError("invalid") + } else { + setExpiresAtError(null) + } + }, 600) + }} + invalid={!!expiresAtError} + errortext={expiresAtError ? t`Expected format: YYYY-MM-DD HH:mm:ss` : undefined} + placeholder={t`YYYY-MM-DD HH:mm:ss`} + disabled={isBusy} + /> +
{/* ── Custom metadata ───────────────────────────────────────────── */}
- +

Metadata

@@ -478,6 +484,7 @@ export const EditObjectMetadataModal = ({ label={t`Add Property`} onClick={() => setIsAddingNew(true)} variant="primary" + size="small" icon="addCircle" disabled={isAddingNew || hasEditing || isBusy} /> @@ -593,7 +600,7 @@ export const EditObjectMetadataModal = ({ />
- - {/* Mutation error */} - {updateMutation.isError && ( - - Failed to update object: {mutationErrorMessage} - - )} )} diff --git a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/GenerateTempUrlModal.tsx b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/GenerateTempUrlModal.tsx index 77c147690..07c75b45b 100644 --- a/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/GenerateTempUrlModal.tsx +++ b/apps/aurora-portal/src/client/routes/_auth/projects/$projectId/storage/-components/Swift/Objects/GenerateTempUrlModal.tsx @@ -2,16 +2,7 @@ import { useEffect, useRef, useState, type ChangeEvent } from "react" import { Trans, useLingui } from "@lingui/react/macro" import { trpcReact } from "@/client/trpcClient" import { useProjectId } from "@/client/hooks/useProjectId" -import { - Modal, - Message, - Stack, - Spinner, - TextInput, - Icon, - Select, - SelectOption, -} from "@cloudoperators/juno-ui-components" +import { Modal, Stack, Spinner, TextInput, Icon, Select, SelectOption } from "@cloudoperators/juno-ui-components" import { useParams } from "@tanstack/react-router" import { ObjectRow } from "./" @@ -279,12 +270,37 @@ export const GenerateTempUrlModal = ({ disableConfirmButton={isPending || (isCustom && (!customMinutes.trim() || !!customMinutesError))} > - +

A temporary URL grants time-limited read access to this object without requiring authentication. Anyone with the link can download it until it expires. - +

+ + {/* No key configured error */} + {noKeyError && ( +

+ + No Temp URL key configured. A temporary URL key must be set at the account or container + level before temporary URLs can be generated. Contact your administrator to configure{" "} + X-Account-Meta-Temp-URL-Key or X-Container-Meta-Temp-URL-Key. + +

+ )} + + {/* General error */} + {generalError && ( +

+ Failed to generate temporary URL: {generalError} +

+ )} + + {/* Clipboard copy error */} + {copyError && ( +

+ {copyError} +

+ )} {/* Expiry selector */}