Skip to content

Commit b83e5e2

Browse files
committed
feat(dashboard): move Repositories from the Marketplace to Admin
Connecting an External-Apps repository creates a cluster-scoped Tap and runs its charts in the management cluster. That is an operator action, not something a tenant does while browsing the catalog, so the entry belongs next to the other cluster-wide areas in the Admin portal rather than in the Marketplace sidebar. The page moves to /admin/taps under Administration. /marketplace/taps redirects there, since the :appName catch-all would otherwise render an order page for an application called "taps". Assisted-by: LLM Signed-off-by: Andrei Kvapil <[email protected]>
1 parent b99db14 commit b83e5e2

6 files changed

Lines changed: 60 additions & 9 deletions

File tree

‎packages/system/dashboard/images/console/apps/console/src/App.routing.test.tsx‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect, vi, beforeAll } from "vitest"
22
import { screen } from "@testing-library/react"
3+
import { useLocation } from "react-router"
34
import {
45
K8sClient,
56
type K8sList,
@@ -8,6 +9,12 @@ import {
89
import App from "./App.tsx"
910
import { renderWithK8sProvider } from "./test-utils/render.tsx"
1011

12+
// Echoes the router location so a redirect can be asserted by URL.
13+
function LocationProbe() {
14+
const { pathname } = useLocation()
15+
return <div data-testid="location">{pathname}</div>
16+
}
17+
1118
function makeClient(): K8sClient {
1219
const client = new K8sClient()
1320
vi.spyOn(client, "list").mockImplementation(async (_g, _v, plural) => {
@@ -70,4 +77,22 @@ describe("default landing", () => {
7077
await screen.findByRole("heading", { name: "Marketplace", level: 1 }),
7178
).toBeTruthy()
7279
})
80+
81+
it("sends the old Marketplace Repositories URL to the Admin portal", async () => {
82+
// Without the redirect the :appName catch-all would render an order page
83+
// for an application called "taps".
84+
const client = makeClient()
85+
renderWithK8sProvider(
86+
<>
87+
<App />
88+
<LocationProbe />
89+
</>,
90+
{ client, initialRoute: "/marketplace/taps" },
91+
)
92+
93+
expect(
94+
await screen.findByRole("heading", { name: "Repositories", level: 1 }),
95+
).toBeTruthy()
96+
expect(screen.getByTestId("location").textContent).toBe("/admin/taps")
97+
})
7398
})

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ describe("AdminPage routing & access gate", () => {
118118
).toBeInTheDocument()
119119
})
120120

121+
it("serves Repositories under Administration, with no operator area", async () => {
122+
renderWithK8sProvider(
123+
<TenantProvider>
124+
<AdminPage />
125+
</TenantProvider>,
126+
{ client: makeClient({ nodes: false, backupclasses: false }), initialRoute: "/taps" },
127+
)
128+
expect(
129+
await screen.findByRole("heading", { name: "Repositories" }),
130+
).toBeInTheDocument()
131+
})
132+
121133
it("guards capacity routes for a backup-only operator hitting a capacity URL", async () => {
122134
// The capacity area must stay closed without nodes/list, even though the
123135
// portal itself is now ungated.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ import { InfoRedirect } from "./InfoRedirect.tsx"
1414
import { ModulesPage } from "./ModulesPage.tsx"
1515
import { ExternalIpsPage } from "./ExternalIpsPage.tsx"
1616
import { TenantsPage } from "./TenantsPage.tsx"
17+
import { TapsPage } from "./TapsPage.tsx"
1718
import { ApplicationOrderPage } from "./ApplicationOrderPage.tsx"
1819
import { ApplicationEditRoute } from "./detail/ApplicationEditRoute.tsx"
1920
import { ApplicationDetailPage } from "./detail/ApplicationDetailPage.tsx"
2021
import { ApplicationListPage } from "./ApplicationListPage.tsx"
2122

2223
/**
2324
* Admin portal at /admin/*. Administration (Info, Modules, External IPs,
24-
* Tenants) is always accessible, so the portal itself carries no gate. The two
25+
* Repositories, Tenants) is always accessible, so the portal itself carries no gate. The two
2526
* cluster-wide operator areas keep their own independent permissions: Capacity
2627
* (nodes/list) and Backup Classes (backupclasses/update). Each is wrapped in a
2728
* layout guard that closes the direct-URL hole the sidebar already hides, so a
@@ -47,6 +48,7 @@ export function AdminPage() {
4748
<Route path="info" element={<InfoRedirect />} />
4849
<Route path="modules" element={<ModulesPage />} />
4950
<Route path="external-ips" element={<ExternalIpsPage />} />
51+
<Route path="taps" element={<TapsPage />} />
5052
<Route path="tenants" element={<TenantsPage />} />
5153
<Route path="new/:appName" element={<ApplicationOrderPage />} />
5254
<Route path=":plural/:name/edit" element={<ApplicationEditRoute />} />

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import { Route, Routes } from "react-router"
1+
import { Navigate, Route, Routes } from "react-router"
22
import { MarketplaceList } from "./MarketplaceList.tsx"
33
import { ApplicationOrderPage } from "./ApplicationOrderPage.tsx"
4-
import { TapsPage } from "./TapsPage.tsx"
54

65
export function MarketplacePage() {
76
return (
87
<Routes>
98
<Route index element={<MarketplaceList />} />
109
<Route path="c/:category" element={<MarketplaceList />} />
11-
{/* Static route must precede the :appName catch-all. */}
12-
<Route path="taps" element={<TapsPage />} />
10+
{/* Repositories moved to Admin; must precede the :appName catch-all. */}
11+
<Route path="taps" element={<Navigate to="/admin/taps" replace />} />
1312
<Route path=":appName" element={<ApplicationOrderPage />} />
1413
</Routes>
1514
)

‎packages/system/dashboard/images/console/apps/console/src/routes/sidebar-sections.test.tsx‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
useAdminSidebarSections,
1313
useCanSeeAdmin,
1414
useConsoleSidebarSections,
15+
useMarketplaceSidebarSections,
1516
} from "./sidebar-sections.tsx"
1617

1718
const emptyAppDefList: K8sList<unknown> = {
@@ -108,6 +109,17 @@ describe("useConsoleSidebarSections — admin areas moved out", () => {
108109
})
109110
})
110111

112+
describe("useMarketplaceSidebarSections", () => {
113+
it("leaves Repositories to the Admin portal", () => {
114+
const client = makeClient({})
115+
const { result } = renderHook(() => useMarketplaceSidebarSections(), {
116+
wrapper: makeWrapper(client),
117+
})
118+
expect(findItem(result.current, "Marketplace")?.to).toBe("/marketplace")
119+
expect(findItem(result.current, "Repositories")).toBeUndefined()
120+
})
121+
})
122+
111123
describe("useAdminSidebarSections", () => {
112124
it("always shows Administration, even with no operator permissions", async () => {
113125
const client = makeClient({ nodes: false, backupclasses: false })
@@ -120,6 +132,7 @@ describe("useAdminSidebarSections", () => {
120132
// Per-tenant Info moved into the Tenants tree rows — no sidebar entry.
121133
expect(findItem(result.current, "Info")).toBeUndefined()
122134
expect(findItem(result.current, "External IPs")?.to).toBe("/admin/external-ips")
135+
expect(findItem(result.current, "Repositories")?.to).toBe("/admin/taps")
123136
// No operator area leaks in while the gates deny.
124137
await waitFor(() => expect(client.create).toHaveBeenCalled())
125138
expect(findItem(result.current, "Cluster")).toBeUndefined()

‎packages/system/dashboard/images/console/apps/console/src/routes/sidebar-sections.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ export function useMarketplaceSidebarSections(): SidebarSection[] {
5757
title: "Marketplace",
5858
items: [
5959
{ label: "Marketplace", to: "/marketplace", end: true, icon: LayoutGrid },
60-
{ label: "Repositories", to: "/marketplace/taps", icon: Package },
6160
...ordered.map((category) => ({
6261
label: category,
6362
to: `/marketplace/c/${encodeURIComponent(category)}`,
@@ -155,7 +154,7 @@ export function useAdminAccess(): {
155154

156155
/**
157156
* The Admin tab is always visible: Administration (Info, Modules, External IPs,
158-
* Tenants) needs no special permission. The gated Capacity/Backup-Classes areas
157+
* Repositories, Tenants) needs no special permission. The gated Capacity/Backup-Classes areas
159158
* keep their own per-area guards inside the portal.
160159
*/
161160
export function useCanSeeAdmin(): boolean {
@@ -164,8 +163,8 @@ export function useCanSeeAdmin(): boolean {
164163

165164
/**
166165
* Admin sidebar: Administration first (always visible, no permission needed —
167-
* Tenants, Modules, External IPs; per-tenant Info is a row action in the
168-
* Tenants tree), then the cluster-wide operator areas (Capacity and Backup
166+
* Tenants, Modules, External IPs, Repositories; per-tenant Info is a row action
167+
* in the Tenants tree), then the cluster-wide operator areas (Capacity and Backup
169168
* Classes). Each operator area is gated by its own permission so the sidebar
170169
* never shows an area the user cannot open.
171170
*/
@@ -202,6 +201,7 @@ export function useAdminSidebarSections(): SidebarSection[] {
202201
alsoMatch: modulePaths ? modulePaths.split(",") : undefined,
203202
},
204203
{ label: "External IPs", to: "/admin/external-ips", icon: Globe },
204+
{ label: "Repositories", to: "/admin/taps", icon: Package },
205205
],
206206
},
207207
]

0 commit comments

Comments
 (0)