diff --git a/packages/core/src/common/k8s-api/endpoints/namespace.api.ts b/packages/core/src/common/k8s-api/endpoints/namespace.api.ts index 456cd8bc6c..c27a233d45 100644 --- a/packages/core/src/common/k8s-api/endpoints/namespace.api.ts +++ b/packages/core/src/common/k8s-api/endpoints/namespace.api.ts @@ -34,9 +34,19 @@ export class Namespace extends KubeObject< return this.status?.phase ?? "-"; } - isSubnamespace(){ + isSubnamespace() { return this.getAnnotations().find(annotation => annotation.includes("hnc.x-k8s.io/subnamespace-of")); } + + isChildOf(parentName: string) { + this.getLabels().find(label => label === `${parentName}.tree.hnc.x-k8s.io/depth=1`); + } + + isControlledByHNC() { + const hierarchicalNamesaceControllerLabel = "hnc.x-k8s.io/included-namespace=true"; + + return this.getLabels().find(label => label === hierarchicalNamesaceControllerLabel); + } } export class NamespaceApi extends KubeApi { diff --git a/packages/core/src/renderer/components/+namespaces/__snapshots__/namespace-tree-view.test.tsx.snap b/packages/core/src/renderer/components/+namespaces/__snapshots__/namespace-tree-view.test.tsx.snap index e5d026b5b3..206c68cd27 100644 --- a/packages/core/src/renderer/components/+namespaces/__snapshots__/namespace-tree-view.test.tsx.snap +++ b/packages/core/src/renderer/components/+namespaces/__snapshots__/namespace-tree-view.test.tsx.snap @@ -20,7 +20,7 @@ exports[` collapses item by clicking minus button 1`] = `
  • @@ -47,6 +47,7 @@ exports[` collapses item by clicking minus button 1`] = ` class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1" > levels-deep +
      collapses item by clicking minus button 1`] = ` >
    • @@ -96,24 +97,11 @@ exports[` collapses item by clicking minus button 1`] = ` -
        -
        -
        -
        -
    • @@ -190,19 +178,6 @@ exports[` collapses item by clicking minus button 1`] = ` -
        -
        -
        -
        -
    • @@ -238,7 +213,7 @@ exports[` expands item by clicking plus button 1`] = `
    • @@ -265,6 +240,7 @@ exports[` expands item by clicking plus button 1`] = ` class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1" > levels-deep +
        expands item by clicking plus button 1`] = ` >
      • @@ -314,24 +290,11 @@ exports[` expands item by clicking plus button 1`] = ` -
          -
          -
          -
          -
      • @@ -374,7 +337,7 @@ exports[` expands item by clicking plus button 1`] = ` >
      • @@ -408,19 +371,6 @@ exports[` expands item by clicking plus button 1`] = ` -
          -
          -
          -
          -
      • @@ -456,7 +406,7 @@ exports[` renders 2 levels deep 1`] = `
      • @@ -483,6 +433,7 @@ exports[` renders 2 levels deep 1`] = ` class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1" > levels-deep +
          renders 2 levels deep 1`] = ` >
        • @@ -532,24 +483,11 @@ exports[` renders 2 levels deep 1`] = ` -
            -
            -
            -
            -
        • @@ -592,7 +530,7 @@ exports[` renders 2 levels deep 1`] = ` >
        • @@ -626,19 +564,6 @@ exports[` renders 2 levels deep 1`] = ` -
            -
            -
            -
            -
        • @@ -674,7 +599,7 @@ exports[` renders namespace with 2 children namespaces 1`]
        • @@ -701,6 +626,7 @@ exports[` renders namespace with 2 children namespaces 1`] class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1" > acme-org +
            renders namespace with 2 children namespaces 1`] >
          • @@ -750,23 +676,10 @@ exports[` renders namespace with 2 children namespaces 1`] -
              -
              -
              -
              -
          • @@ -800,19 +713,6 @@ exports[` renders namespace with 2 children namespaces 1`] -
              -
              -
              -
              -
          • @@ -844,7 +744,7 @@ exports[` renders namespace with children namespaces and a
          • @@ -871,6 +771,7 @@ exports[` renders namespace with children namespaces and a class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1" > org-a +
              renders namespace with children namespaces and a >
            • @@ -920,23 +821,10 @@ exports[` renders namespace with children namespaces and a -
                -
                -
                -
                -
            • @@ -970,26 +858,13 @@ exports[` renders namespace with children namespaces and a S -
                -
                -
                -
                -
            • @@ -1001,12 +876,6 @@ exports[` renders namespace with children namespaces and a `; -exports[` renders null with regular namespace 1`] = ` - -
              - -`; - exports[` renders one namespace without children 1`] = `
              @@ -1026,7 +895,7 @@ exports[` renders one namespace without children 1`] = ` >
            • @@ -1057,6 +926,7 @@ exports[` renders one namespace without children 1`] = ` class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1" > single-root +
            • diff --git a/packages/core/src/renderer/components/+namespaces/namespace-details.tsx b/packages/core/src/renderer/components/+namespaces/namespace-details.tsx index b66ab0e772..d38cab94ba 100644 --- a/packages/core/src/renderer/components/+namespaces/namespace-details.tsx +++ b/packages/core/src/renderer/components/+namespaces/namespace-details.tsx @@ -27,6 +27,8 @@ import resourceQuotaStoreInjectable from "../+config-resource-quotas/store.injec import type { Logger } from "../../../common/logger"; import loggerInjectable from "../../../common/logger.injectable"; import { NamespaceTreeView } from "./namespace-tree-view"; +import namespaceStoreInjectable from "./store.injectable"; +import type { NamespaceStore } from "./store"; export interface NamespaceDetailsProps extends KubeObjectDetailsProps { } @@ -36,6 +38,7 @@ interface Dependencies { getDetailsUrl: GetDetailsUrl; resourceQuotaStore: ResourceQuotaStore; limitRangeStore: LimitRangeStore; + namespaceStore: NamespaceStore; logger: Logger; } @@ -105,7 +108,9 @@ class NonInjectedNamespaceDetails extends React.Component - + {namespace.isControlledByHNC() && ( + + )} ); } @@ -118,6 +123,7 @@ export const NamespaceDetails = withInjectables ({ Link: ({ children }: { children: React.ReactNode }) => children, @@ -24,7 +25,7 @@ function createNamespace(name: string, labels?: Record, annotati name, resourceVersion: "1", selfLink: `/api/v1/namespaces/${name}`, - uid: `${name}-1`, + uid: `${name}`, labels: { ...labels, }, @@ -123,59 +124,169 @@ describe("", () => { render = renderFor(di); }); - it("renders null with regular namespace", () => { - const result = render(); - - expect(result.baseElement).toMatchSnapshot(); - }); - it("renders one namespace without children", () => { - const result = render(); + const tree: NamespaceTree = { + id: "single-root", + namespace: singleRoot + } + + const result = render(); expect(result.baseElement).toMatchSnapshot(); }); it("renders namespace with 2 children namespaces", () => { - const result = render(); + const tree: NamespaceTree = { + id: "acme-org", + namespace: acmeGroup, + children: [ + { + id: "team-a", + namespace: teamA + }, + { + id: "team-b", + namespace: teamB + } + ] + } + + const result = render(); expect(result.baseElement).toMatchSnapshot(); }); it("renders namespace with children namespaces and a subnamespace", () => { - const result = render(); + const tree: NamespaceTree = { + id: "org-a", + namespace: orgA, + children: [ + { + id: "team-c", + namespace: teamC + }, + { + id: "service-1", + namespace: service1 + } + ] + } + const result = render(); expect(result.baseElement).toMatchSnapshot(); }); it("renders an indicator badge for the subnamespace", () => { - const result = render(); + const tree: NamespaceTree = { + id: "org-a", + namespace: orgA, + children: [ + { + id: "team-c", + namespace: teamC + }, + { + id: "service-1", + namespace: service1 + } + ] + } + const result = render(); - expect(result.getByTestId("namespace-details-badge-for-service-1-1")).toBeInTheDocument(); + expect(result.getByTestId("namespace-details-badge-for-service-1")).toBeInTheDocument(); }); it("does not render an indicator badge for the true namespace", () => { - const result = render(); - const trueNamespace = result.getByTestId("namespace-team-c-1"); + const tree: NamespaceTree = { + id: "org-a", + namespace: orgA, + children: [ + { + id: "team-c", + namespace: teamC + }, + { + id: "service-1", + namespace: service1 + } + ] + } + const result = render(); + const trueNamespace = result.getByTestId("namespace-team-c"); - expect(trueNamespace.querySelector("[data-testid='namespace-details-badge-for-team-c-1']")).toBeNull(); + expect(trueNamespace.querySelector("[data-testid='namespace-details-badge-for-team-c']")).toBeNull(); }); it("renders 2 levels deep", () => { - const result = render(); + const tree: NamespaceTree = { + id: "levels-deep", + namespace: levelsDeep, + children: [ + { + id: "level-deep-child-a", + namespace: levelDeepChildA + }, + { + id: "level-deep-child-b", + namespace: levelDeepChildB, + children: [{ + id: "level-deep-subchild-a", + namespace: levelDeepSubChildA + }] + } + ] + } + const result = render(); expect(result.baseElement).toMatchSnapshot(); }); it("expands children items by default", () => { - const result = render(); - const deepest = result.getByTestId("namespace-level-deep-child-b-1"); + const tree: NamespaceTree = { + id: "levels-deep", + namespace: levelsDeep, + children: [ + { + id: "level-deep-child-a", + namespace: levelDeepChildA + }, + { + id: "level-deep-child-b", + namespace: levelDeepChildB, + children: [{ + id: "level-deep-subchild-a", + namespace: levelDeepSubChildA + }] + } + ] + } + const result = render(); + const deepest = result.getByTestId("namespace-level-deep-child-b"); expect(deepest).toHaveAttribute("aria-expanded", "true"); }); it("collapses item by clicking minus button", () => { - const result = render(); - const levelB = result.getByTestId("namespace-level-deep-child-b-1"); + const tree: NamespaceTree = { + id: "levels-deep", + namespace: levelsDeep, + children: [ + { + id: "level-deep-child-a", + namespace: levelDeepChildA + }, + { + id: "level-deep-child-b", + namespace: levelDeepChildB, + children: [{ + id: "level-deep-subchild-a", + namespace: levelDeepSubChildA + }] + } + ] + } + const result = render(); + const levelB = result.getByTestId("namespace-level-deep-child-b"); const minusButton = levelB.querySelector("[data-testid='minus-square']"); if (minusButton) { @@ -186,8 +297,26 @@ describe("", () => { }); it("expands item by clicking plus button", () => { - const result = render(); - const levelB = result.getByTestId("namespace-level-deep-child-b-1"); + const tree: NamespaceTree = { + id: "levels-deep", + namespace: levelsDeep, + children: [ + { + id: "level-deep-child-a", + namespace: levelDeepChildA + }, + { + id: "level-deep-child-b", + namespace: levelDeepChildB, + children: [{ + id: "level-deep-subchild-a", + namespace: levelDeepSubChildA + }] + } + ] + } + const result = render(); + const levelB = result.getByTestId("namespace-level-deep-child-b"); const minusButton = levelB.querySelector("[data-testid='minus-square']"); if (minusButton) { diff --git a/packages/core/src/renderer/components/+namespaces/namespace-tree-view.tsx b/packages/core/src/renderer/components/+namespaces/namespace-tree-view.tsx index f479bb91be..57f6589ba2 100644 --- a/packages/core/src/renderer/components/+namespaces/namespace-tree-view.tsx +++ b/packages/core/src/renderer/components/+namespaces/namespace-tree-view.tsx @@ -17,9 +17,10 @@ import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injec import { SubnamespaceBadge } from "./subnamespace-badge"; import hierarchicalNamespacesInjectable from "./hierarchical-namespaces.injectable"; import { prevDefault } from "../../utils"; +import type { NamespaceTree } from "./store"; interface NamespaceTreeViewProps { - root: Namespace; + tree: NamespaceTree; } interface Dependencies { @@ -27,48 +28,33 @@ interface Dependencies { getDetailsUrl: GetDetailsUrl; } -function isNamespaceControlledByHNC(namespace: Namespace) { - const hierarchicalNamesaceControllerLabel = "hnc.x-k8s.io/included-namespace=true"; - - return namespace.getLabels().find(label => label === hierarchicalNamesaceControllerLabel); -} - -function NonInjectableNamespaceTreeView({ root, namespaces, getDetailsUrl }: Dependencies & NamespaceTreeViewProps) { - const [expandedItems, setExpandedItems] = React.useState(namespaces.map(ns => `namespace-${ns.getId()}`)); +function NonInjectableNamespaceTreeView({ tree, namespaces, getDetailsUrl }: Dependencies & NamespaceTreeViewProps) { + const [expandedItems, setExpandedItems] = React.useState(namespaces.map(ns => ns.getId())); const classes = { group: styles.group, label: styles.label }; - const nodeId = `namespace-${root.getId()}`; - function renderChildren(parent: Namespace) { - const children = namespaces.filter(ns => - ns.getLabels().find(label => label === `${parent.getName()}.tree.hnc.x-k8s.io/depth=1`), - ); - - return children.map(child => { - const childId = `namespace-${child.getId()}`; - - return ( - toggleNode(childId))} - label={( - <> - - {child.getName()} - - {" "} - {child.isSubnamespace() && ( - - )} - - )} + function renderTree(nodes: NamespaceTree) { + return ( + toggleNode(nodes.id))} + label={( + <> + + {nodes.namespace.getName()} + + {" "} + {nodes.namespace.isSubnamespace() && ( + + )} + + )} > - {renderChildren(child)} - - ); - }); + {Array.isArray(nodes.children) ? nodes.children.map((node) => renderTree(node)) : null} + + ) } function toggleNode(id: string) { @@ -79,29 +65,17 @@ function NonInjectableNamespaceTreeView({ root, namespaces, getDetailsUrl }: Dep } } - if (!isNamespaceControlledByHNC(root)) { - return null; - } - return (
              Tree View } defaultExpandIcon={} defaultEndIcon={(
              )} expanded={expandedItems} > - toggleNode(nodeId))} - > - {renderChildren(root)} - + {renderTree(tree)}
              ); diff --git a/packages/core/src/renderer/components/+namespaces/store.ts b/packages/core/src/renderer/components/+namespaces/store.ts index 78f836e807..fee3e3fcd1 100644 --- a/packages/core/src/renderer/components/+namespaces/store.ts +++ b/packages/core/src/renderer/components/+namespaces/store.ts @@ -12,6 +12,12 @@ import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { NamespaceApi } from "../../../common/k8s-api/endpoints/namespace.api"; import { Namespace } from "../../../common/k8s-api/endpoints/namespace.api"; +export type NamespaceTree = { + id: string, + namespace: Namespace, + children?: NamespaceTree[] +} + interface Dependencies extends KubeObjectStoreDependencies { readonly storage: StorageLayer; readonly clusterConfiguredAccessibleNamespaces: IComputedValue; @@ -202,6 +208,16 @@ export class NamespaceStore extends KubeObjectStore { this.selectAll(); } + getNamespaceTree(root: Namespace): NamespaceTree { + const children = this.items.filter(namespace => namespace.isChildOf(root.getName())); + + return { + id: root.getId(), + namespace: root, + children: children.map(this.getNamespaceTree) + } + } + @action async remove(item: Namespace) { await super.remove(item);