diff --git a/packages/system/dashboard/images/console/apps/console/src/routes/sidebar-sections.test.tsx b/packages/system/dashboard/images/console/apps/console/src/routes/sidebar-sections.test.tsx index ae8a623bf..efb69dc7b 100644 --- a/packages/system/dashboard/images/console/apps/console/src/routes/sidebar-sections.test.tsx +++ b/packages/system/dashboard/images/console/apps/console/src/routes/sidebar-sections.test.tsx @@ -24,9 +24,12 @@ const emptyAppDefList: K8sList = { // The admin gates issue two SSARs (nodes/list for Cluster Usage, // backupclasses/update for Backup Classes); answer each by requested resource. -function makeClient(allow: Record): K8sClient { +function makeClient( + allow: Record, + appDefs: K8sList = emptyAppDefList, +): K8sClient { const client = new K8sClient() - vi.spyOn(client, "list").mockResolvedValue(emptyAppDefList as K8sList) + vi.spyOn(client, "list").mockResolvedValue(appDefs as K8sList) vi.spyOn(client, "create").mockImplementation(async (_g, _v, _p, body) => { const resource = (body as SelfSubjectAccessReview).spec?.resourceAttributes?.resource ?? "" @@ -109,6 +112,59 @@ describe("useConsoleSidebarSections — admin areas moved out", () => { }) }) +function appDef(name: string, kind: string, category: string, module = false) { + return { + apiVersion: "cozystack.io/v1alpha1", + kind: "ApplicationDefinition", + metadata: { name }, + spec: { + application: { kind, plural: `${name}s` }, + dashboard: { category, ...(module ? { module: true } : {}) }, + }, + } +} + +// A tapped repository brings its own category next to the built-in ones. +const tappedAppDefs: K8sList = { + ...emptyAppDefList, + items: [ + appDef("postgres", "Postgres", "PaaS"), + appDef("vminstance", "VMInstance", "IaaS"), + appDef("oberonvm", "OberonVM", "Paleocomputing"), + appDef("ingress", "Ingress", "Administration", true), + appDef("seaweedfs", "SeaweedFS", "Administration"), + ], +} + +describe("custom categories from tapped repositories", () => { + it("lists a tapped category in the marketplace sidebar after the built-in ones", async () => { + const client = makeClient({}, tappedAppDefs) + const { result } = renderHook(() => useMarketplaceSidebarSections(), { + wrapper: makeWrapper(client), + }) + await waitFor(() => + expect(findItem(result.current, "Paleocomputing")?.to).toBe( + "/marketplace/c/Paleocomputing", + ), + ) + const labels = result.current[0].items.map((i) => i.label) + expect(labels).toEqual(["Marketplace", "IaaS", "PaaS", "Paleocomputing"]) + }) + + it("gives a tapped category its own console section and keeps Administration out", async () => { + const client = makeClient({}, tappedAppDefs) + const { result } = renderHook(() => useConsoleSidebarSections(), { + wrapper: makeWrapper(client), + }) + await waitFor(() => expect(sectionIndex(result.current, "Paleocomputing")).not.toBe(-1)) + expect(hasItemTo(result.current, "/console/oberonvms")).toBe(true) + expect(sectionIndex(result.current, "PaaS")).toBeLessThan( + sectionIndex(result.current, "Paleocomputing"), + ) + expect(sectionIndex(result.current, "Administration")).toBe(-1) + }) +}) + describe("useMarketplaceSidebarSections", () => { it("leaves Repositories to the Admin portal", () => { const client = makeClient({}) diff --git a/packages/system/dashboard/images/console/apps/console/src/routes/sidebar-sections.tsx b/packages/system/dashboard/images/console/apps/console/src/routes/sidebar-sections.tsx index f1db4e578..d5d100813 100644 --- a/packages/system/dashboard/images/console/apps/console/src/routes/sidebar-sections.tsx +++ b/packages/system/dashboard/images/console/apps/console/src/routes/sidebar-sections.tsx @@ -29,7 +29,8 @@ import { } from "../lib/sidebar-icons.tsx" import type { ComponentType } from "react" -const MARKETPLACE_CATEGORIES = ["IaaS", "PaaS", "NaaS"] +// Lives in the Admin portal; any other category, tapped ones included, gets an entry. +const ADMIN_CATEGORY = "Administration" const CATEGORY_ICON: Record = { IaaS: Cloud, PaaS: Database, @@ -38,7 +39,8 @@ const CATEGORY_ICON: Record = { /** * Marketplace sidebar: a flat list of filters — "All applications" followed - * by the three categories. Category links rely on pathname-based matching + * by every category in {@link groupByCategory} order (IaaS, PaaS, NaaS, then + * the rest alphabetically), Administration excepted. Category links rely on pathname-based matching * (`/marketplace/c/`) so NavLink correctly highlights the active * entry; see MarketplaceList for the counterpart. */ @@ -47,10 +49,9 @@ export function useMarketplaceSidebarSections(): SidebarSection[] { const grouped = useMemo(() => groupByCategory(data), [data]) return useMemo(() => { - const available = grouped + const ordered = grouped .map((g) => g.category) - .filter((c) => MARKETPLACE_CATEGORIES.includes(c)) - const ordered = MARKETPLACE_CATEGORIES.filter((c) => available.includes(c)) + .filter((c) => c !== ADMIN_CATEGORY) return [ { @@ -70,7 +71,8 @@ export function useMarketplaceSidebarSections(): SidebarSection[] { /** * Console sidebar: every non-module, non-Tenant ApplicationDefinition in the - * cluster as a separate entry grouped by IaaS / PaaS / NaaS, plus the per-tenant + * cluster as a separate entry grouped by category in {@link groupByCategory} + * order, Administration excepted, plus the per-tenant * Backups section. Administration (Info, Modules, External IPs, Tenants) now * lives in the Admin portal — see {@link useAdminSidebarSections}. */ @@ -79,13 +81,7 @@ export function useConsoleSidebarSections(): SidebarSection[] { const grouped = useMemo(() => groupByCategory(data), [data]) return useMemo(() => { - const sorted = [...grouped] - .filter(({ category }) => MARKETPLACE_CATEGORIES.includes(category)) - .sort( - (a, b) => - MARKETPLACE_CATEGORIES.indexOf(a.category) - - MARKETPLACE_CATEGORIES.indexOf(b.category), - ) + const sorted = grouped.filter(({ category }) => category !== ADMIN_CATEGORY) const categorySections: SidebarSection[] = sorted.map(({ category, items }) => ({ title: category,