Merge remote-tracking branch 'origin/master' into replication_controllers_new_resource_view

This commit is contained in:
Roman committed 2023-02-14 12:36:00 +02:00
commit 440ca8c40f
30 files changed
+3075 -186

No files matched your search

@@ -33,6 +33,20 @@ export class Namespace extends KubeObject<
getStatus() {
return this.status?.phase ?? "-";
}
isSubnamespace() {
return this.getAnnotations().find(annotation => annotation.includes("hnc.x-k8s.io/subnamespace-of"));
}
isChildOf(parentName: string) {
return 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<Namespace> {
@@ -23,7 +23,7 @@ export const defaultTerminalConfig: TerminalConfig = {
fontFamily: defaultTerminalFontFamily,
};
interface BaseEditorConfiguration extends Required<Pick<editor.IStandaloneEditorConstructionOptions, "minimap" | "tabSize" | "fontSize" | "fontFamily">> {
export interface BaseEditorConfiguration extends Required<Pick<editor.IStandaloneEditorConstructionOptions, "minimap" | "tabSize" | "fontSize" | "fontFamily">> {
lineNumbers: NonNullable<Exclude<editor.IStandaloneEditorConstructionOptions["lineNumbers"], Function>>;
}
@@ -10119,11 +10119,11 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam
class="footer"
>
<div
class="Dock"
class="Dock isOpen"
tabindex="-1"
>
<div
class="ResizingAnchor vertical leading disabled"
class="ResizingAnchor vertical leading"
/>
<div
class="tabs-container flex align-center"
@@ -10134,10 +10134,63 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam
>
<div
class="Tabs tabs"
/>
>
<div
class="Tab flex gaps align-center DockTab active"
data-testid="dock-tab-for-some-first-tab-id"
id="tab-some-first-tab-id"
role="tab"
tabindex="0"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="edit"
>
edit
</span>
</i>
<div
class="label"
>
<div
class="flex align-center"
>
<span
class="title"
>
Namespace: some-name
</span>
<div
class="close"
>
<i
class="Icon material interactive focusable small"
data-testid="dock-tab-close-for-some-first-tab-id"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div
data-testid="tooltip-content-for-dock-tab-close-for-some-first-tab-id"
>
Close ⌘+W
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="toolbar flex gaps align-center box grow pl-0"
class="toolbar flex gaps align-center box grow"
>
<div
class="dock-menu box grow"
@@ -10158,6 +10211,118 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam
New tab
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="fullscreen"
>
fullscreen
</span>
</i>
<div>
Fit to window
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
<div>
Minimize
</div>
</div>
</div>
<div
class="tab-content edit-resource"
data-testid="dock-tab-content-for-some-first-tab-id"
style="flex-basis: 300px;"
>
<div
class="EditResource flex column"
>
<div
class="InfoPanel flex gaps align-center"
>
<div
class="controls"
>
<div
class="resource-info flex gaps align-center"
>
<span>
Kind:
</span>
<div
class="badge"
>
Namespace
</div>
<span>
Name:
</span>
<div
class="badge"
>
some-name
</div>
<span>
Namespace:
</span>
<div
class="badge"
>
default
</div>
</div>
</div>
<div
class="flex gaps align-center"
/>
<button
class="Button plain"
data-testid="cancel-edit-resource-from-tab-for-some-first-tab-id"
type="button"
>
Cancel
</button>
<button
class="Button active outlined"
data-testid="save-edit-resource-from-tab-for-some-first-tab-id"
type="button"
>
Save
</button>
<button
class="Button primary active"
data-testid="save-and-close-edit-resource-from-tab-for-some-first-tab-id"
type="button"
>
Save & Close
</button>
</div>
<textarea
data-testid="monaco-editor-for-some-first-tab-id"
>
apiVersion: some-api-version
kind: Namespace
metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeRemoved: some-value
somePropertyToBeChanged: some-old-value
</textarea>
</div>
</div>
</div>
@@ -401,8 +401,7 @@ metadata:
expect(rendered.baseElement).toMatchSnapshot();
});
// TODO: Not doable at the moment because info panel controls closing of the tab
xit("does not close the dock tab", () => {
it("does not close the dock tab", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
@@ -11,7 +11,7 @@ const apiBaseHostHeaderInjectable = getInjectable({
instantiate: (di) => {
const lensProxyPort = di.inject(lensProxyPortInjectable);
return `localhost:${lensProxyPort.get()}`;
return `lens.app:${lensProxyPort.get()}`;
},
injectionToken: apiBaseHostHeaderInjectionToken,
});
@@ -2,7 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { SelfSignedCert } from "selfsigned";
import { lensProxyCertificateChannel } from "../../common/certificate/lens-proxy-certificate-channel";
import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens";
import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable";
@@ -10,7 +9,7 @@ import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-
const lensProxyCertificateRequestHandlerInjectable = getRequestChannelListenerInjectable({
channel: lensProxyCertificateChannel,
handler: (di) => {
const lensProxyCertificate = di.inject(lensProxyCertificateInjectable).get() as SelfSignedCert;
const lensProxyCertificate = di.inject(lensProxyCertificateInjectable).get();
return () => ({
cert: lensProxyCertificate.cert,
@@ -0,0 +1,937 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<NamespaceTreeView /> collapses item by clicking minus button 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-levels-deep"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
levels-deep
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-child-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-a
</div>
</div>
</li>
<li
aria-expanded="false"
class="MuiTreeItem-root"
data-testid="namespace-level-deep-child-b"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="plus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 12.977h-4.923v4.896q0 .401-.281.682t-.682.281v0q-.375 0-.669-.281t-.294-.682v-4.896h-4.923q-.401 0-.682-.294t-.281-.669v0q0-.401.281-.682t.682-.281h4.923v-4.896q0-.401.294-.682t.669-.281v0q.401 0 .682.281t.281.682v4.896h4.923q.401 0 .682.281t.281.682v0q0 .375-.281.669t-.682.294z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-b
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group"
role="group"
style="min-height: 0px; height: 0px; transition-duration: 300ms;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-subchild-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-subchild-a
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
</div>
</body>
`;
exports[`<NamespaceTreeView /> expands item by clicking plus button 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-levels-deep"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
levels-deep
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-child-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-a
</div>
</div>
</li>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-child-b"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-b
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group"
role="group"
style="min-height: 0px; height: 0px; transition-duration: 300ms;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-subchild-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-subchild-a
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
</div>
</body>
`;
exports[`<NamespaceTreeView /> renders 2 levels deep 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-levels-deep"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
levels-deep
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-child-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-a
</div>
</div>
</li>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-child-b"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-child-b
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-level-deep-subchild-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
level-deep-subchild-a
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
</div>
</body>
`;
exports[`<NamespaceTreeView /> renders namespace with 2 children namespaces 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-acme-org"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
acme-org
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-team-a"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
team-a
</div>
</div>
</li>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-team-b"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
team-b
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
</div>
</body>
`;
exports[`<NamespaceTreeView /> renders namespace with children namespaces and a subnamespace 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-expanded="true"
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-org-a"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
org-a
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-team-c"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
team-c
</div>
</div>
</li>
<li
class="MuiTreeItem-root Mui-expanded"
data-testid="namespace-service-1"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
service-1
<span
class="subnamespaceBadge"
data-testid="namespace-details-badge-for-service-1"
id="namespace-details-badge-for-service-1"
>
S
</span>
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
</div>
</body>
`;
exports[`<NamespaceTreeView /> renders one namespace without children 1`] = `
<body>
<div>
<div
class="TreeView"
data-testid="namespace-tree-view"
>
<div
class="DrawerTitle title"
>
Tree View
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
class="MuiTreeItem-root"
data-testid="namespace-single-root"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<div
style="opacity: 0.3;"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="minus-square"
focusable="false"
style="width: 14px; height: 14px;"
viewBox="0 0 24 24"
>
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"
/>
</svg>
</div>
</div>
<div
class="MuiTypography-root MuiTreeItem-label label MuiTypography-body1"
>
single-root
</div>
</div>
</li>
</ul>
</div>
</div>
</body>
`;
@@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import namespaceStoreInjectable from "./store.injectable";
const hierarchicalNamespacesInjectable = getInjectable({
id: "hierarchical-namespaces",
instantiate: (di) => {
const namespaceStore = di.inject(namespaceStoreInjectable);
return namespaceStore.items.filter(item => item.isControlledByHNC());
},
});
export default hierarchicalNamespacesInjectable;
@@ -26,6 +26,9 @@ import limitRangeStoreInjectable from "../+config-limit-ranges/store.injectable"
import resourceQuotaStoreInjectable from "../+config-resource-quotas/store.injectable";
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<Namespace> {
}
@@ -35,6 +38,7 @@ interface Dependencies {
getDetailsUrl: GetDetailsUrl;
resourceQuotaStore: ResourceQuotaStore;
limitRangeStore: LimitRangeStore;
namespaceStore: NamespaceStore;
logger: Logger;
}
@@ -103,6 +107,10 @@ class NonInjectedNamespaceDetails extends React.Component<NamespaceDetailsProps
</Link>
))}
</DrawerItem>
{namespace.isControlledByHNC() && (
<NamespaceTreeView tree={this.props.namespaceStore.getNamespaceTree(namespace)}/>
)}
</div>
);
}
@@ -115,6 +123,7 @@ export const NamespaceDetails = withInjectables<Dependencies, NamespaceDetailsPr
getDetailsUrl: di.inject(getDetailsUrlInjectable),
limitRangeStore: di.inject(limitRangeStoreInjectable),
resourceQuotaStore: di.inject(resourceQuotaStoreInjectable),
namespaceStore: di.inject(namespaceStoreInjectable),
logger: di.inject(loggerInjectable),
}),
});
@@ -0,0 +1,205 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import { observable } from "mobx";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
import type { NamespaceStore } from "./store";
import namespaceStoreInjectable from "./store.injectable";
function createNamespace(name: string, labels?: Record<string, string>, annotations?: Record<string, string>): Namespace {
return new Namespace({
apiVersion: "v1",
kind: "Namespace",
metadata: {
name,
resourceVersion: "1",
selfLink: `/api/v1/namespaces/${name}`,
uid: `${name}`,
labels: {
...labels,
},
annotations: {
...annotations,
},
},
});
}
const singleRoot = createNamespace("single-root", {
"hnc.x-k8s.io/included-namespace": "true",
});
const acmeGroup = createNamespace("acme-org", {
"hnc.x-k8s.io/included-namespace": "true",
});
const orgA = createNamespace("org-a", {
"hnc.x-k8s.io/included-namespace": "true",
});
const teamA = createNamespace("team-a", {
"hnc.x-k8s.io/included-namespace": "true",
"acme-org.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-a",
"team-a.tree.hnc.x-k8s.io/depth": "0",
});
const teamB = createNamespace("team-b", {
"hnc.x-k8s.io/included-namespace": "true",
"acme-org.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-b",
"team-b.tree.hnc.x-k8s.io/depth": "0",
});
const teamC = createNamespace("team-c", {
"hnc.x-k8s.io/included-namespace": "true",
"org-a.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-c",
"team-c.tree.hnc.x-k8s.io/depth": "0",
});
const service1 = createNamespace("service-1", {
"hnc.x-k8s.io/included-namespace": "true",
"org-a.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-c",
"service-1.tree.hnc.x-k8s.io/depth": "0",
}, {
"hnc.x-k8s.io/subnamespace-of": "org-a",
});
const levelsDeep = createNamespace("levels-deep", {
"hnc.x-k8s.io/included-namespace": "true",
});
const levelDeepChildA = createNamespace("level-deep-child-a", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "1",
"level-deep-child-a.tree.hnc.x-k8s.io/depth": "0",
});
const levelDeepChildB = createNamespace("level-deep-child-b", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "1",
"level-deep-child-b.tree.hnc.x-k8s.io/depth": "0",
});
const levelDeepSubChildA = createNamespace("level-deep-subchild-a", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "2",
"level-deep-child-b.tree.hnc.x-k8s.io/depth": "1",
"level-deep-subchild-a.tree.hnc.x-k8s.io/depth": "0",
});
describe("NamespaceStore", () => {
let di: DiContainer;
let namespaceStore: NamespaceStore;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
namespaceStore = di.inject(namespaceStoreInjectable);
namespaceStore.items = observable.array([
acmeGroup,
orgA,
teamA,
teamB,
teamC,
service1,
levelsDeep,
levelDeepChildA,
levelDeepChildB,
levelDeepSubChildA,
]);
});
it("returns tree for single node", () => {
const tree = namespaceStore.getNamespaceTree(service1);
expect(tree).toEqual({
id: "service-1",
namespace: service1,
children: [],
});
});
it("returns tree for namespace not listed in store", () => {
const tree = namespaceStore.getNamespaceTree(singleRoot);
expect(tree).toEqual({
id: "single-root",
namespace: singleRoot,
children: [],
});
});
it("return tree for namespace with children", () => {
const tree = namespaceStore.getNamespaceTree(acmeGroup);
expect(tree).toEqual({
id: "acme-org",
namespace: acmeGroup,
children: [
{
id: "team-a",
namespace: teamA,
children: [],
},
{
id: "team-b",
namespace: teamB,
children: [],
},
],
});
});
it("return tree for namespace with deep nested children", () => {
const tree = namespaceStore.getNamespaceTree(levelsDeep);
expect(tree).toEqual({
id: "levels-deep",
namespace: levelsDeep,
children: [
{
id: "level-deep-child-a",
namespace: levelDeepChildA,
children: [],
},
{
id: "level-deep-child-b",
namespace: levelDeepChildB,
children: [{
id: "level-deep-subchild-a",
namespace: levelDeepSubChildA,
children: [],
}],
},
],
});
});
});
@@ -0,0 +1,14 @@
.TreeView {
.group {
margin-inline-start: var(--margin);
padding-inline-start: calc(var(--padding) * 2);
border-inline-start: 1px dashed var(--borderColor);
}
.label {
font-size: inherit;
line-height: 1.8;
cursor: default;
background-color: transparent!important;
}
}
@@ -0,0 +1,334 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import { fireEvent } from "@testing-library/react";
import React from "react";
import { Namespace } from "../../../common/k8s-api/endpoints";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor";
import hierarchicalNamespacesInjectable from "./hierarchical-namespaces.injectable";
import { NamespaceTreeView } from "./namespace-tree-view";
import type { NamespaceTree } from "./store";
jest.mock("react-router-dom", () => ({
Link: ({ children }: { children: React.ReactNode }) => children,
}));
function createNamespace(name: string, labels?: Record<string, string>, annotations?: Record<string, string>): Namespace {
return new Namespace({
apiVersion: "v1",
kind: "Namespace",
metadata: {
name,
resourceVersion: "1",
selfLink: `/api/v1/namespaces/${name}`,
uid: `${name}`,
labels: {
...labels,
},
annotations: {
...annotations,
},
},
});
}
const singleRoot = createNamespace("single-root", {
"hnc.x-k8s.io/included-namespace": "true",
});
const acmeGroup = createNamespace("acme-org", {
"hnc.x-k8s.io/included-namespace": "true",
});
const orgA = createNamespace("org-a", {
"hnc.x-k8s.io/included-namespace": "true",
});
const teamA = createNamespace("team-a", {
"hnc.x-k8s.io/included-namespace": "true",
"acme-org.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-a",
"team-a.tree.hnc.x-k8s.io/depth": "0",
});
const teamB = createNamespace("team-b", {
"hnc.x-k8s.io/included-namespace": "true",
"acme-org.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-b",
"team-b.tree.hnc.x-k8s.io/depth": "0",
});
const teamC = createNamespace("team-c", {
"hnc.x-k8s.io/included-namespace": "true",
"org-a.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-c",
"team-c.tree.hnc.x-k8s.io/depth": "0",
});
const service1 = createNamespace("service-1", {
"hnc.x-k8s.io/included-namespace": "true",
"org-a.tree.hnc.x-k8s.io/depth": "1",
"kubernetes.io/metadata.name": "team-c",
"service-1.tree.hnc.x-k8s.io/depth": "0",
}, {
"hnc.x-k8s.io/subnamespace-of": "org-a",
});
const levelsDeep = createNamespace("levels-deep", {
"hnc.x-k8s.io/included-namespace": "true",
});
const levelDeepChildA = createNamespace("level-deep-child-a", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "1",
"level-deep-child-a.tree.hnc.x-k8s.io/depth": "0",
});
const levelDeepChildB = createNamespace("level-deep-child-b", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "1",
"level-deep-child-b.tree.hnc.x-k8s.io/depth": "0",
});
const levelDeepSubChildA = createNamespace("level-deep-subchild-a", {
"hnc.x-k8s.io/included-namespace": "true",
"levels-deep.tree.hnc.x-k8s.io/depth": "2",
"level-deep-child-b.tree.hnc.x-k8s.io/depth": "1",
"level-deep-subchild-a.tree.hnc.x-k8s.io/depth": "0",
});
describe("<NamespaceTreeView />", () => {
let di: DiContainer;
let render: DiRender;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(hierarchicalNamespacesInjectable, () => [
acmeGroup,
orgA,
teamA,
teamB,
teamC,
service1,
levelsDeep,
levelDeepChildA,
levelDeepChildB,
levelDeepSubChildA,
]);
render = renderFor(di);
});
it("renders one namespace without children", () => {
const tree: NamespaceTree = {
id: "single-root",
namespace: singleRoot,
};
const result = render(<NamespaceTreeView tree={tree} />);
expect(result.baseElement).toMatchSnapshot();
});
it("renders namespace with 2 children namespaces", () => {
const tree: NamespaceTree = {
id: "acme-org",
namespace: acmeGroup,
children: [
{
id: "team-a",
namespace: teamA,
},
{
id: "team-b",
namespace: teamB,
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
expect(result.baseElement).toMatchSnapshot();
});
it("renders namespace with children namespaces and a subnamespace", () => {
const tree: NamespaceTree = {
id: "org-a",
namespace: orgA,
children: [
{
id: "team-c",
namespace: teamC,
},
{
id: "service-1",
namespace: service1,
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
expect(result.baseElement).toMatchSnapshot();
});
it("renders an indicator badge for the subnamespace", () => {
const tree: NamespaceTree = {
id: "org-a",
namespace: orgA,
children: [
{
id: "team-c",
namespace: teamC,
},
{
id: "service-1",
namespace: service1,
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
expect(result.getByTestId("namespace-details-badge-for-service-1")).toBeInTheDocument();
});
it("does not render an indicator badge for the true namespace", () => {
const tree: NamespaceTree = {
id: "org-a",
namespace: orgA,
children: [
{
id: "team-c",
namespace: teamC,
},
{
id: "service-1",
namespace: service1,
},
],
};
const result = render(<NamespaceTreeView tree={tree} />);
const trueNamespace = result.getByTestId("namespace-team-c");
expect(trueNamespace.querySelector("[data-testid='namespace-details-badge-for-team-c']")).toBeNull();
});
it("renders 2 levels deep", () => {
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(<NamespaceTreeView tree={tree} />);
expect(result.baseElement).toMatchSnapshot();
});
it("expands children items by default", () => {
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(<NamespaceTreeView tree={tree} />);
const deepest = result.getByTestId("namespace-level-deep-child-b");
expect(deepest).toHaveAttribute("aria-expanded", "true");
});
it("collapses item by clicking minus button", () => {
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(<NamespaceTreeView tree={tree} />);
const levelB = result.getByTestId("namespace-level-deep-child-b");
const minusButton = levelB.querySelector("[data-testid='minus-square']");
if (minusButton) {
fireEvent.click(minusButton);
}
expect(result.baseElement).toMatchSnapshot();
});
it("expands item by clicking plus button", () => {
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(<NamespaceTreeView tree={tree} />);
const levelB = result.getByTestId("namespace-level-deep-child-b");
const minusButton = levelB.querySelector("[data-testid='minus-square']");
if (minusButton) {
fireEvent.click(minusButton);
}
const plusButton = levelB.querySelector("[data-testid='plus-square']");
if (plusButton) {
fireEvent.click(plusButton);
}
expect(result.baseElement).toMatchSnapshot();
});
});
@@ -0,0 +1,106 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./namespace-tree-view.module.scss";
import { SvgIcon } from "@material-ui/core";
import { TreeItem, TreeView } from "@material-ui/lab";
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import { Link } from "react-router-dom";
import type { Namespace } from "../../../common/k8s-api/endpoints";
import { DrawerTitle } from "../drawer";
import type { GetDetailsUrl } from "../kube-detail-params/get-details-url.injectable";
import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable";
import { SubnamespaceBadge } from "./subnamespace-badge";
import hierarchicalNamespacesInjectable from "./hierarchical-namespaces.injectable";
import { prevDefault } from "../../utils";
import type { NamespaceTree } from "./store";
interface NamespaceTreeViewProps {
tree: NamespaceTree;
}
interface Dependencies {
namespaces: Namespace[];
getDetailsUrl: GetDetailsUrl;
}
function NonInjectableNamespaceTreeView({ tree, namespaces, getDetailsUrl }: Dependencies & NamespaceTreeViewProps) {
const [expandedItems, setExpandedItems] = React.useState<string[]>(namespaces.map(ns => ns.getId()));
const classes = { group: styles.group, label: styles.label };
function renderTree(nodes: NamespaceTree) {
return (
<TreeItem
key={nodes.id}
nodeId={nodes.id}
data-testid={`namespace-${nodes.id}`}
classes={classes}
onIconClick={prevDefault(() => toggleNode(nodes.id))}
label={(
<>
<Link key={nodes.namespace.getId()} to={getDetailsUrl(nodes.namespace.selfLink)}>
{nodes.namespace.getName()}
</Link>
{" "}
{nodes.namespace.isSubnamespace() && (
<SubnamespaceBadge id={`namespace-details-badge-for-${nodes.namespace.getId()}`} />
)}
</>
)}
>
{Array.isArray(nodes.children) ? nodes.children.map((node) => renderTree(node)) : null}
</TreeItem>
);
}
function toggleNode(id: string) {
if (expandedItems.includes(id)) {
setExpandedItems(expandedItems.filter(item => item !== id));
} else {
setExpandedItems([...expandedItems, id]);
}
}
return (
<div data-testid="namespace-tree-view" className={styles.TreeView}>
<DrawerTitle>Tree View</DrawerTitle>
<TreeView
defaultExpanded={[tree.id]}
defaultCollapseIcon={<MinusSquareIcon />}
defaultExpandIcon={<PlusSquareIcon />}
defaultEndIcon={(<div style={{ opacity: 0.3 }}><MinusSquareIcon /></div>)}
expanded={expandedItems}
>
{renderTree(tree)}
</TreeView>
</div>
);
}
function MinusSquareIcon() {
return (
<SvgIcon style={{ width: 14, height: 14 }} data-testid="minus-square">
<path d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z" />
</SvgIcon>
);
}
function PlusSquareIcon() {
return (
<SvgIcon style={{ width: 14, height: 14 }} data-testid="plus-square">
<path d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 12.977h-4.923v4.896q0 .401-.281.682t-.682.281v0q-.375 0-.669-.281t-.294-.682v-4.896h-4.923q-.401 0-.682-.294t-.281-.669v0q0-.401.281-.682t.682-.281h4.923v-4.896q0-.401.294-.682t.669-.281v0q.401 0 .682.281t.281.682v4.896h4.923q.401 0 .682.281t.281.682v0q0 .375-.281.669t-.682.294z" />
</SvgIcon>
);
}
export const NamespaceTreeView = withInjectables<Dependencies, NamespaceTreeViewProps>(NonInjectableNamespaceTreeView, {
getProps: (di, props) => ({
namespaces: di.inject(hierarchicalNamespacesInjectable),
getDetailsUrl: di.inject(getDetailsUrlInjectable),
...props,
}),
});
@@ -22,4 +22,8 @@
@include namespaceStatus;
}
}
.subnamespaceBadge {
margin-inline-start: var(--margin);
}
}
@@ -16,6 +16,7 @@ import { withInjectables } from "@ogre-tools/injectable-react";
import namespaceStoreInjectable from "./store.injectable";
import { KubeObjectAge } from "../kube-object/age";
import openAddNamepaceDialogInjectable from "./add-dialog/open.injectable";
import { SubnamespaceBadge } from "./subnamespace-badge";
enum columnId {
name = "name",
@@ -55,7 +56,12 @@ const NonInjectedNamespacesRoute = ({ namespaceStore, openAddNamespaceDialog }:
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
]}
renderTableContents={namespace => [
namespace.getName(),
<>
{namespace.getName()}
{namespace.isSubnamespace() && (
<SubnamespaceBadge className="subnamespaceBadge" id={`namespace-list-badge-for-${namespace.getId()}`} />
)}
</>,
<KubeObjectStatusIcon key="icon" object={namespace} />,
namespace.getLabels().map(label => (
<Badge
@@ -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 interface NamespaceTree {
id: string;
namespace: Namespace;
children?: NamespaceTree[];
}
interface Dependencies extends KubeObjectStoreDependencies {
readonly storage: StorageLayer<string[] | undefined>;
readonly clusterConfiguredAccessibleNamespaces: IComputedValue<string[]>;
@@ -202,6 +208,16 @@ export class NamespaceStore extends KubeObjectStore<Namespace, NamespaceApi> {
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);
@@ -0,0 +1,11 @@
.subnamespaceBadge {
border-radius: 3px;
padding: 0px 4px;
border: 1px solid cadetblue;
color: cadetblue;
display: inline-flex;
font-size: x-small;
font-weight: bold;
height: 16px;
align-items: center;
}
@@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./subnamespace-badge.module.scss";
import React from "react";
import { Tooltip } from "../tooltip";
import { cssNames } from "../../utils";
interface SubnamespaceBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
id: string;
}
export function SubnamespaceBadge({ id, className, ...other }: SubnamespaceBadgeProps) {
return (
<>
<span
className={cssNames(styles.subnamespaceBadge, className)}
data-testid={id}
id={id}
{...other}
>
S
</span>
<Tooltip targetId={id}>
Subnamespace
</Tooltip>
</>
);
}
@@ -76,7 +76,7 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
this.error = error.toString();
};
create = async (): Promise<void> => {
create = async (): Promise<string | void> => {
const { apiManager, getDetailsUrl, navigate, requestKubeObjectCreation } = this.props;
if (this.error || !this.data?.trim()) {
@@ -98,7 +98,7 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
this.props.logger.warn("Failed to create resource", { resource }, result.error);
this.props.showCheckedErrorNotification(result.error, "Unknown error occured while creating resources");
return;
throw result.error;
}
const { kind, apiVersion, metadata: { name, namespace }} = result.response;
@@ -122,7 +122,13 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
));
});
await Promise.allSettled(creatingResources);
const results = await Promise.allSettled(creatingResources);
if (results.some(result => result.status === "rejected")) {
return;
}
return "All resources have been successfully created";
};
renderControls() {
@@ -174,5 +174,7 @@ export class EditResourceModel {
runInAction(() => {
this.editingResource.firstDraft = currentValue;
});
return result.response.toString();
};
}
@@ -93,18 +93,25 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
if (showNotifications && result) {
this.props.showSuccessNotification(result);
}
return result;
} catch (error) {
if (showNotifications) {
this.props.showCheckedErrorNotification(error, "Unknown error while submitting");
}
return false;
} finally {
this.waiting = false;
}
};
submitAndClose = async () => {
await this.submit();
this.close();
const result = await this.submit();
if (result) {
this.close();
}
};
close = () => {