Skip to content

Commit 42471fb

Browse files
authored
feat(portal): compute image and flavor UI improvements (#912)
* feat(portal): image detail action parity with list row for Images Move Reject to ContentHeader kebab for accepted shared images, add Manage Access, and show unstyled Accept/Reject in the info box for pending/rejected state. Hide More Actions when no items apply. * fix(portal): align SharedImageBox top margin with actions row spacing * fix(portal): set consistent mt-3 spacing between divider and actions/box * feat(portal): flavor/image UI — spec permissions, clipboard IDs, action parity - Images: More Actions for all shared images; Accept for pending; ID and owner use ClipboardText - Flavors detail: Metadata button for view-only; "Edit Metadata" when editable - Flavors list: fetch flavor_specs permissions; gate Metadata on permissions; default popup icon; pass canEdit to EditSpecModal - Flavors list: Manage Access disabled for public flavors * chore: consolidate changesets for til-ui-imp * chore: merge main into til-ui-imp, resolve conflicts * feat(portal): move flavor Metadata into More Actions on detail page * fix(portal): tighten image More Actions guards and EditSpecModal canEdit - More Actions only shown for actionable member statuses (pending/accepted), not for rejected or loading states — prevents empty popup menus - headerActions condition scoped to non-shared images for edit buttons - hasMoreActions includes owner Manage Access condition - EditSpecModal: skip permissions network call when canEdit prop is provided, use it directly as source of truth for add/delete controls - Add German translation for "Access Type" (Zugriffstyp) - Update changeset with accurate descriptions per PR objectives * fix(portal): inline accept/reject for suggested images, more actions reject for accepted * fix(portal): catch policy mismatch error in EditSpecModal with ErrorBoundary Rejected promises from canUser/getExtraSpecs (e.g. unknown rule in policy file) were propagating through React.use() and crashing the app. Wrapping the Suspense in an ErrorBoundary keeps the error inside the modal. * fix(portal): add ErrorBoundary to Overview, Images, and Flavors list views Policy file mismatches cause canUser/data promises to reject, which propagates through React.use() and crashes the full page without a boundary. * chore: add ErrorBoundary fix to silver-tigers-jump changeset * fix(portal): localize ErrorBoundary fallback string in Overview * refactor(portal): simplify hasMoreActions logic and remove dead accept/reject modals in image detail * refactor(portal): remove accept/reject modals from image table row Missed in 5f77999 — apply the same direct-call pattern already used in the image detail page (call handleMemberStatusChange directly, no modal).
1 parent 2db15e8 commit 42471fb

21 files changed

Lines changed: 351 additions & 270 deletions

File tree

.changeset/silver-tigers-jump.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@cobaltcore-dev/aurora": patch
3+
---
4+
5+
feat(portal): compute image and flavor UI improvements
6+
7+
**Images**
8+
- Detail page: More Actions button now appears for accepted shared images (pending/suggested images keep inline Accept/Reject via SharedImageBox and do not show the menu) — closes #902
9+
- Detail page: Accept action added for pending shared images in More Actions; SharedImageBox retains inline Accept button for the detail body
10+
- Detail page: Image ID and Owner Project ID rendered with ClipboardText for one-click copy
11+
- Detail page: spacing fixes for SharedImageBox and actions row
12+
13+
**Flavors**
14+
- Detail page: users with view-only spec access (`flavor_specs:list`) see a **Metadata** button; users with create/delete access see **Edit Metadata** — closes #907
15+
- Detail page: Metadata and Manage Access moved into the More Actions menu (action parity with list row)
16+
- List view: `flavor_specs` permissions fetched and propagated to list row and EditSpecModal
17+
- List view: Metadata item gated on spec permissions; view-only users see **Metadata**, editors see **Edit Metadata**
18+
- List view: new **Access Type** column shows Public or Private status for each flavor — closes #908
19+
- List view: popup menu uses default icon toggle (consistent with Images); Manage Access disabled for public flavors
20+
- Manage Access modal: Flavor ID column removed for a simpler two-column layout — closes #909
21+
22+
**Error handling**
23+
- Overview, Images list, and Flavors list wrapped with ErrorBoundary to catch policy file mismatch errors that reject `canUser`/data promises via `React.use()`

.changeset/tiny-wolves-fix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cobaltcore-dev/aurora": patch
3+
---
4+
5+
refactor(portal): remove accept/reject confirmation modals from image table row

packages/aurora/src/client/components/ContentHeader/ContentHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function ContentHeader({ title, projectId, actions }: ContentHeaderProps)
2222
</div>
2323
</div>
2424
<Divider className="mt-4" />
25-
{actions && <div className="mt-2 flex justify-end">{actions}</div>}
25+
{actions && <div className="mt-3 flex justify-end">{actions}</div>}
2626
</header>
2727
)
2828
}

packages/aurora/src/client/routes/_auth/projects/$projectId/compute/-components/Flavors/-components/EditSpecModal.tsx

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { use, Suspense, useState, startTransition } from "react"
2+
import { ErrorBoundary } from "react-error-boundary"
23
import { TrpcClient } from "@/client/trpcClient"
34
import { useLingui } from "@lingui/react/macro"
45
import { useErrorTranslation } from "@/client/utils/useErrorTranslation"
@@ -23,6 +24,7 @@ interface EditSpecModalProps {
2324
onClose: () => void
2425
project: string
2526
flavor: Flavor | null
27+
canEdit?: boolean
2628
}
2729

2830
const createPermissionsPromise = (client: TrpcClient, project: string) => {
@@ -53,6 +55,13 @@ function SpecsLoading() {
5355
)
5456
}
5557

58+
function SpecsError({ error }: { error: unknown }) {
59+
const { t } = useLingui()
60+
const { translateError } = useErrorTranslation()
61+
const message = error instanceof Error ? translateError(error.message) : t`An unexpected error occurred.`
62+
return <Message variant="error" text={message} />
63+
}
64+
5665
function EditSpecContent({
5766
permissionsPromise,
5867
extraSpecsPromise,
@@ -247,17 +256,29 @@ function EditSpecContent({
247256
)
248257
}
249258

250-
export const EditSpecModal: React.FC<EditSpecModalProps> = ({ client, isOpen, onClose, project, flavor }) => {
259+
export const EditSpecModal: React.FC<EditSpecModalProps> = ({ client, isOpen, onClose, project, flavor, canEdit }) => {
251260
const { t } = useLingui()
252261

253262
const [message, setMessage] = useState<{ text: string; type: "error" | "info" } | null>(null)
254263
const [isAddingSpec, setIsAddingSpec] = useState(false)
255264
const [extraSpecsPromise, setExtraSpecsPromise] = useState<Promise<Record<string, string>> | null>(null)
265+
const [resolvedCanEdit, setResolvedCanEdit] = useState<boolean | undefined>(canEdit)
256266

257-
const permissionsPromise = React.useMemo(
258-
() => (isOpen ? createPermissionsPromise(client, project) : null),
259-
[client, project, isOpen]
260-
)
267+
const permissionsPromise = React.useMemo(() => {
268+
if (!isOpen) return null
269+
if (canEdit !== undefined) return Promise.resolve({ canCreate: canEdit, canDelete: canEdit })
270+
return createPermissionsPromise(client, project)
271+
}, [client, project, isOpen, canEdit])
272+
273+
React.useEffect(() => {
274+
if (canEdit !== undefined) {
275+
setResolvedCanEdit(canEdit)
276+
return
277+
}
278+
permissionsPromise?.then(({ canCreate, canDelete }) => setResolvedCanEdit(canCreate || canDelete))
279+
}, [permissionsPromise, canEdit])
280+
281+
const title = resolvedCanEdit ? t`Edit Metadata` : t`Metadata`
261282

262283
React.useEffect(() => {
263284
if (isOpen && flavor?.id) {
@@ -285,26 +306,28 @@ export const EditSpecModal: React.FC<EditSpecModalProps> = ({ client, isOpen, on
285306
}
286307

287308
return (
288-
<Modal onCancel={handleClose} title={t`Edit Metadata`} open={isOpen} size="large">
309+
<Modal onCancel={handleClose} title={title} open={isOpen} size="xl">
289310
<div>
290311
{message && (
291312
<Message onDismiss={() => setMessage(null)} text={message.text} variant={message.type} className="mb-4" />
292313
)}
293314

294-
<Suspense fallback={<SpecsLoading />}>
295-
<EditSpecContent
296-
permissionsPromise={permissionsPromise}
297-
extraSpecsPromise={extraSpecsPromise}
298-
client={client}
299-
project={project}
300-
flavor={flavor}
301-
onSpecsUpdate={handleSpecsUpdate}
302-
isAddingSpec={isAddingSpec}
303-
setIsAddingSpec={setIsAddingSpec}
304-
message={message}
305-
setMessage={setMessage}
306-
/>
307-
</Suspense>
315+
<ErrorBoundary fallbackRender={({ error }) => <SpecsError error={error} />}>
316+
<Suspense fallback={<SpecsLoading />}>
317+
<EditSpecContent
318+
permissionsPromise={permissionsPromise}
319+
extraSpecsPromise={extraSpecsPromise}
320+
client={client}
321+
project={project}
322+
flavor={flavor}
323+
onSpecsUpdate={handleSpecsUpdate}
324+
isAddingSpec={isAddingSpec}
325+
setIsAddingSpec={setIsAddingSpec}
326+
message={message}
327+
setMessage={setMessage}
328+
/>
329+
</Suspense>
330+
</ErrorBoundary>
308331
</div>
309332
</Modal>
310333
)

packages/aurora/src/client/routes/_auth/projects/$projectId/compute/-components/Flavors/-components/FlavorListContainer.tsx

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { useState } from "react"
1919
import { TrpcClient } from "@/client/trpcClient"
2020
import { EditSpecModal } from "./EditSpecModal"
2121
import { ManageAccessModal } from "./ManageAccessModal"
22-
import { Link, useParams, useNavigate } from "@tanstack/react-router"
22+
import { useParams, useNavigate } from "@tanstack/react-router"
2323

2424
interface FlavorListContainerProps {
2525
flavors?: Flavor[]
@@ -29,6 +29,8 @@ interface FlavorListContainerProps {
2929
onFlavorDeleted?: (flavorName: string) => void
3030
canDeleteFlavor?: boolean
3131
canMangageAccess?: boolean
32+
canManageSpecs?: boolean
33+
canListSpecs?: boolean
3234
currentPage?: number
3335
totalPages?: number
3436
onPageChange?: (page: number) => void
@@ -42,6 +44,8 @@ export const FlavorListContainer = ({
4244
onFlavorDeleted,
4345
canDeleteFlavor,
4446
canMangageAccess,
47+
canManageSpecs,
48+
canListSpecs,
4549
currentPage = 1,
4650
totalPages = 1,
4751
onPageChange,
@@ -123,7 +127,7 @@ export const FlavorListContainer = ({
123127

124128
return (
125129
<>
126-
<DataGrid columns={6} minContentColumns={[5]} className="flavors" data-testid="flavors-table">
130+
<DataGrid columns={7} minContentColumns={[6]} className="flavors" data-testid="flavors-table">
127131
<DataGridRow>
128132
<DataGridHeadCell>
129133
<Trans>Name</Trans>
@@ -140,6 +144,9 @@ export const FlavorListContainer = ({
140144
<DataGridHeadCell>
141145
<Trans>Swap (MiB)</Trans>
142146
</DataGridHeadCell>
147+
<DataGridHeadCell>
148+
<Trans>Access Type</Trans>
149+
</DataGridHeadCell>
143150
<DataGridHeadCell></DataGridHeadCell>
144151
</DataGridRow>
145152

@@ -159,22 +166,31 @@ export const FlavorListContainer = ({
159166
<DataGridCell>{flavor.ram || "–"}</DataGridCell>
160167
<DataGridCell>{flavor.disk || "–"}</DataGridCell>
161168
<DataGridCell>{flavor.swap || "–"}</DataGridCell>
169+
<DataGridCell>{flavor["os-flavor-access:is_public"] === false ? t`Private` : t`Public`}</DataGridCell>
162170
<DataGridCell onClick={(e) => e.stopPropagation()}>
163171
<PopupMenu>
164172
<PopupMenuOptions>
165-
<PopupMenuItem>
166-
<Link
167-
to="/projects/$projectId/compute/flavors/$flavorId"
168-
params={{ projectId: projectId, flavorId: flavor.id }}
169-
className="text-theme-default"
170-
>
171-
{t`Details`}
172-
</Link>
173-
</PopupMenuItem>
174-
<PopupMenuItem label={t`Metadata`} onClick={() => openSpecModal(flavor)} />
175-
173+
<PopupMenuItem
174+
label={t`Details`}
175+
onClick={() =>
176+
navigate({
177+
to: "/projects/$projectId/compute/flavors/$flavorId",
178+
params: { projectId, flavorId: flavor.id },
179+
})
180+
}
181+
/>
182+
{(canManageSpecs || canListSpecs) && (
183+
<PopupMenuItem
184+
label={canManageSpecs ? t`Edit Metadata` : t`Metadata`}
185+
onClick={() => openSpecModal(flavor)}
186+
/>
187+
)}
176188
{canMangageAccess && (
177-
<PopupMenuItem label={t`Manage Access`} onClick={() => openAccessModal(flavor)} />
189+
<PopupMenuItem
190+
label={t`Manage Access`}
191+
onClick={() => openAccessModal(flavor)}
192+
disabled={flavor["os-flavor-access:is_public"] !== false}
193+
/>
178194
)}
179195
{canDeleteFlavor && (
180196
<PopupMenuItem label={t`Delete Flavor`} onClick={() => openDeleteModal(flavor)} />
@@ -211,6 +227,7 @@ export const FlavorListContainer = ({
211227
onClose={() => setSpecModalOpen(false)}
212228
project={project}
213229
flavor={selectedFlavor}
230+
canEdit={canManageSpecs}
214231
/>
215232

216233
<ManageAccessModal

packages/aurora/src/client/routes/_auth/projects/$projectId/compute/-components/Flavors/-components/ManageAccessModal.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const createFlavorAccessPromise = (client: TrpcClient, project: string, flavorId
4949
function AccessLoading() {
5050
return (
5151
<DataGridRow>
52-
<DataGridCell colSpan={3}>
52+
<DataGridCell colSpan={2}>
5353
<Stack distribution="center" alignment="center">
5454
<Spinner variant="primary" />
5555
</Stack>
@@ -186,14 +186,13 @@ function AccessContent({
186186

187187
if (isPublicFlavor) {
188188
return (
189-
<DataGrid columns={3}>
189+
<DataGrid columns={2}>
190190
<DataGridRow>
191-
<DataGridHeadCell>{t`Flavor ID`}</DataGridHeadCell>
192191
<DataGridHeadCell>{t`Tenant ID`}</DataGridHeadCell>
193192
<DataGridHeadCell> </DataGridHeadCell>
194193
</DataGridRow>
195194
<DataGridRow>
196-
<DataGridCell colSpan={3} className="text-theme-default py-4 text-center">
195+
<DataGridCell colSpan={2} className="text-theme-default py-4 text-center">
197196
{t`This is a public flavor. All tenants have access to it.`}
198197
</DataGridCell>
199198
</DataGridRow>
@@ -215,17 +214,15 @@ function AccessContent({
215214
</Stack>
216215
)}
217216

218-
<DataGrid columns={3}>
217+
<DataGrid columns={2}>
219218
<DataGridRow>
220-
<DataGridHeadCell>{t`Flavor ID`}</DataGridHeadCell>
221219
<DataGridHeadCell>{t`Tenant ID`}</DataGridHeadCell>
222220
<DataGridHeadCell></DataGridHeadCell>
223221
</DataGridRow>
224222

225223
{isAddingAccess && (
226224
<TenantAccessFormRow
227225
tenantId={tenantId}
228-
flavorId={flavor.id}
229226
errors={errors}
230227
isLoading={isLoading}
231228
onTenantIdChange={handleTenantIdChange}
@@ -250,7 +247,7 @@ function AccessContent({
250247

251248
{shouldShowEmptyState && (
252249
<DataGridRow>
253-
<DataGridCell colSpan={3} className="text-theme-default py-4 text-center">
250+
<DataGridCell colSpan={2} className="text-theme-default py-4 text-center">
254251
{t`No specific tenant access configured for this private flavor. Click "Add Tenant Access" to grant access.`}
255252
</DataGridCell>
256253
</DataGridRow>

packages/aurora/src/client/routes/_auth/projects/$projectId/compute/-components/Flavors/-components/SpecRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const SpecRow: React.FC<SpecRowProps> = ({ specKey, value, isDeleting, on
6262
return (
6363
<DataGridRow>
6464
<DataGridCell>{specKey}</DataGridCell>
65-
<DataGridCell>{value}</DataGridCell>
65+
<DataGridCell className="break-all">{value}</DataGridCell>
6666
<DataGridCell>
6767
{isDeleting ? (
6868
<Stack distribution="center" alignment="center">

packages/aurora/src/client/routes/_auth/projects/$projectId/compute/-components/Flavors/-components/TenantAccessFormRow.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { DataGridRow, DataGridCell, TextInput, ButtonRow, Button } from "@cloudo
44

55
interface TenantAccessFormRowProps {
66
tenantId: string
7-
flavorId: string
87
errors: { tenantId?: string }
98
isLoading: boolean
109
onTenantIdChange: (tenantId: string) => void
@@ -14,7 +13,6 @@ interface TenantAccessFormRowProps {
1413

1514
export const TenantAccessFormRow: React.FC<TenantAccessFormRowProps> = ({
1615
tenantId,
17-
flavorId,
1816
errors,
1917
isLoading,
2018
onTenantIdChange,
@@ -25,7 +23,6 @@ export const TenantAccessFormRow: React.FC<TenantAccessFormRowProps> = ({
2523

2624
return (
2725
<DataGridRow>
28-
<DataGridCell>{flavorId}</DataGridCell>
2926
<DataGridCell className="pl-0">
3027
<TextInput
3128
value={tenantId}

packages/aurora/src/client/routes/_auth/projects/$projectId/compute/-components/Flavors/-components/TenantAccessRow.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ export const TenantAccessRow: React.FC<TenantAccessRowProps> = ({ access, isDele
6868

6969
return (
7070
<DataGridRow>
71-
<DataGridCell>{access.flavor_id}</DataGridCell>
7271
<DataGridCell className="break-all">{access.tenant_id}</DataGridCell>
7372
<DataGridCell>
7473
{isDeleting ? (

0 commit comments

Comments
 (0)