Skip to content

Commit 3ccd9e4

Browse files
fix(dashboard): re-assert the URL tenant on every navigation to it
Adopting the tenant once per distinct value left the URL and the context disagreeing after the picker switched tenant and history returned to the entry, which puts the wrong tenant's resource behind a Delete confirm that shows only the relative name -- the same hazard the query param was added to close. Key the adoption on the navigation instead: the picker still wins between navigations, and arriving at the entry again asserts what the URL says. Assisted-By: Claude <[email protected]> Signed-off-by: Myasnikov Daniil <[email protected]>
1 parent 1896801 commit 3ccd9e4

2 files changed

Lines changed: 53 additions & 9 deletions

File tree

‎packages/system/dashboard/images/console/apps/console/src/lib/tenant-context.test.tsx‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { describe, it, expect, vi, beforeEach } from "vitest"
22
import { screen } from "@testing-library/react"
3+
import userEvent from "@testing-library/user-event"
4+
import { useNavigate } from "react-router"
35
import { K8sClient, type K8sList } from "@cozystack/k8s-client"
46
import { TenantProvider, useTenantContext, useTenantFromUrl } from "./tenant-context.tsx"
57
import { SELECTED_TENANT_KEY } from "./constants.ts"
@@ -32,8 +34,22 @@ function makeClient(): K8sClient {
3234

3335
function Probe() {
3436
useTenantFromUrl()
35-
const { tenantNamespace } = useTenantContext()
36-
return <output>{tenantNamespace ?? "none"}</output>
37+
const { tenantNamespace, selectTenant } = useTenantContext()
38+
const navigate = useNavigate()
39+
return (
40+
<>
41+
<output>{tenantNamespace ?? "none"}</output>
42+
<button type="button" onClick={() => selectTenant("globex")}>
43+
pick globex
44+
</button>
45+
<button type="button" onClick={() => navigate("/console")}>
46+
leave
47+
</button>
48+
<button type="button" onClick={() => navigate(-1)}>
49+
back
50+
</button>
51+
</>
52+
)
3753
}
3854

3955
function renderAt(route: string) {
@@ -71,6 +87,29 @@ describe("useTenantFromUrl", () => {
7187
expect(window.localStorage.getItem(SELECTED_TENANT_KEY)).toBe("acme")
7288
})
7389

90+
// The picker switches tenant without leaving the page, so re-applying the
91+
// param on every render would drag the user back to it.
92+
it("does not drag the picker back to the tenant in the URL", async () => {
93+
renderAt("/console/vminstances/demo?tenant=acme")
94+
await screen.findByText("tenant-acme")
95+
96+
await userEvent.click(screen.getByRole("button", { name: "pick globex" }))
97+
98+
expect(await screen.findByText("tenant-globex")).toBeInTheDocument()
99+
})
100+
101+
it("asserts the URL tenant again when the entry is revisited", async () => {
102+
renderAt("/console/vminstances/demo?tenant=acme")
103+
await screen.findByText("tenant-acme")
104+
await userEvent.click(screen.getByRole("button", { name: "pick globex" }))
105+
await screen.findByText("tenant-globex")
106+
107+
await userEvent.click(screen.getByRole("button", { name: "leave" }))
108+
await userEvent.click(screen.getByRole("button", { name: "back" }))
109+
110+
expect(await screen.findByText("tenant-acme")).toBeInTheDocument()
111+
})
112+
74113
it("leaves the stored tenant alone when the URL names none", async () => {
75114
window.localStorage.setItem(SELECTED_TENANT_KEY, "globex")
76115

‎packages/system/dashboard/images/console/apps/console/src/lib/tenant-context.tsx‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
useState,
88
type ReactNode,
99
} from "react"
10-
import { useSearchParams } from "react-router"
10+
import { useLocation, useSearchParams } from "react-router"
1111
import { useK8sList } from "@cozystack/k8s-client"
1212
import type { TenantNamespace } from "@cozystack/types"
1313
import { SELECTED_TENANT_KEY, TENANT_NAMESPACE_PREFIX } from "./constants.ts"
@@ -107,20 +107,25 @@ export function useTenantContext(): TenantContextValue {
107107
* new tab, a bookmarked or pasted URL — never run React's onClick, and two
108108
* tenants under different parents can share a relative CR name, so the name
109109
* alone would resolve against whichever tenant happened to be selected last.
110-
* Applied once per distinct value, so the provider's own fallback stays free
111-
* to reject a tenant the user cannot see.
110+
* Applied once per navigation rather than on every render, so the tenant picker
111+
* -- which switches the tenant without leaving the page -- is not dragged back
112+
* to the URL under the user, while returning to the entry through history
113+
* asserts it again. The provider's own fallback stays free to reject a tenant
114+
* the user cannot see.
112115
*/
113116
export function useTenantFromUrl() {
117+
const { key } = useLocation()
114118
const [params] = useSearchParams()
115119
const wanted = params.get("tenant")
116120
const { selectTenant } = useTenantContext()
117-
const applied = useRef<string | null>(null)
121+
const navigated = useRef<string | null>(null)
118122

119123
useEffect(() => {
120-
if (!wanted || applied.current === wanted) return
121-
applied.current = wanted
124+
const previous = navigated.current
125+
navigated.current = key
126+
if (!wanted || previous === key) return
122127
selectTenant(wanted)
123-
}, [wanted, selectTenant])
128+
}, [key, wanted, selectTenant])
124129
}
125130

126131
/**

0 commit comments

Comments
 (0)