Skip to content

Commit c533928

Browse files
fix(dashboard): tell a 403 from a broken page on capacity drill-downs
ClusterUsageResourcePage and StorageClassUsagePage rendered the same "Failed to load..." text for every list error, so a user who can list nodes but not pods or PVCs sees what looks like a broken page. Check K8sApiError.status the way ClusterStorageSection already does in the same Capacity area. Fixes #3107 Signed-off-by: Myasnikov Daniil <[email protected]>
1 parent c96ebe7 commit c533928

4 files changed

Lines changed: 67 additions & 6 deletions

File tree

‎packages/system/dashboard/images/console/apps/console/src/routes/ClusterUsageResourcePage.test.tsx‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, expect, vi, beforeAll } from "vitest"
22
import { screen, within } from "@testing-library/react"
33
import { Route, Routes } from "react-router"
4-
import { K8sClient, type K8sList } from "@cozystack/k8s-client"
4+
import { K8sClient, K8sApiError, type K8sList } from "@cozystack/k8s-client"
55
import { ClusterUsageResourcePage } from "./ClusterUsageResourcePage.tsx"
66
import { aggregateNodeResources } from "../lib/cluster-usage/aggregate.ts"
77
import { humanizeCpu } from "../lib/k8s-quantity.ts"
@@ -84,6 +84,22 @@ function makeClient(
8484
return client
8585
}
8686

87+
// Only the pod list fails. Rejecting every list would render the same notice
88+
// no matter which request broke, so the test could not tell them apart.
89+
function makeFailingClient(error: Error): K8sClient {
90+
const client = new K8sClient()
91+
vi.spyOn(client, "list").mockImplementation(async (_g, _v, plural) => {
92+
if (plural === "pods") throw error
93+
return {
94+
apiVersion: "v1",
95+
kind: `${plural}List`,
96+
metadata: {},
97+
items: plural === "nodes" ? DEFAULT_NODES : [],
98+
} as K8sList<unknown>
99+
})
100+
return client
101+
}
102+
87103
function renderResource(client: K8sClient, resource: string) {
88104
return renderWithK8sProvider(
89105
<TenantProvider>
@@ -196,6 +212,20 @@ describe("ClusterUsageResourcePage", () => {
196212
).toBeInTheDocument()
197213
})
198214

215+
it("shows a permission notice when the pod list is forbidden", async () => {
216+
const client = makeFailingClient(new K8sApiError(403, { message: "forbidden" }))
217+
renderResource(client, GPU)
218+
expect(
219+
await screen.findByText(/you do not have permission to view cluster usage/i),
220+
).toBeInTheDocument()
221+
})
222+
223+
it("shows a failure notice when the pod list errors", async () => {
224+
const client = makeFailingClient(new K8sApiError(500, { message: "boom" }))
225+
renderResource(client, GPU)
226+
expect(await screen.findByText(/failed to load cluster usage: boom/i)).toBeInTheDocument()
227+
})
228+
199229
it("renders the resource key as the page heading", async () => {
200230
const client = makeClient([])
201231
renderResource(client, GPU)

‎packages/system/dashboard/images/console/apps/console/src/routes/ClusterUsageResourcePage.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useMemo } from "react"
22
import { Link, useParams } from "react-router"
33
import { Section, Spinner } from "@cozystack/ui"
4-
import { useK8sList } from "@cozystack/k8s-client"
4+
import { useK8sList, K8sApiError } from "@cozystack/k8s-client"
55
import { ChevronLeft } from "lucide-react"
66
import { parseQuantity, humanizeBytes, humanizeCpu } from "../lib/k8s-quantity.ts"
77
import { workloadOwner } from "../lib/workload.ts"
@@ -121,7 +121,9 @@ export function ClusterUsageResourcePage() {
121121
) : error ? (
122122
<Section>
123123
<div className="px-2 py-4 text-sm text-red-700">
124-
Failed to load cluster usage: {error.message}
124+
{error instanceof K8sApiError && error.status === 403
125+
? "You do not have permission to view cluster usage."
126+
: `Failed to load cluster usage: ${error.message}`}
125127
</div>
126128
</Section>
127129
) : rows.length === 0 ? (

‎packages/system/dashboard/images/console/apps/console/src/routes/StorageClassUsagePage.test.tsx‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, expect, vi, beforeAll } from "vitest"
22
import { screen, within } from "@testing-library/react"
33
import { Route, Routes } from "react-router"
4-
import { K8sClient, type K8sList } from "@cozystack/k8s-client"
4+
import { K8sClient, K8sApiError, type K8sList } from "@cozystack/k8s-client"
55
import { StorageClassUsagePage } from "./StorageClassUsagePage.tsx"
66
import { TenantProvider } from "../lib/tenant-context.tsx"
77
import { renderWithK8sProvider } from "../test-utils/render.tsx"
@@ -50,6 +50,17 @@ function makeClient(pvcs: unknown[], appDefs: unknown[] = []): K8sClient {
5050
return client
5151
}
5252

53+
// Only the PVC list fails. Rejecting every list would render the same notice
54+
// no matter which request broke, so the test could not tell them apart.
55+
function makeFailingClient(error: Error): K8sClient {
56+
const client = new K8sClient()
57+
vi.spyOn(client, "list").mockImplementation(async (_g, _v, plural) => {
58+
if (plural === "persistentvolumeclaims") throw error
59+
return { apiVersion: "v1", kind: `${plural}List`, metadata: {}, items: [] } as K8sList<unknown>
60+
})
61+
return client
62+
}
63+
5364
function renderPage(client: K8sClient, sc: string) {
5465
return renderWithK8sProvider(
5566
<TenantProvider>
@@ -116,6 +127,22 @@ describe("StorageClassUsagePage", () => {
116127
expect(await screen.findByText(/no tenant workloads use/i)).toBeInTheDocument()
117128
})
118129

130+
it("shows a permission notice when the PVC list is forbidden", async () => {
131+
const client = makeFailingClient(new K8sApiError(403, { message: "forbidden" }))
132+
renderPage(client, "replicated")
133+
expect(
134+
await screen.findByText(/you do not have permission to view persistent volume claims/i),
135+
).toBeInTheDocument()
136+
})
137+
138+
it("shows a failure notice when the PVC list errors", async () => {
139+
const client = makeFailingClient(new K8sApiError(500, { message: "boom" }))
140+
renderPage(client, "replicated")
141+
expect(
142+
await screen.findByText(/failed to load persistent volume claims: boom/i),
143+
).toBeInTheDocument()
144+
})
145+
119146
it("renders the storage class as the heading", async () => {
120147
const client = makeClient([])
121148
renderPage(client, "replicated")

‎packages/system/dashboard/images/console/apps/console/src/routes/StorageClassUsagePage.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useMemo } from "react"
22
import { Link, useParams } from "react-router"
33
import { Section, Spinner } from "@cozystack/ui"
4-
import { useK8sList } from "@cozystack/k8s-client"
4+
import { useK8sList, K8sApiError } from "@cozystack/k8s-client"
55
import { ChevronLeft } from "lucide-react"
66
import { parseQuantity, humanizeBytes } from "../lib/k8s-quantity.ts"
77
import { workloadOwner } from "../lib/workload.ts"
@@ -76,7 +76,9 @@ export function StorageClassUsagePage() {
7676
) : error ? (
7777
<Section>
7878
<div className="px-2 py-4 text-sm text-red-700">
79-
Failed to load persistent volume claims: {error.message}
79+
{error instanceof K8sApiError && error.status === 403
80+
? "You do not have permission to view persistent volume claims."
81+
: `Failed to load persistent volume claims: ${error.message}`}
8082
</div>
8183
</Section>
8284
) : rows.length === 0 ? (

0 commit comments

Comments
 (0)