feat(console): Admin portal — Capacity (Cluster/Nodes/Storage) and Backup Classes#28
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
📝 WalkthroughWalkthroughAdds an /admin portal with RBAC checks and a separate admin sidebar; refactors cluster usage into aggregates+gauges and a transposed nodes-as-columns table; introduces storage/resource drill-down pages, workload utilities, guards, hooks, and associated tests. ChangesAdmin Portal Infrastructure & Cluster Usage Refactor
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a dedicated Admin portal (/admin/*) gated by operator permissions, moving cluster-wide views like Cluster Usage and Backups out of the tenant-facing Console section. It also refactors the Cluster Usage aggregate view into a unified table and adds a per-resource drill-down page to view consumers grouped by tenant namespace. Feedback on these changes includes filtering out pods in Succeeded or Failed phases to avoid over-reporting resources, removing the 100% cap on resource request percentages to expose overcommit metrics, and factoring in initContainers when calculating a pod's resource requests to prevent under-reporting.
| for (const pod of podsList?.items ?? []) { | ||
| const requested = podResourceRequest(pod, resource) | ||
| if (requested <= 0) continue |
There was a problem hiding this comment.
Pods in the Succeeded or Failed phase (such as completed Jobs or finished single-run pods) no longer consume node resources. Including them in the calculation will significantly over-report the requested resources in the cluster. Consider filtering out pods that are not active.
| for (const pod of podsList?.items ?? []) { | |
| const requested = podResourceRequest(pod, resource) | |
| if (requested <= 0) continue | |
| for (const pod of podsList?.items ?? []) { | |
| const phase = pod.status?.phase | |
| if (phase === "Succeeded" || phase === "Failed") continue | |
| const requested = podResourceRequest(pod, resource) | |
| if (requested <= 0) continue |
| function percent(value: number, allocatable: number): number | null { | ||
| if (allocatable <= 0) return null | ||
| return Math.min(100, Math.round((value / allocatable) * 100)) | ||
| } |
There was a problem hiding this comment.
Capping the percentage at 100% hides resource overcommit, which is a critical metric for cluster administrators (e.g., knowing if CPU or Memory requests exceed 100% of allocatable capacity). Consider removing Math.min(100, ...) to allow percentages over 100% to be displayed.
| function percent(value: number, allocatable: number): number | null { | |
| if (allocatable <= 0) return null | |
| return Math.min(100, Math.round((value / allocatable) * 100)) | |
| } | |
| function percent(value: number, allocatable: number): number | null { | |
| if (allocatable <= 0) return null | |
| return Math.round((value / allocatable) * 100) | |
| } |
| function podResourceRequest(pod: Pod, resource: string): number { | ||
| let total = 0 | ||
| for (const container of pod.spec?.containers ?? []) { | ||
| const req = container.resources?.requests?.[resource] | ||
| const lim = container.resources?.limits?.[resource] | ||
| const value = req ?? lim | ||
| if (value !== undefined) total += parseQuantity(value) | ||
| } | ||
| return total | ||
| } |
There was a problem hiding this comment.
The current implementation only sums requests for app containers (pod.spec.containers), completely ignoring initContainers. In Kubernetes, the effective request of a pod is the maximum of the sum of app containers' requests and the maximum of any init container's request. Ignoring init containers can lead to under-reporting resource requests.
function podResourceRequest(pod: Pod, resource: string): number {
let containersTotal = 0
for (const container of pod.spec?.containers ?? []) {
const req = container.resources?.requests?.[resource]
const lim = container.resources?.limits?.[resource]
const value = req ?? lim
if (value !== undefined) containersTotal += parseQuantity(value)
}
let initContainersMax = 0
for (const container of pod.spec?.initContainers ?? []) {
const req = container.resources?.requests?.[resource]
const lim = container.resources?.limits?.[resource]
const value = req ?? lim
if (value !== undefined) {
initContainersMax = Math.max(initContainersMax, parseQuantity(value))
}
}
return Math.max(containersTotal, initContainersMax)
}
Lay the cluster-usage resources out top-to-bottom (one row per resource:
CPU, Memory, Storage, Pods, then each discovered extended resource)
instead of a left-to-right card grid. Each requestable resource row links
to a new drill-down page that lists who consumes it across the cluster,
grouped by tenant namespace and owning application (read from pod
apps.cozystack.io/application.{kind,name} labels). The resource key flows
through a splat route so vendor.com/model GPU names survive routing.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…in portal Add an Admin top-nav entry (Marketplace / Console / Admin) with its own sidebar and relocate the cluster-wide operator views out of the tenant-facing Console: Cluster Usage and the Backup Classes management added in cozystack-ui#21. Per-tenant backups (Plans / Backup Jobs / Backups / Restore Jobs) stay in Console. The Admin tab and /admin/* routes are gated by useAdminAccess — a user reaches the portal if they can use at least one area (nodes/list for Cluster Usage or backupclasses/update for Backup Classes); the tab is hidden otherwise and direct-URL access renders a 403. Each area keeps its own guard (the Cluster Usage page on nodes/list, the Backup Classes routes via BackupClassAdminGuard). Backup Classes internal links are repointed to /admin/backups/backupclasses. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…tion Each consumer row in the per-resource drill-down now deep-links to the deployed application's Console page (/console/<plural>/<name>), resolving the plural from the application kind via ApplicationDefinitions and switching the Console tenant context to the consumer's namespace on click. Only real app instances are linked — the row must live in a tenant namespace and its kind must resolve to a known application; everything else (system pods, unrecognised owners) stays plain text. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…ed table Rename the Admin sidebar section Cluster → Resources and add a dedicated Nodes entry beside Cluster Usage. The per-node breakdown moves off the Cluster Usage page (now just the cluster-wide resources table) onto its own /admin/nodes page, and the node table is transposed: each node is a column and each attribute (Status, Roles, CPU, Memory, every extended resource, Age) is a row read top-to-bottom, with a sticky label column and a name/role filter that narrows the visible node columns. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…rill-down to per-node usage Rename the admin Cluster Usage entry/page to Resources and move the routes to /admin/resources-usage and /admin/resources-nodes. Clicking a resource now opens a per-node usage page (same Capacity / Allocatable / Requested / Used columns as the Resources table, pivoted to one row per node that exposes the resource) instead of the consumer-by-tenant breakdown. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…view Clicking a resource on the Resources page again opens the consumer breakdown (which tenant namespace and owning application uses it, derived from pod labels, with a deep-link to the deployed application) rather than the per-node usage table. Keeps the Resources/resources-usage renames; the back-link points at /admin/resources-usage. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…sumers, usage gauges - Rename the admin section to Capacity with Cluster (/admin/capacity/cluster) and Nodes (/admin/capacity/nodes) entries; drill-down at /admin/capacity/cluster/r/*. - Make resource row labels in the transposed Nodes table link to the same per-resource consumer drill-down as the Cluster resources table. - Restrict the resource consumer drill-down to tenant namespaces (skip system/control-plane namespaces). - Add cluster-wide allocation gauges (Requested vs Allocatable rings) atop the Cluster page, reusing the per-tenant quota GaugeCard for a consistent look. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
- Move Status / Roles / Age into each node's column header in the transposed Nodes table, so the body rows are purely resources (CPU, Memory, extended). - Make the cluster usage gauges clickable: each ring links to the per-resource consumer drill-down (Pods excluded, matching the resources table). Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Collapse the Kind / Name / Pods columns in the per-resource drill-down into a single Workload column (owning application name with its kind as a subtitle, deep-linked to the Console app page), dropping the raw pod count. Consumers are grouped by the owning application (apps.cozystack.io/application labels), which the lineage controller also stamps on the workload's PVCs and Services. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
- Add a Persistent Storage panel to the Cluster page: tenant-namespace PVCs aggregated by StorageClass (claim count, requested and bound totals). Each StorageClass links to a per-class drill-down listing the consuming workloads (owning application) and their summed requested storage. - Extract a shared WorkloadCell component and workloadOwner helper so both the node-resource and storage drill-downs render the workload deep-link identically; refactor the resource drill-down to use them. - Give the transposed Nodes table a per-node min-width so it scrolls horizontally instead of squishing when there are many nodes. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Add a Storage entry to the Capacity section (/admin/capacity/storage) and move the PersistentVolumeClaim-by-StorageClass panel off the Cluster page onto its own page, with loading and empty states. The per-StorageClass drill-down is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
8722855 to
089162e
Compare
Worker-node VMs of a tenant Kubernetes cluster (and their virt-launcher
pods) are created by Cluster API, not the cozystack lineage controller,
so they carry only CAPI labels and lack the
apps.cozystack.io/application.{kind,name} labels that workloadOwner keys
on. As a result they showed as plain text in the resource-consumer
Workload column instead of linking to the owning app.
Map pods carrying cluster.x-k8s.io/cluster-name=kubernetes-<instance>
back to the kubernetes app instance <instance>, so worker VMs of the
same cluster group together and deep-link to its Console page, matching
how a standalone VMInstance is linked today.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
apps/console/src/routes/AdminPage.routing.test.tsx (1)
43-69: ⚡ Quick winAdd a backupclasses-only direct-capacity regression case.
The suite never exercises
{ nodes: false, backupclasses: true }hitting/capacity/cluster, which is the case that validates the per-area route split. Adding that case here would keep this auth boundary from regressing silently.Proposed test
describe("AdminPage routing & access gate", () => { it("renders the Cluster Usage page at /cluster-usage for an operator", async () => { renderWithK8sProvider(<AdminPage />, { client: makeClient({ nodes: true }), initialRoute: "/capacity/cluster", }) expect(await screen.findByText("Cluster")).toBeInTheDocument() }) @@ it("blocks direct access with a 403 notice when the user has neither admin area", async () => { renderWithK8sProvider(<AdminPage />, { client: makeClient({ nodes: false, backupclasses: false }), initialRoute: "/capacity/cluster", }) expect( await screen.findByText(/you do not have permission to access the admin portal/i), ).toBeInTheDocument() }) + + it("blocks direct capacity access when only backup class admin is allowed", async () => { + renderWithK8sProvider(<AdminPage />, { + client: makeClient({ nodes: false, backupclasses: true }), + initialRoute: "/capacity/cluster", + }) + expect( + await screen.findByText(/you do not have permission/i), + ).toBeInTheDocument() + }) })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/console/src/routes/AdminPage.routing.test.tsx` around lines 43 - 69, Add a new test in AdminPage.routing.test.tsx alongside the existing cases to cover the { nodes: false, backupclasses: true } scenario: use renderWithK8sProvider(<AdminPage />) with client: makeClient({ nodes: false, backupclasses: true }) and initialRoute "/capacity/cluster", then assert that the page either renders the Cluster content (if backupclasses grants access) or shows the proper 403 notice (matching the existing permission message) depending on intended behavior; place this test near the other routing tests so the per-area route split for AdminPage is exercised and protected from regressions.apps/console/src/routes/ClusterUsageResourcePage.tsx (1)
28-34: 💤 Low value
UsageRow.podsis computed but never displayed.
podsis tracked in the aggregation (incremented at Lines 80 and 83) yet the table only renders namespace, workload, and requested. Either surface it as a column or drop the field to avoid dead state.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/console/src/routes/ClusterUsageResourcePage.tsx` around lines 28 - 34, The UsageRow type includes a pods field that is incremented during aggregation (see the code that updates UsageRow.pods) but never rendered; either remove pods from the UsageRow type and stop incrementing it, or add a "Pods" column to the table output that displays row.pods. To fix, in ClusterUsageResourcePage.tsx locate the UsageRow definition and the table/rows rendering (where namespace, workload/kind/name and requested are rendered) and either (A) delete the pods property and remove the increments, or (B) add a new column header "Pods" and render the pods value for each UsageRow (e.g., include row.pods in the cell renderer alongside the other fields) so the tracked value is surfaced. Ensure consistency between the aggregation logic (where pods is incremented) and the table columns.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/console/src/components/cluster-usage/ClusterUsageTable.tsx`:
- Around line 138-148: Update the top doc comment in ClusterUsageTable.tsx to
reflect the refactor that moved Status, Roles, and Age into each node's column
header (see the code that renders node headers around where Status/Roles/Age are
created) so it no longer says those are rows; explicitly state that only CPU,
Memory, and discovered extended-resource keys are rendered as attributeRows and
that Status/Roles/Age appear in the per-node header columns and the first column
remains a sticky label column used for attribute row labels.
In `@apps/console/src/routes/AdminPage.tsx`:
- Around line 54-67: The capacity routes are still registered even when
canClusterUsage is false, allowing unauthorized access; modify the route
registration in AdminPage to guard the entire capacity subtree by either
wrapping the capacity Route elements with a guard component (e.g., render
<BackupClassAdminGuard> around the Routes that include ClusterUsagePage,
ClusterUsageResourcePage, StorageClassUsagePage, StoragePage, NodesPage) or
conditionally register those Route entries only when canClusterUsage is true,
ensuring the Navigate default remains unchanged but the capacity/* routes are
not mounted unless canClusterUsage is true.
In `@apps/console/src/routes/ClusterUsageResourcePage.tsx`:
- Around line 69-86: In ClusterUsageResourcePage.tsx, when iterating podsList
items, skip terminal and unscheduled/orphaned pods by checking pod.status.phase
and pod.spec.nodeName (or other indicator used in aggregate.ts) before computing
requested — specifically ignore pods where pod.status.phase === "Succeeded" ||
pod.status.phase === "Failed" and those considered unscheduled/orphaned so the
drill-down matches aggregate.ts; keep using podResourceRequest,
TENANT_NAMESPACE_PREFIX, workloadOwner, byKey, and totalRequested. Also wire the
accumulated UsageRow.pods into the rendered table (add a "Pods" column or render
the pods count where UsageRow is displayed) so the pods counter that is
incremented is actually shown.
---
Nitpick comments:
In `@apps/console/src/routes/AdminPage.routing.test.tsx`:
- Around line 43-69: Add a new test in AdminPage.routing.test.tsx alongside the
existing cases to cover the { nodes: false, backupclasses: true } scenario: use
renderWithK8sProvider(<AdminPage />) with client: makeClient({ nodes: false,
backupclasses: true }) and initialRoute "/capacity/cluster", then assert that
the page either renders the Cluster content (if backupclasses grants access) or
shows the proper 403 notice (matching the existing permission message) depending
on intended behavior; place this test near the other routing tests so the
per-area route split for AdminPage is exercised and protected from regressions.
In `@apps/console/src/routes/ClusterUsageResourcePage.tsx`:
- Around line 28-34: The UsageRow type includes a pods field that is incremented
during aggregation (see the code that updates UsageRow.pods) but never rendered;
either remove pods from the UsageRow type and stop incrementing it, or add a
"Pods" column to the table output that displays row.pods. To fix, in
ClusterUsageResourcePage.tsx locate the UsageRow definition and the table/rows
rendering (where namespace, workload/kind/name and requested are rendered) and
either (A) delete the pods property and remove the increments, or (B) add a new
column header "Pods" and render the pods value for each UsageRow (e.g., include
row.pods in the cell renderer alongside the other fields) so the tracked value
is surfaced. Ensure consistency between the aggregation logic (where pods is
incremented) and the table columns.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 137a756f-ec76-4be4-8b4e-67d67b57b277
📒 Files selected for processing (32)
apps/console/src/App.tsxapps/console/src/components/QuotaDisplay.tsxapps/console/src/components/WorkloadCell.tsxapps/console/src/components/cluster-usage/ClusterStorageSection.test.tsxapps/console/src/components/cluster-usage/ClusterStorageSection.tsxapps/console/src/components/cluster-usage/ClusterUsageAggregates.test.tsxapps/console/src/components/cluster-usage/ClusterUsageAggregates.tsxapps/console/src/components/cluster-usage/ClusterUsageGauges.tsxapps/console/src/components/cluster-usage/ClusterUsageTable.test.tsxapps/console/src/components/cluster-usage/ClusterUsageTable.tsxapps/console/src/lib/cluster-usage/types.tsapps/console/src/lib/workload.test.tsapps/console/src/lib/workload.tsapps/console/src/routes/AdminPage.routing.test.tsxapps/console/src/routes/AdminPage.tsxapps/console/src/routes/BackupClassCreatePage.tsxapps/console/src/routes/BackupClassDetailPage.tsxapps/console/src/routes/BackupClassEditPage.tsxapps/console/src/routes/BackupClassListPage.tsxapps/console/src/routes/ClusterUsagePage.test.tsxapps/console/src/routes/ClusterUsagePage.tsxapps/console/src/routes/ClusterUsageResourcePage.test.tsxapps/console/src/routes/ClusterUsageResourcePage.tsxapps/console/src/routes/ConsolePage.routing.test.tsxapps/console/src/routes/ConsolePage.tsxapps/console/src/routes/NodesPage.test.tsxapps/console/src/routes/NodesPage.tsxapps/console/src/routes/StorageClassUsagePage.test.tsxapps/console/src/routes/StorageClassUsagePage.tsxapps/console/src/routes/StoragePage.tsxapps/console/src/routes/sidebar-sections.test.tsxapps/console/src/routes/sidebar-sections.tsx
💤 Files with no reviewable changes (1)
- apps/console/src/routes/ConsolePage.tsx
| /** | ||
| * Per-node table rendered below the aggregate panel. Fixed columns | ||
| * (Name, Status, Roles, CPU, Memory) plus one column per full | ||
| * extended-resource key found in the cluster, then Age. Headers click | ||
| * to sort; default sort is Name ascending. A filter input above the | ||
| * table filters by name and roles substring. | ||
| * Per-node table, transposed: each NODE is a column and each attribute | ||
| * (Status, Roles, CPU, Memory, every discovered extended-resource key, then | ||
| * Age) is a row, read top-to-bottom. The first column is a sticky label | ||
| * column; node columns scroll horizontally when they overflow. The filter | ||
| * input narrows which node columns are shown (by name or role). | ||
| * | ||
| * NotReady nodes show em dashes for CPU / Memory because status.capacity | ||
| * stops being authoritative; the rest of the row remains visible so the | ||
| * row remains a useful pointer for the operator. When pods-list failed | ||
| * cluster-wide, Requested values in every cell are replaced by an em | ||
| * dash with a tooltip explaining the missing permission. | ||
| * stops being authoritative. When the cluster-wide pods list failed, | ||
| * Requested figures are replaced by an em dash with an explanatory tooltip. | ||
| */ |
There was a problem hiding this comment.
Stale doc comment: Status/Roles/Age are headers, not rows.
The comment states these render as rows top-to-bottom, but the refactor moves Status, Roles, and Age into each node's column header (lines 217-219); only CPU/Memory/extended remain as attributeRows.
📝 Suggested wording fix
/**
* Per-node table, transposed: each NODE is a column and each attribute
- * (Status, Roles, CPU, Memory, every discovered extended-resource key, then
- * Age) is a row, read top-to-bottom. The first column is a sticky label
- * column; node columns scroll horizontally when they overflow. The filter
- * input narrows which node columns are shown (by name or role).
+ * (CPU, Memory, then every discovered extended-resource key) is a row, read
+ * top-to-bottom. Node metadata (Status, Roles, Age) lives in each node's
+ * column header. The first column is a sticky label column; node columns
+ * scroll horizontally when they overflow. The filter input narrows which
+ * node columns are shown (by name or role).📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Per-node table rendered below the aggregate panel. Fixed columns | |
| * (Name, Status, Roles, CPU, Memory) plus one column per full | |
| * extended-resource key found in the cluster, then Age. Headers click | |
| * to sort; default sort is Name ascending. A filter input above the | |
| * table filters by name and roles substring. | |
| * Per-node table, transposed: each NODE is a column and each attribute | |
| * (Status, Roles, CPU, Memory, every discovered extended-resource key, then | |
| * Age) is a row, read top-to-bottom. The first column is a sticky label | |
| * column; node columns scroll horizontally when they overflow. The filter | |
| * input narrows which node columns are shown (by name or role). | |
| * | |
| * NotReady nodes show em dashes for CPU / Memory because status.capacity | |
| * stops being authoritative; the rest of the row remains visible so the | |
| * row remains a useful pointer for the operator. When pods-list failed | |
| * cluster-wide, Requested values in every cell are replaced by an em | |
| * dash with a tooltip explaining the missing permission. | |
| * stops being authoritative. When the cluster-wide pods list failed, | |
| * Requested figures are replaced by an em dash with an explanatory tooltip. | |
| */ | |
| /** | |
| * Per-node table, transposed: each NODE is a column and each attribute | |
| * (CPU, Memory, then every discovered extended-resource key) is a row, read | |
| * top-to-bottom. Node metadata (Status, Roles, Age) lives in each node's | |
| * column header. The first column is a sticky label column; node columns | |
| * scroll horizontally when they overflow. The filter input narrows which | |
| * node columns are shown (by name or role). | |
| */ |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/console/src/components/cluster-usage/ClusterUsageTable.tsx` around lines
138 - 148, Update the top doc comment in ClusterUsageTable.tsx to reflect the
refactor that moved Status, Roles, and Age into each node's column header (see
the code that renders node headers around where Status/Roles/Age are created) so
it no longer says those are rows; explicitly state that only CPU, Memory, and
discovered extended-resource keys are rendered as attributeRows and that
Status/Roles/Age appear in the per-node header columns and the first column
remains a sticky label column used for attribute row labels.
…e fix)
The cluster.x-k8s.io/cluster-name special-case in workloadOwner was a UI
band-aid for worker-node VMs that lacked lineage labels. cozystack#2779
now stamps apps.cozystack.io/application.{group,kind,name} on the worker
VM template, so those pods carry the standard lineage labels and the
generic workloadOwner path resolves them — the special-case is dead code.
This reverts commit d3fd9ac.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
The resource-consumer drill-down linked each workload to its application's overview page. Point the link at the app's Workloads tab instead, so a click lands directly on the workloads of the selected resource. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
The Admin portal lets a user in if they can use either area, so a backup-only operator (backupclasses/update but not nodes/list) passed the portal gate and could open the Capacity pages by direct URL — some of which then issued cluster-wide pod/PVC lists they were never gated for. Wrap the capacity/* routes in a CapacityAdminGuard that mirrors the Backup Classes guard and gates on nodes/list, and route useAdminAccess through the shared useClusterUsageAccess hook so the gate and the guard agree. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
ClusterStorageSection handled only loading and empty states, so a failed or forbidden PersistentVolumeClaim list rendered as "No persistent volume claims found" — indistinguishable from an actually-empty cluster. Add an error branch that shows a permission notice on 403 and the failure message otherwise. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
ClusterUsageAggregates now renders the resources table plus ClusterUsageGauges and no longer uses ResourceCard, leaving the component and its test with no production consumer. Remove both. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
workloadOwner also tries the app.kubernetes.io/name label between instance and the bare-name fallback; the ClusterUsageResourcePage comment omitted it. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
…egate Clicking a resource's cluster-wide Requested figure opened a drill-down that counted differently: it summed requests-or-limits, with no scheduled or terminal-phase filter, so a limits-only or terminal/unscheduled pod inflated the breakdown that was meant to itemize that number. Share the aggregate's inclusion rule (scheduled to a known node, non-terminal, requests only) via a podCountsTowardRequested helper and apply it in both places. The drill-down stays tenant-scoped, now stated in the copy as the tenant portion of the cluster figure rather than silently disagreeing with it. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
Admin portal: lifts the cluster-wide operator areas (Capacity — Cluster/Nodes/Storage — and Backup Classes) out of the tenant Console into a dedicated /admin portal, with per-resource and per-StorageClass consumer drill-downs. typecheck and the full vitest suite (275 tests) pass. Each area is gated by its own fail-closed SSAR guard (nodes/list, backupclasses/update), the cross-area direct-URL hole is closed and tested, the resource drill-down reconciles with the cluster aggregate via a shared inclusion rule, and the storage panel distinguishes a forbidden list from an empty one. LGTM.
Summary
Introduces a dedicated Admin portal (third top-nav entry beside Marketplace and Console) for cluster-wide operator views, and moves those views out of the tenant-facing Console. The portal is gated by access: a user sees it only if they can use at least one area (
nodes/listfor Capacity,backupclasses/updatefor Backup Classes); the nav tab is hidden otherwise and direct-URL access renders a 403.Capacity
/admin/capacity/cluster) — cluster-wide resource accounting laid out top-to-bottom: one row per resource (CPU, Memory, Storage, Pods, and each discovered extended resource such as GPUs) with Capacity / Allocatable / Requested / Used, plus circular allocation gauges (reusing the per-tenant quota rings). Each resource links to a per-resource drill-down listing the consuming Workloads (owning application, grouped from pod labels, restricted to tenant namespaces) with a deep-link to the app in Console./admin/capacity/nodes) — the per-node breakdown, transposed so each node is a column (Status / Roles / Age in the column header, resources as rows). Resource labels link to the same per-resource drill-down; a name/role filter and a per-node min-width (horizontal scroll) keep large clusters usable./admin/capacity/storage) — PersistentVolumeClaims across tenant namespaces aggregated by StorageClass (claim count, requested, bound). Each StorageClass drills down to the consuming Workloads and their summed requested storage.Backups
backupclasses/update. The per-tenant Backups screens (Plans / Backup Jobs / Backups / Restore Jobs) stay in Console.Console
Test plan
pnpm -r typecheckpassespnpm --filter @cozystack/console testpasses (272 tests, incl. routing/access gates, the transposed node table, both drill-downs, gauges, and the storage aggregation)pnpm build:consolepassesNote: repo-wide
eslintreports pre-existing errors in files untouched by this PR; all files changed here lint clean.Summary by CodeRabbit