Add breadcrumbs to removal of kube objects, but also discuss about general improvements (#4282)

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>
Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
Co-authored-by: Janne Savolainen <janne.savolainen@houston-inc.com>
Co-authored-by: Mikko Aspiala <mikko.aspiala@houston-inc.com>
This commit is contained in:
authored and GitHub committed 2021-12-06 13:05:05 -05:00
1 parent 304941d397
commit 7ed50f83fc
23 files changed
+1214 -73

No files matched your search

@@ -140,6 +140,7 @@ export class ConfirmDialog extends React.Component<ConfirmDialogProps> {
isOpen={dialogState.isOpen}
onClose={this.onClose}
close={this.close}
{...(dialogState.isOpen ? { "data-testid":"confirmation-dialog" } : {})}
>
<div className="confirm-content">
{icon} {message}
@@ -158,6 +159,7 @@ export class ConfirmDialog extends React.Component<ConfirmDialogProps> {
label={labelOk}
onClick={prevDefault(this.ok)}
waiting={this.isSaving}
data-testid="confirm"
{...okButtonProps}
/>
</div>
+3 -1
View File
@@ -41,6 +41,7 @@ export interface DialogProps {
modal?: boolean;
pinned?: boolean;
animated?: boolean;
"data-testid"?: string;
}
interface DialogState {
@@ -149,7 +150,7 @@ export class Dialog extends React.PureComponent<DialogProps, DialogState> {
};
render() {
const { modal, animated, pinned } = this.props;
const { modal, animated, pinned, "data-testid": testId } = this.props;
let { className } = this.props;
className = cssNames("Dialog flex center", className, { modal, pinned });
@@ -158,6 +159,7 @@ export class Dialog extends React.PureComponent<DialogProps, DialogState> {
className={className}
onClick={stopPropagation}
ref={this.ref}
data-testid={testId}
>
<div className="box" ref={e => this.contentElem = e}>
{this.props.children}
@@ -0,0 +1,371 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`kube-object-menu given kube object renders 1`] = `
<body>
<div>
<div>
<ul
class="Animate opacity Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<li>
Some menu item
</li>
<li
class="MenuItem"
data-testid="menu-action-remove"
tabindex="0"
>
<i
class="Icon material interactive focusable"
id="tooltip_target_4"
tabindex="0"
>
<span
class="icon"
data-icon-name="delete"
>
delete
</span>
<div />
</i>
<span
class="title"
>
Delete
</span>
</li>
</ul>
</div>
</div>
<div
class="Animate opacity-scale Dialog flex center ConfirmDialog modal"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
/>
<div
class="Tooltip narrow invisible formatter"
>
Delete
</div>
</body>
`;
exports[`kube-object-menu given kube object when removing kube object renders 1`] = `
<body>
<div>
<div>
<ul
class="Animate opacity Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<li>
Some menu item
</li>
<li
class="MenuItem"
data-testid="menu-action-remove"
tabindex="0"
>
<i
class="Icon material interactive focusable"
id="tooltip_target_8"
tabindex="0"
>
<span
class="icon"
data-icon-name="delete"
>
delete
</span>
<div />
</i>
<span
class="title"
>
Delete
</span>
</li>
</ul>
</div>
</div>
<div
class="Animate opacity-scale Dialog flex center ConfirmDialog modal"
data-testid="confirmation-dialog"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<div
class="confirm-content"
>
<i
class="Icon material focusable big"
>
<span
class="icon"
data-icon-name="warning"
>
warning
</span>
</i>
<div>
<p>
Remove
some-kind
<b>
some-namespace/some-name
</b>
from
<b>
Some name
</b>
?
</p>
</div>
</div>
<div
class="confirm-buttons"
>
<button
class="Button cancel plain"
type="button"
>
Cancel
</button>
<button
class="Button ok primary"
data-testid="confirm"
type="button"
>
Remove
</button>
</div>
</div>
</div>
<div
class="Tooltip narrow invisible formatter"
>
Delete
</div>
</body>
`;
exports[`kube-object-menu given kube object with namespace when removing kube object, renders confirmation dialog with namespace 1`] = `
<body>
<div>
<div>
<ul
class="Animate opacity Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<li>
Some menu item
</li>
<li
class="MenuItem"
data-testid="menu-action-remove"
tabindex="0"
>
<i
class="Icon material interactive focusable"
id="tooltip_target_33"
tabindex="0"
>
<span
class="icon"
data-icon-name="delete"
>
delete
</span>
<div />
</i>
<span
class="title"
>
Delete
</span>
</li>
</ul>
</div>
</div>
<div
class="Animate opacity-scale Dialog flex center ConfirmDialog modal"
data-testid="confirmation-dialog"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<div
class="confirm-content"
>
<i
class="Icon material focusable big"
>
<span
class="icon"
data-icon-name="warning"
>
warning
</span>
</i>
<div>
<p>
Remove
some-kind
<b>
some-namespace/some-name
</b>
from
<b>
Some name
</b>
?
</p>
</div>
</div>
<div
class="confirm-buttons"
>
<button
class="Button cancel plain"
type="button"
>
Cancel
</button>
<button
class="Button ok primary"
data-testid="confirm"
type="button"
>
Remove
</button>
</div>
</div>
</div>
<div
class="Tooltip narrow invisible formatter"
>
Delete
</div>
</body>
`;
exports[`kube-object-menu given kube object without namespace when removing kube object, renders confirmation dialog without namespace 1`] = `
<body>
<div>
<div>
<ul
class="Animate opacity Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<li>
Some menu item
</li>
<li
class="MenuItem"
data-testid="menu-action-remove"
tabindex="0"
>
<i
class="Icon material interactive focusable"
id="tooltip_target_41"
tabindex="0"
>
<span
class="icon"
data-icon-name="delete"
>
delete
</span>
<div />
</i>
<span
class="title"
>
Delete
</span>
</li>
</ul>
</div>
</div>
<div
class="Animate opacity-scale Dialog flex center ConfirmDialog modal"
data-testid="confirmation-dialog"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<div
class="confirm-content"
>
<i
class="Icon material focusable big"
>
<span
class="icon"
data-icon-name="warning"
>
warning
</span>
</i>
<div>
<p>
Remove
some-kind
<b>
some-name
</b>
from
<b>
Some name
</b>
?
</p>
</div>
</div>
<div
class="confirm-buttons"
>
<button
class="Button cancel plain"
type="button"
>
Cancel
</button>
<button
class="Button ok primary"
data-testid="confirm"
type="button"
>
Remove
</button>
</div>
</div>
</div>
<div
class="Tooltip narrow invisible formatter"
>
Delete
</div>
</body>
`;
exports[`kube-object-menu given no kube object, renders 1`] = `
<body>
<div>
<ul
class="Animate opacity Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
/>
</div>
</body>
`;
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { ApiManager } from "../../../../common/k8s-api/api-manager";
import { apiManager } from "../../../../common/k8s-api/api-manager";
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
const apiManagerInjectable: Injectable<ApiManager> = {
getDependencies: () => ({}),
instantiate: () => apiManager,
lifecycle: lifecycleEnum.singleton,
};
export default apiManagerInjectable;
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import type { Cluster } from "../../../../main/cluster";
import clusterInjectable from "./cluster.injectable";
interface Dependencies {
cluster: Cluster;
}
const clusterNameInjectable: Injectable<string | undefined, Dependencies> = {
getDependencies: di => ({
cluster: di.inject(clusterInjectable),
}),
instantiate: ({ cluster }) => cluster?.name,
lifecycle: lifecycleEnum.transient,
};
export default clusterNameInjectable;
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getActiveClusterEntity } from "../../../api/catalog-entity-registry";
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import type { Cluster } from "../../../../main/cluster";
const clusterInjectable: Injectable<Cluster | null> = {
getDependencies: () => ({}),
instantiate: () => getActiveClusterEntity(),
lifecycle: lifecycleEnum.transient,
};
export default clusterInjectable;
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { editResourceTab } from "../../dock/edit-resource.store";
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
const editResourceTabInjectable: Injectable<typeof editResourceTab> = {
getDependencies: () => ({}),
instantiate: () => editResourceTab,
lifecycle: lifecycleEnum.singleton,
};
export default editResourceTabInjectable;
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { hideDetails } from "../../kube-detail-params";
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
export const hideDetailsInjectable: Injectable<typeof hideDetails> = {
getDependencies: () => ({}),
instantiate: () => hideDetails,
lifecycle: lifecycleEnum.singleton,
};
export default hideDetailsInjectable;
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { KubeObjectMenuRegistry } from "../../../../../extensions/registries";
import type { KubeObject } from "../../../../../common/k8s-api/kube-object";
export interface Dependencies {
kubeObjectMenuRegistry: KubeObjectMenuRegistry;
}
export interface InstantiationParameter {
kubeObject: KubeObject;
}
export const getKubeObjectMenuItems = (
{ kubeObjectMenuRegistry }: Dependencies,
{ kubeObject }: InstantiationParameter,
) => {
if (!kubeObject) {
return [];
}
return kubeObjectMenuRegistry
.getItemsForKind(kubeObject.kind, kubeObject.apiVersion)
.map(item => item.components.MenuItem);
};
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { Injectable, lifecycleEnum } from "@ogre-tools/injectable";
import kubeObjectMenuRegistryInjectable from "./kube-object-menu-registry.injectable";
import {
InstantiationParameter,
Dependencies,
getKubeObjectMenuItems,
} from "./get-kube-object-menu-items";
const kubeObjectMenuItemsInjectable: Injectable<
ReturnType<typeof getKubeObjectMenuItems>,
Dependencies,
InstantiationParameter
> = {
getDependencies: di => ({
kubeObjectMenuRegistry: di.inject(kubeObjectMenuRegistryInjectable),
}),
instantiate: getKubeObjectMenuItems,
lifecycle: lifecycleEnum.transient,
};
export default kubeObjectMenuItemsInjectable;
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { KubeObjectMenuRegistry } from "../../../../../extensions/registries";
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
const kubeObjectMenuRegistryInjectable: Injectable<KubeObjectMenuRegistry> = {
getDependencies: () => ({}),
instantiate: () => KubeObjectMenuRegistry.getInstance(),
lifecycle: lifecycleEnum.singleton,
};
export default kubeObjectMenuRegistryInjectable;
@@ -19,4 +19,5 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export * from "./kube-object-menu";
export type { KubeObjectMenuProps } from "./kube-object-menu";
export { KubeObjectMenu } from "./kube-object-menu-container";
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectedComponent } from "@ogre-tools/injectable-react";
import KubeObjectMenuInjectable from "./kube-object-menu.injectable";
export const KubeObjectMenu = getInjectedComponent(KubeObjectMenuInjectable);
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import {
KubeObjectMenu,
KubeObjectMenuDependencies,
KubeObjectMenuProps,
} from "./kube-object-menu";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { lifecycleEnum, Injectable } from "@ogre-tools/injectable";
import apiManagerInjectable from "./dependencies/api-manager.injectable";
import clusterNameInjectable from "./dependencies/cluster-name.injectable";
import editResourceTabInjectable from "./dependencies/edit-resource-tab.injectable";
import hideDetailsInjectable from "./dependencies/hide-details.injectable";
import kubeObjectMenuItemsInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-items.injectable";
const KubeObjectMenuInjectable: Injectable<
JSX.Element,
KubeObjectMenuDependencies<KubeObject>,
KubeObjectMenuProps<KubeObject>
> = {
getDependencies: (di, props) => ({
clusterName: di.inject(clusterNameInjectable),
apiManager: di.inject(apiManagerInjectable),
editResourceTab: di.inject(editResourceTabInjectable),
hideDetails: di.inject(hideDetailsInjectable),
kubeObjectMenuItems: di.inject(kubeObjectMenuItemsInjectable, {
kubeObject: props.object,
}),
}),
instantiate: (dependencies, props) => (
<KubeObjectMenu {...dependencies} {...props} />
),
lifecycle: lifecycleEnum.transient,
};
export default KubeObjectMenuInjectable;
@@ -0,0 +1,284 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import { screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import userEvent from "@testing-library/user-event";
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
import type { KubeObjectMenuRegistration } from "../../../extensions/registries";
import { KubeObjectMenuRegistry } from "../../../extensions/registries";
import { ConfirmDialog } from "../confirm-dialog";
import asyncFn, { AsyncFnMock } from "@async-fn/jest";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import clusterInjectable from "./dependencies/cluster.injectable";
import hideDetailsInjectable from "./dependencies/hide-details.injectable";
import editResourceTabInjectable from "./dependencies/edit-resource-tab.injectable";
import { TabKind } from "../dock/dock.store";
import kubeObjectMenuRegistryInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-registry.injectable";
import { DiRender, renderFor } from "../test-utils/renderFor";
import type { Cluster } from "../../../main/cluster";
import type { ApiManager } from "../../../common/k8s-api/api-manager";
import apiManagerInjectable from "./dependencies/api-manager.injectable";
import { KubeObjectMenu } from "./index";
describe("kube-object-menu", () => {
let di: ConfigurableDependencyInjectionContainer;
let render: DiRender;
beforeEach(() => {
di = getDiForUnitTesting();
// TODO: Remove global shared state
KubeObjectMenuRegistry.resetInstance();
KubeObjectMenuRegistry.createInstance();
render = renderFor(di);
di.override(clusterInjectable, {
name: "Some name",
} as Cluster);
di.override(apiManagerInjectable, {
// eslint-disable-next-line unused-imports/no-unused-vars-ts
getStore: api => undefined,
} as ApiManager);
di.override(hideDetailsInjectable, () => {});
di.override(editResourceTabInjectable, () => ({
id: "irrelevant",
kind: TabKind.TERMINAL,
pinned: false,
title: "irrelevant",
}));
addDynamicMenuItem({
di,
apiVersions: ["some-api-version"],
kind: "some-kind",
});
addDynamicMenuItem({
di,
apiVersions: ["some-unrelated-api-version"],
kind: "some-kind",
});
addDynamicMenuItem({
di,
apiVersions: ["some-api-version"],
kind: "some-unrelated-kind",
});
});
it("given no cluster, does not crash", () => {
di.override(clusterInjectable, null);
expect(() => {
render(<KubeObjectMenu object={null} toolbar={true} />);
}).not.toThrow();
});
it("given no kube object, renders", () => {
const { baseElement } = render(
<KubeObjectMenu object={null} toolbar={true} />,
);
expect(baseElement).toMatchSnapshot();
});
describe("given kube object", () => {
let baseElement: Element;
let removeActionMock: AsyncFnMock<Function>;
beforeEach(async () => {
const objectStub = KubeObject.create({
apiVersion: "some-api-version",
kind: "some-kind",
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
namespace: "some-namespace",
},
});
removeActionMock = asyncFn();
({ baseElement } = render(
<div>
<ConfirmDialog />
<KubeObjectMenu
object={objectStub}
toolbar={true}
removeAction={removeActionMock}
/>
</div>,
));
});
it("renders", () => {
expect(baseElement).toMatchSnapshot();
});
it("does not open a confirmation dialog yet", () => {
expect(screen.queryByTestId("confirmation-dialog")).toBeNull();
});
describe("when removing kube object", () => {
beforeEach(() => {
const menuItem = screen.getByTestId("menu-action-remove");
userEvent.click(menuItem);
});
it("renders", () => {
expect(baseElement).toMatchSnapshot();
});
it("opens a confirmation dialog", () => {
screen.getByTestId("confirmation-dialog");
});
describe("when remove is confirmed", () => {
beforeEach(() => {
const confirmRemovalButton = screen.getByTestId("confirm");
userEvent.click(confirmRemovalButton);
});
it("calls for removal of the kube object", () => {
expect(removeActionMock).toHaveBeenCalledWith();
});
it("does not close the confirmation dialog yet", () => {
screen.getByTestId("confirmation-dialog");
});
it("when removal resolves, closes the confirmation dialog", async () => {
await removeActionMock.resolve();
expect(screen.queryByTestId("confirmation-dialog")).toBeNull();
});
});
});
});
describe("given kube object with namespace", () => {
let baseElement: Element;
beforeEach(async () => {
const objectStub = KubeObject.create({
apiVersion: "some-api-version",
kind: "some-kind",
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
namespace: "some-namespace",
},
});
({ baseElement } = render(
<div>
<ConfirmDialog />
<KubeObjectMenu
object={objectStub}
toolbar={true}
removeAction={() => {}}
/>
</div>,
));
});
it("when removing kube object, renders confirmation dialog with namespace", () => {
const menuItem = screen.getByTestId("menu-action-remove");
userEvent.click(menuItem);
expect(baseElement).toMatchSnapshot();
});
});
describe("given kube object without namespace", () => {
let baseElement: Element;
beforeEach(async () => {
const objectStub = KubeObject.create({
apiVersion: "some-api-version",
kind: "some-kind",
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
namespace: undefined,
},
});
({ baseElement } = render(
<div>
<ConfirmDialog />
<KubeObjectMenu
object={objectStub}
toolbar={true}
removeAction={() => {}}
/>
</div>,
));
});
it("when removing kube object, renders confirmation dialog without namespace", () => {
const menuItem = screen.getByTestId("menu-action-remove");
userEvent.click(menuItem);
expect(baseElement).toMatchSnapshot();
});
});
});
const addDynamicMenuItem = ({
di,
apiVersions,
kind,
}: {
di: ConfigurableDependencyInjectionContainer;
apiVersions: string[];
kind: string;
}) => {
const MenuItemComponent: React.FC = () => <li>Some menu item</li>;
const dynamicMenuItemStub: KubeObjectMenuRegistration = {
apiVersions,
kind,
components: { MenuItem: MenuItemComponent },
};
const kubeObjectMenuRegistry = di.inject(kubeObjectMenuRegistryInjectable);
kubeObjectMenuRegistry.add(dynamicMenuItemStub);
};
@@ -22,25 +22,37 @@
import React from "react";
import { boundMethod, cssNames } from "../../utils";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { editResourceTab } from "../dock/edit-resource.store";
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
import { hideDetails } from "../kube-detail-params";
import { apiManager } from "../../../common/k8s-api/api-manager";
import { KubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
import { MenuActions, MenuActionsProps } from "../menu";
import identity from "lodash/identity";
import type { ApiManager } from "../../../common/k8s-api/api-manager";
export interface KubeObjectMenuProps<T> extends MenuActionsProps {
object: T | null | undefined;
export interface KubeObjectMenuDependencies<TKubeObject> {
apiManager: ApiManager;
kubeObjectMenuItems: React.ElementType[];
clusterName: string;
hideDetails: () => void;
editResourceTab: (kubeObject: TKubeObject) => void;
}
export interface KubeObjectMenuProps<TKubeObject> extends MenuActionsProps {
object: TKubeObject | null | undefined;
editable?: boolean;
removable?: boolean;
}
export class KubeObjectMenu<T extends KubeObject> extends React.Component<KubeObjectMenuProps<T>> {
export interface KubeObjectMenuPropsAndDependencies<TKubeObject>
extends KubeObjectMenuProps<TKubeObject>,
KubeObjectMenuDependencies<TKubeObject> {}
export class KubeObjectMenu<
TKubeObject extends KubeObject,
> extends React.Component<KubeObjectMenuPropsAndDependencies<TKubeObject>> {
get store() {
const { object } = this.props;
if (!object) return null;
return apiManager.getStore(object.selfLink);
return this.props.apiManager.getStore(object.selfLink);
}
get isEditable() {
@@ -53,13 +65,13 @@ export class KubeObjectMenu<T extends KubeObject> extends React.Component<KubeOb
@boundMethod
async update() {
hideDetails();
editResourceTab(this.props.object);
this.props.hideDetails();
this.props.editResourceTab(this.props.object);
}
@boundMethod
async remove() {
hideDetails();
this.props.hideDetails();
const { object, removeAction } = this.props;
if (removeAction) await removeAction();
@@ -74,28 +86,27 @@ export class KubeObjectMenu<T extends KubeObject> extends React.Component<KubeOb
return null;
}
const breadcrumbParts = [object.getNs(), object.getName()];
const breadcrumb = breadcrumbParts.filter(identity).join("/");
return (
<p>Remove {object.kind} <b>{object.getName()}</b>?</p>
<p>
Remove {object.kind} <b>{breadcrumb}</b> from <b>{this.props.clusterName}</b>?
</p>
);
}
getMenuItems(): React.ReactChild[] {
const { object, toolbar } = this.props;
if (!object) {
return [];
}
return KubeObjectMenuRegistry
.getInstance()
.getItemsForKind(object.kind, object.apiVersion)
.map(({ components: { MenuItem }}, index) => (
<MenuItem
object={object}
key={`menu-item-${index}`}
toolbar={toolbar}
/>
));
return this.props.kubeObjectMenuItems.map((MenuItem, index) => (
<MenuItem
object={object}
toolbar={toolbar}
key={`menu-item-${index}`}
/>
));
}
render() {
+25 -27
View File
@@ -30,7 +30,6 @@ import { Icon, IconProps } from "../icon";
import { Menu, MenuItem, MenuProps } from "../menu";
import uniqueId from "lodash/uniqueId";
import isString from "lodash/isString";
import { RenderDelay } from "../render-delay/render-delay";
export interface MenuActionsProps extends Partial<MenuProps> {
className?: string;
@@ -125,32 +124,31 @@ export class MenuActions extends React.Component<MenuActionsProps> {
return (
<>
{this.renderTriggerIcon()}
<RenderDelay>
<Menu
htmlFor={this.id}
isOpen={this.isOpen} open={this.toggle} close={this.toggle}
className={menuClassName}
usePortal={autoClose}
closeOnScroll={autoClose}
closeOnClickItem={autoCloseOnSelect ?? autoClose }
closeOnClickOutside={autoClose}
{...menuProps}
>
{children}
{updateAction && (
<MenuItem onClick={updateAction}>
<Icon material="edit" interactive={toolbar} tooltip="Edit"/>
<span className="title">Edit</span>
</MenuItem>
)}
{removeAction && (
<MenuItem onClick={this.remove}>
<Icon material="delete" interactive={toolbar} tooltip="Delete"/>
<span className="title">Delete</span>
</MenuItem>
)}
</Menu>
</RenderDelay>
<Menu
htmlFor={this.id}
isOpen={this.isOpen} open={this.toggle} close={this.toggle}
className={menuClassName}
usePortal={autoClose}
closeOnScroll={autoClose}
closeOnClickItem={autoCloseOnSelect ?? autoClose }
closeOnClickOutside={autoClose}
{...menuProps}
>
{children}
{updateAction && (
<MenuItem onClick={updateAction}>
<Icon material="edit" interactive={toolbar} tooltip="Edit"/>
<span className="title">Edit</span>
</MenuItem>
)}
{removeAction && (
<MenuItem onClick={this.remove} data-testid="menu-action-remove">
<Icon material="delete" interactive={toolbar} tooltip="Delete"/>
<span className="title">Delete</span>
</MenuItem>
)}
</Menu>
</>
);
}
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import {
render as testingLibraryRender,
RenderResult,
} from "@testing-library/react";
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
import { DiContextProvider } from "@ogre-tools/injectable-react";
export type DiRender = (ui: React.ReactElement) => RenderResult;
type DiRenderFor = (
di: ConfigurableDependencyInjectionContainer,
) => DiRender;
export const renderFor: DiRenderFor = di => ui =>
testingLibraryRender(
<DiContextProvider value={{ di }}>{ui}</DiContextProvider>,
);