diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts index 36173c0e9d..64ca60380d 100644 --- a/integration/__tests__/cluster-pages.tests.ts +++ b/integration/__tests__/cluster-pages.tests.ts @@ -331,6 +331,14 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { await cleanup(); }, 10*60*1000); + it("shows cluster context menu in sidebar", async () => { + await frame.click(`[data-testid="sidebar-cluster-dropdown"]`); + await frame.waitForSelector(`.Menu >> text="Add to Hotbar"`); + await frame.waitForSelector(`.Menu >> text="Settings"`); + await frame.waitForSelector(`.Menu >> text="Disconnect"`); + await frame.waitForSelector(`.Menu >> text="Delete"`); + }); + it("should navigate around common cluster pages", async () => { for (const test of commonPageTests) { if (isTopPageTest(test)) { @@ -362,6 +370,8 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { } }, 10*60*1000); + + it("show logs and highlight the log search entries", async () => { await frame.click(`a[href="/workloads"]`); await frame.click(`a[href="/pods"]`); diff --git a/src/renderer/components/layout/__tests__/sidebar-cluster.test.tsx b/src/renderer/components/layout/__tests__/sidebar-cluster.test.tsx new file mode 100644 index 0000000000..195368408d --- /dev/null +++ b/src/renderer/components/layout/__tests__/sidebar-cluster.test.tsx @@ -0,0 +1,74 @@ +/** + * 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 "@testing-library/jest-dom/extend-expect"; +import { render, fireEvent } from "@testing-library/react"; +import { SidebarCluster } from "../sidebar-cluster"; +import { KubernetesCluster } from "../../../../common/catalog-entities"; + +jest.mock("../../../../common/hotbar-store", () => ({ + HotbarStore: { + getInstance: () => ({ + isAddedToActive: jest.fn(), + }), + }, +})); + +const clusterEntity = new KubernetesCluster({ + metadata: { + uid: "test-uid", + name: "test-cluster", + source: "local", + labels: {}, + }, + spec: { + kubeconfigPath: "", + kubeconfigContext: "", + }, + status: { + phase: "connected", + }, +}); + +describe("", () => { + it("renders w/o errors", () => { + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("renders cluster avatar and name", () => { + const { getByText } = render(); + + expect(getByText("tc")).toBeInTheDocument(); + expect(getByText("test-cluster")).toBeInTheDocument(); + }); + + it("renders cluster menu", async () => { + const { getByTestId, getByText } = render(); + const link = getByTestId("sidebar-cluster-dropdown"); + + fireEvent.click(link); + expect(await getByText("Add to Hotbar")).toBeInTheDocument(); + }); +}); + diff --git a/src/renderer/components/layout/sidebar-cluster.module.css b/src/renderer/components/layout/sidebar-cluster.module.css new file mode 100644 index 0000000000..37a9aed9fa --- /dev/null +++ b/src/renderer/components/layout/sidebar-cluster.module.css @@ -0,0 +1,57 @@ +/** + * 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. + */ + +.SidebarCluster { + display: flex; + align-items: center; + padding: 1.25rem; + cursor: pointer; + + &:focus-visible { + background: var(--blue); + color: white; + } + + &:hover { + background: var(--sidebarLogoBackground); + } +} + +.clusterName { + font-weight: bold; + overflow: hidden; + word-break: break-word; + color: var(--textColorAccent); + display: -webkit-box; + /* Simulate text-overflow:ellipsis styles but for multiple text lines */ + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; +} + +.menu { + width: 185px; + margin-top: -10px; +} + +.avatar { + font-weight: 500; + margin-right: 1.25rem; +} \ No newline at end of file diff --git a/src/renderer/components/layout/sidebar-cluster.tsx b/src/renderer/components/layout/sidebar-cluster.tsx new file mode 100644 index 0000000000..8b47ac122f --- /dev/null +++ b/src/renderer/components/layout/sidebar-cluster.tsx @@ -0,0 +1,140 @@ +/** + * 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 styles from "./sidebar-cluster.module.css"; +import { observable } from "mobx"; +import React, { useState } from "react"; +import { HotbarStore } from "../../../common/hotbar-store"; +import { broadcastMessage } from "../../../common/ipc"; +import type { CatalogEntity, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; +import { IpcRendererNavigationEvents } from "../../navigation/events"; +import { Avatar } from "../avatar/avatar"; +import { Icon } from "../icon"; +import { navigate } from "../../navigation"; +import { Menu, MenuItem } from "../menu"; +import { ConfirmDialog } from "../confirm-dialog"; + +const contextMenu: CatalogEntityContextMenuContext = observable({ + menuItems: [], + navigate: (url: string, forceMainFrame = true) => { + if (forceMainFrame) { + broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, url); + } else { + navigate(url); + } + }, +}); + +function onMenuItemClick(menuItem: CatalogEntityContextMenu) { + if (menuItem.confirm) { + ConfirmDialog.open({ + okButtonProps: { + primary: false, + accent: true, + }, + ok: () => { + menuItem.onClick(); + }, + message: menuItem.confirm.message, + }); + } else { + menuItem.onClick(); + } +} + +export function SidebarCluster({ clusterEntity }: { clusterEntity: CatalogEntity }) { + const [opened, setOpened] = useState(false); + + if (!clusterEntity) { + return null; + } + + const onMenuOpen = () => { + const hotbarStore = HotbarStore.getInstance(); + const isAddedToActive = HotbarStore.getInstance().isAddedToActive(clusterEntity); + const title = isAddedToActive + ? "Remove from Hotbar" + : "Add to Hotbar"; + const onClick = isAddedToActive + ? () => hotbarStore.removeFromHotbar(metadata.uid) + : () => hotbarStore.addToHotbar(clusterEntity); + + contextMenu.menuItems = [{ title, onClick }]; + clusterEntity.onContextMenuOpen(contextMenu); + + toggle(); + }; + + const onKeyDown = (evt: React.KeyboardEvent) => { + if (evt.code == "Space") { + toggle(); + } + }; + + const toggle = () => { + setOpened(!opened); + }; + + const { metadata, spec } = clusterEntity; + const id = `cluster-${metadata.uid}`; + + return ( + + ); +} diff --git a/src/renderer/components/layout/sidebar.module.css b/src/renderer/components/layout/sidebar.module.css index cf1257627f..13f10c76a9 100644 --- a/src/renderer/components/layout/sidebar.module.css +++ b/src/renderer/components/layout/sidebar.module.css @@ -40,17 +40,3 @@ padding: 3px; border-radius: 50%; } - -.cluster { - @apply flex items-center m-5; -} - -.clusterName { - @apply font-bold overflow-hidden; - word-break: break-word; - color: var(--textColorAccent); - display: -webkit-box; - /* Simulate text-overflow:ellipsis styles but for multiple text lines */ - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; -} diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index f89b06e6d4..702a7a61d2 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -32,7 +32,7 @@ import { Storage } from "../+storage"; import { Network } from "../+network"; import { crdStore } from "../+custom-resources/crd.store"; import { CustomResources } from "../+custom-resources/custom-resources"; -import { isActiveRoute, navigate } from "../../navigation"; +import { isActiveRoute } from "../../navigation"; import { isAllowedResource } from "../../../common/utils/allowed-resource"; import { Spinner } from "../spinner"; import { ClusterPageMenuRegistration, ClusterPageMenuRegistry, ClusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries"; @@ -41,12 +41,7 @@ import { Apps } from "../+apps"; import * as routes from "../../../common/routes"; import { Config } from "../+config"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -import { HotbarIcon } from "../hotbar/hotbar-icon"; -import { makeObservable, observable } from "mobx"; -import type { CatalogEntityContextMenuContext } from "../../../common/catalog"; -import { HotbarStore } from "../../../common/hotbar-store"; -import { broadcastMessage } from "../../../common/ipc"; -import { IpcRendererNavigationEvents } from "../../navigation/events"; +import { SidebarCluster } from "./sidebar-cluster"; interface Props { className?: string; @@ -55,21 +50,6 @@ interface Props { @observer export class Sidebar extends React.Component { static displayName = "Sidebar"; - @observable private contextMenu: CatalogEntityContextMenuContext = { - menuItems: [], - navigate: (url: string, forceMainFrame = true) => { - if (forceMainFrame) { - broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, url); - } else { - navigate(url); - } - }, - }; - - constructor(props: Props) { - super(props); - makeObservable(this); - } async componentDidMount() { crdStore.reloadAll(); @@ -198,44 +178,6 @@ export class Sidebar extends React.Component { }); } - renderCluster() { - if (!this.clusterEntity) { - return null; - } - - const { metadata, spec } = this.clusterEntity; - - return ( -
- navigate("/")} - menuItems={this.contextMenu.menuItems} - onMenuOpen={() => { - const hotbarStore = HotbarStore.getInstance(); - const isAddedToActive = HotbarStore.getInstance().isAddedToActive(this.clusterEntity); - const title = isAddedToActive - ? "Remove from Hotbar" - : "Add to Hotbar"; - const onClick = isAddedToActive - ? () => hotbarStore.removeFromHotbar(metadata.uid) - : () => hotbarStore.addToHotbar(this.clusterEntity); - - this.contextMenu.menuItems = [{ title, onClick }]; - this.clusterEntity.onContextMenuOpen(this.contextMenu); - }} - /> -
- {metadata.name} -
-
- ); - } - get clusterEntity() { return catalogEntityRegistry.activeEntity; } @@ -245,7 +187,7 @@ export class Sidebar extends React.Component { return (
- {this.renderCluster()} +