From ab4a8c8a1df9b82ad1a901e113a8db83d9ccdae9 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Tue, 28 Dec 2021 12:35:47 +0200 Subject: [PATCH 01/10] Convert a class component to functional component Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen --- src/renderer/components/+welcome/welcome.tsx | 147 ++++++++++++------- 1 file changed, 90 insertions(+), 57 deletions(-) diff --git a/src/renderer/components/+welcome/welcome.tsx b/src/renderer/components/+welcome/welcome.tsx index 669e768782..52d71bffb0 100644 --- a/src/renderer/components/+welcome/welcome.tsx +++ b/src/renderer/components/+welcome/welcome.tsx @@ -30,70 +30,103 @@ import { WelcomeBannerRegistry } from "../../../extensions/registries"; export const defaultWidth = 320; -@observer -export class Welcome extends React.Component { - render() { - const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems(); +export const Welcome: React.FC = observer(() => { + const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems(); - // if there is banner with specified width, use it to calculate the width of the container - const maxWidth = welcomeBanner.reduce((acc, curr) => { - const currWidth = curr.width ?? 0; + // if there is banner with specified width, use it to calculate the width of the container + const maxWidth = welcomeBanner.reduce((acc, curr) => { + const currWidth = curr.width ?? 0; - if (acc > currWidth) { - return acc; - } + if (acc > currWidth) { + return acc; + } - return currWidth; - }, defaultWidth); + return currWidth; + }, defaultWidth); - return ( -
-
- {welcomeBanner.length > 0 ? ( - 1} - autoPlay={true} - navButtonsAlwaysInvisible={true} - indicatorIconButtonProps={{ - style: { - color: "var(--iconActiveBackground)", - }, - }} - activeIndicatorIconButtonProps={{ - style: { - color: "var(--iconActiveColor)", - }, - }} - interval={8000} + return ( +
+
+ {welcomeBanner.length > 0 ? ( + 1} + autoPlay={true} + navButtonsAlwaysInvisible={true} + indicatorIconButtonProps={{ + style: { + color: "var(--iconActiveBackground)", + }, + }} + activeIndicatorIconButtonProps={{ + style: { + color: "var(--iconActiveColor)", + }, + }} + interval={8000} + > + {welcomeBanner.map((item, index) => ( + + ))} + + ) : ( + + )} + +
+
+

Welcome to {productName} 5!

+ +

+ To get you started we have auto-detected your clusters in your + kubeconfig file and added them to the catalog, your centralized + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our{" "} + + Lens Community slack channel + + . +

+ +
- ); - } -} +
+ ); +}); From 04ac2ff3ddc66f9cc5b1fd5478377086b93262e1 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Tue, 28 Dec 2021 12:52:39 +0200 Subject: [PATCH 02/10] Remove WelcomeMenuRegistry in favor of reactive solution Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen --- .../extension-loader/extension-loader.ts | 1 - src/extensions/lens-renderer-extension.ts | 3 +- src/extensions/registries/index.ts | 1 - .../renderer-extensions.injectable.ts | 33 +++++++++++ src/renderer/bootstrap.tsx | 3 - .../+welcome/__test__/welcome.test.tsx | 15 +++-- .../get-welcome-menu-items.ts | 46 ++++++++++++++++ .../welcome-menu-items.injectable.ts} | 25 ++++----- .../welcome-menu-registration.d.ts} | 4 -- src/renderer/components/+welcome/welcome.tsx | 55 ++++++++++++------- src/renderer/initializers/index.ts | 1 - src/renderer/initializers/registries.ts | 1 - 12 files changed, 139 insertions(+), 49 deletions(-) create mode 100644 src/extensions/renderer-extensions.injectable.ts create mode 100644 src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts rename src/renderer/{initializers/welcome-menu-registry.ts => components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts} (69%) rename src/{extensions/registries/welcome-menu-registry.ts => renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts} (90%) diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index af5f05c100..fb0c42e1ac 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -263,7 +263,6 @@ export class ExtensionLoader { registries.EntitySettingRegistry.getInstance().add(extension.entitySettings), registries.StatusBarRegistry.getInstance().add(extension.statusBarItems), registries.CommandRegistry.getInstance().add(extension.commands), - registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus), registries.WelcomeBannerRegistry.getInstance().add(extension.welcomeBanners), registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), registries.TopBarRegistry.getInstance().add(extension.topBarItems), diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 67567969ee..388f6ba1f2 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -27,6 +27,7 @@ import type { Disposer } from "../common/utils"; import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry"; import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry"; import type { KubernetesCluster } from "../common/catalog-entities"; +import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/welcome-menu-items/welcome-menu-registration"; export class LensRendererExtension extends LensExtension { globalPages: registries.PageRegistration[] = []; @@ -40,7 +41,7 @@ export class LensRendererExtension extends LensExtension { kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = []; kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = []; commands: registries.CommandRegistration[] = []; - welcomeMenus: registries.WelcomeMenuRegistration[] = []; + welcomeMenus: WelcomeMenuRegistration[] = []; welcomeBanners: registries.WelcomeBannerRegistration[] = []; catalogEntityDetailItems: registries.CatalogEntityDetailRegistration[] = []; topBarItems: registries.TopBarRegistration[] = []; diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 4dd64a9c82..b9c249ce4c 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -30,7 +30,6 @@ export * from "./kube-object-menu-registry"; export * from "./kube-object-status-registry"; export * from "./command-registry"; export * from "./entity-setting-registry"; -export * from "./welcome-menu-registry"; export * from "./welcome-banner-registry"; export * from "./catalog-entity-detail-registry"; export * from "./workloads-overview-detail-registry"; diff --git a/src/extensions/renderer-extensions.injectable.ts b/src/extensions/renderer-extensions.injectable.ts new file mode 100644 index 0000000000..dddcf11e80 --- /dev/null +++ b/src/extensions/renderer-extensions.injectable.ts @@ -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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { IComputedValue } from "mobx"; +import extensionsInjectable from "./extensions.injectable"; +import type { LensRendererExtension } from "./lens-renderer-extension"; + +const rendererExtensionsInjectable = getInjectable({ + lifecycle: lifecycleEnum.singleton, + + instantiate: (di) => + di.inject(extensionsInjectable) as IComputedValue, +}); + +export default rendererExtensionsInjectable; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 1de360d18a..6e9e9fb471 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -114,9 +114,6 @@ export async function bootstrap(comp: () => Promise, di: Dependenc logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`); initializers.initKubeObjectDetailRegistry(); - logger.info(`${logPrefix} initializing WelcomeMenuRegistry`); - initializers.initWelcomeMenuRegistry(); - logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`); initializers.initWorkloadsOverviewDetailRegistry(); diff --git a/src/renderer/components/+welcome/__test__/welcome.test.tsx b/src/renderer/components/+welcome/__test__/welcome.test.tsx index 2024d69c7e..fa94149caa 100644 --- a/src/renderer/components/+welcome/__test__/welcome.test.tsx +++ b/src/renderer/components/+welcome/__test__/welcome.test.tsx @@ -20,11 +20,14 @@ */ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { Welcome } from "../welcome"; -import { TopBarRegistry, WelcomeMenuRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries"; +import { TopBarRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries"; import { defaultWidth } from "../welcome"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import type { DiRender } from "../../test-utils/renderFor"; +import { renderFor } from "../../test-utils/renderFor"; jest.mock( "electron", @@ -39,15 +42,19 @@ jest.mock( ); describe("", () => { + let render: DiRender; + beforeEach(() => { + const di = getDiForUnitTesting(); + + render = renderFor(di); + TopBarRegistry.createInstance(); - WelcomeMenuRegistry.createInstance(); WelcomeBannerRegistry.createInstance(); }); afterEach(() => { TopBarRegistry.resetInstance(); - WelcomeMenuRegistry.resetInstance(); WelcomeBannerRegistry.resetInstance(); }); diff --git a/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts b/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts new file mode 100644 index 0000000000..c766704cef --- /dev/null +++ b/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts @@ -0,0 +1,46 @@ +/** + * 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 { computed, IComputedValue } from "mobx"; +import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import { navigate } from "../../../navigation"; +import { catalogURL } from "../../../../common/routes"; + +interface Dependencies { + extensions: IComputedValue; +} + +export const getWelcomeMenuItems = ({ extensions }: Dependencies) => { + const browseClusters = { + title: "Browse Clusters in Catalog", + icon: "view_list", + click: () => + navigate( + catalogURL({ + params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }, + }), + ), + }; + + return computed(() => [ + browseClusters, + ...extensions.get().flatMap((extension) => extension.welcomeMenus), + ]); +}; diff --git a/src/renderer/initializers/welcome-menu-registry.ts b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts similarity index 69% rename from src/renderer/initializers/welcome-menu-registry.ts rename to src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts index f658fe5715..384b0b07bc 100644 --- a/src/renderer/initializers/welcome-menu-registry.ts +++ b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts @@ -18,18 +18,17 @@ * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; +import { getWelcomeMenuItems } from "./get-welcome-menu-items"; -import { catalogURL } from "../../common/routes"; -import { WelcomeMenuRegistry } from "../../extensions/registries"; -import { navigate } from "../navigation"; +const welcomeMenuItemsInjectable = getInjectable({ + instantiate: (di) => + getWelcomeMenuItems({ + extensions: di.inject(rendererExtensionsInjectable), + }), -export function initWelcomeMenuRegistry() { - WelcomeMenuRegistry.getInstance() - .add([ - { - title: "Browse Clusters in Catalog", - icon: "view_list", - click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }} )), - }, - ]); -} + lifecycle: lifecycleEnum.singleton, +}); + +export default welcomeMenuItemsInjectable; diff --git a/src/extensions/registries/welcome-menu-registry.ts b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts similarity index 90% rename from src/extensions/registries/welcome-menu-registry.ts rename to src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts index 7092028459..8f4d9833b3 100644 --- a/src/extensions/registries/welcome-menu-registry.ts +++ b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts @@ -19,12 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { BaseRegistry } from "./base-registry"; - export interface WelcomeMenuRegistration { title: string | (() => string); icon: string; click: () => void | Promise; } - -export class WelcomeMenuRegistry extends BaseRegistry {} diff --git a/src/renderer/components/+welcome/welcome.tsx b/src/renderer/components/+welcome/welcome.tsx index 52d71bffb0..f90ede88b8 100644 --- a/src/renderer/components/+welcome/welcome.tsx +++ b/src/renderer/components/+welcome/welcome.tsx @@ -22,15 +22,22 @@ import "./welcome.scss"; import React from "react"; import { observer } from "mobx-react"; +import type { IComputedValue } from "mobx"; import Carousel from "react-material-ui-carousel"; import { Icon } from "../icon"; import { productName, slackUrl } from "../../../common/vars"; -import { WelcomeMenuRegistry } from "../../../extensions/registries"; import { WelcomeBannerRegistry } from "../../../extensions/registries"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import welcomeMenuItemsInjectable from "./welcome-menu-items/welcome-menu-items.injectable"; +import type { WelcomeMenuRegistration } from "./welcome-menu-items/welcome-menu-registration"; export const defaultWidth = 320; -export const Welcome: React.FC = observer(() => { +interface Dependencies { + welcomeMenuItems: IComputedValue +} + +const NonInjectedWelcome: React.FC = ({ welcomeMenuItems }) => { const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems(); // if there is banner with specified width, use it to calculate the width of the container @@ -106,27 +113,35 @@ export const Welcome: React.FC = observer(() => { style={{ width: `${defaultWidth}px` }} data-testid="welcome-menu-container" > - {WelcomeMenuRegistry.getInstance() - .getItems() - .map((item, index) => ( -
  • item.click()} - > - - - {typeof item.title === "string" - ? item.title - : item.title()} - - -
  • - ))} + {welcomeMenuItems.get().map((item, index) => ( +
  • item.click()} + > + + + {typeof item.title === "string" + ? item.title + : item.title()} + + +
  • + ))}
    ); -}); +}; + +export const Welcome = withInjectables( + observer(NonInjectedWelcome), + + { + getProps: (di) => ({ + welcomeMenuItems: di.inject(welcomeMenuItemsInjectable), + }), + }, +); diff --git a/src/renderer/initializers/index.ts b/src/renderer/initializers/index.ts index 55d1bbbb1b..b865368988 100644 --- a/src/renderer/initializers/index.ts +++ b/src/renderer/initializers/index.ts @@ -27,7 +27,6 @@ export * from "./ipc"; export * from "./kube-object-detail-registry"; export * from "./kube-object-menu-registry"; export * from "./registries"; -export * from "./welcome-menu-registry"; export * from "./workloads-overview-detail-registry"; export * from "./catalog-category-registry"; export * from "./status-bar-registry"; diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts index 3063213bc8..4fddb35b3d 100644 --- a/src/renderer/initializers/registries.ts +++ b/src/renderer/initializers/registries.ts @@ -33,7 +33,6 @@ export function initRegistries() { registries.KubeObjectMenuRegistry.createInstance(); registries.KubeObjectStatusRegistry.createInstance(); registries.StatusBarRegistry.createInstance(); - registries.WelcomeMenuRegistry.createInstance(); registries.WelcomeBannerRegistry.createInstance(); registries.WorkloadsOverviewDetailRegistry.createInstance(); registries.TopBarRegistry.createInstance(); From c5613a2e6a38a3acf82452c46442c9a2a884ed45 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Tue, 28 Dec 2021 13:13:19 +0200 Subject: [PATCH 03/10] Remove WelcomeBannerRegistry in favor of reactive solution Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen --- .../extension-loader/extension-loader.ts | 1 - src/extensions/lens-renderer-extension.ts | 3 +- src/extensions/registries/index.ts | 1 - .../+welcome/__test__/welcome.test.tsx | 95 ++++++++++++------- .../welcome-banner-items.injectable.ts | 37 ++++++++ .../welcome-banner-registration.d.ts} | 4 - src/renderer/components/+welcome/welcome.tsx | 17 ++-- src/renderer/initializers/registries.ts | 1 - 8 files changed, 112 insertions(+), 47 deletions(-) create mode 100644 src/renderer/components/+welcome/welcome-banner-items/welcome-banner-items.injectable.ts rename src/{extensions/registries/welcome-banner-registry.ts => renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts} (91%) diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index fb0c42e1ac..db3e924209 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -263,7 +263,6 @@ export class ExtensionLoader { registries.EntitySettingRegistry.getInstance().add(extension.entitySettings), registries.StatusBarRegistry.getInstance().add(extension.statusBarItems), registries.CommandRegistry.getInstance().add(extension.commands), - registries.WelcomeBannerRegistry.getInstance().add(extension.welcomeBanners), registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), registries.TopBarRegistry.getInstance().add(extension.topBarItems), ]; diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 388f6ba1f2..2ccb2cfe7a 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -28,6 +28,7 @@ import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-ent import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry"; import type { KubernetesCluster } from "../common/catalog-entities"; import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/welcome-menu-items/welcome-menu-registration"; +import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration"; export class LensRendererExtension extends LensExtension { globalPages: registries.PageRegistration[] = []; @@ -42,7 +43,7 @@ export class LensRendererExtension extends LensExtension { kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = []; commands: registries.CommandRegistration[] = []; welcomeMenus: WelcomeMenuRegistration[] = []; - welcomeBanners: registries.WelcomeBannerRegistration[] = []; + welcomeBanners: WelcomeBannerRegistration[] = []; catalogEntityDetailItems: registries.CatalogEntityDetailRegistration[] = []; topBarItems: registries.TopBarRegistration[] = []; diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index b9c249ce4c..2cd822f77a 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -30,7 +30,6 @@ export * from "./kube-object-menu-registry"; export * from "./kube-object-status-registry"; export * from "./command-registry"; export * from "./entity-setting-registry"; -export * from "./welcome-banner-registry"; export * from "./catalog-entity-detail-registry"; export * from "./workloads-overview-detail-registry"; export * from "./topbar-registry"; diff --git a/src/renderer/components/+welcome/__test__/welcome.test.tsx b/src/renderer/components/+welcome/__test__/welcome.test.tsx index fa94149caa..f7ae85bfea 100644 --- a/src/renderer/components/+welcome/__test__/welcome.test.tsx +++ b/src/renderer/components/+welcome/__test__/welcome.test.tsx @@ -22,50 +22,60 @@ import React from "react"; import { screen } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; -import { Welcome } from "../welcome"; -import { TopBarRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries"; -import { defaultWidth } from "../welcome"; +import { defaultWidth, Welcome } from "../welcome"; +import { computed } from "mobx"; +import { TopBarRegistry } from "../../../../extensions/registries"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import type { DiRender } from "../../test-utils/renderFor"; import { renderFor } from "../../test-utils/renderFor"; +import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; +import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; +import { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import type { WelcomeBannerRegistration } from "../welcome-banner-items/welcome-banner-registration"; -jest.mock( - "electron", - () => ({ - ipcRenderer: { - on: jest.fn(), - }, - app: { - getPath: () => "tmp", - }, - }), -); +jest.mock("electron", () => ({ + ipcRenderer: { + on: jest.fn(), + }, + app: { + getPath: () => "tmp", + }, +})); describe("", () => { let render: DiRender; + let di: ConfigurableDependencyInjectionContainer; + let welcomeBannersStub: WelcomeBannerRegistration[]; beforeEach(() => { - const di = getDiForUnitTesting(); + di = getDiForUnitTesting(); render = renderFor(di); + welcomeBannersStub = []; + + di.override(rendererExtensionsInjectable, () => + computed(() => [ + new TestExtension({ + id: "some-id", + welcomeBanners: welcomeBannersStub, + }), + ]), + ); + TopBarRegistry.createInstance(); - WelcomeBannerRegistry.createInstance(); }); afterEach(() => { TopBarRegistry.resetInstance(); - WelcomeBannerRegistry.resetInstance(); }); it("renders registered in WelcomeBannerRegistry and hide logo", async () => { const testId = "testId"; - WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { - Banner: () =>
    , - }, - ]); + welcomeBannersStub.push({ + Banner: () =>
    , + }); const { container } = render(); @@ -74,16 +84,15 @@ describe("", () => { }); it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => { - WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { - width: 100, - Banner: () =>
    , - }, - { - width: 800, - Banner: () =>
    , - }, - ]); + welcomeBannersStub.push({ + width: 100, + Banner: () =>
    , + }); + + welcomeBannersStub.push({ + width: 800, + Banner: () =>
    , + }); render(); @@ -99,3 +108,25 @@ describe("", () => { }); }); }); + +class TestExtension extends LensRendererExtension { + constructor({ + id, + welcomeBanners, + }: { + id: string; + welcomeBanners: WelcomeBannerRegistration[]; + }) { + super({ + id, + absolutePath: "irrelevant", + isBundled: false, + isCompatible: false, + isEnabled: false, + manifest: { name: id, version: "some-version" }, + manifestPath: "irrelevant", + }); + + this.welcomeBanners = welcomeBanners; + } +} diff --git a/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-items.injectable.ts b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-items.injectable.ts new file mode 100644 index 0000000000..288bb540ce --- /dev/null +++ b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-items.injectable.ts @@ -0,0 +1,37 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; +import { computed } from "mobx"; + +const welcomeBannerItemsInjectable = getInjectable({ + instantiate: (di) => { + const extensions = di.inject(rendererExtensionsInjectable); + + return computed(() => [ + ...extensions.get().flatMap((extension) => extension.welcomeBanners), + ]); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default welcomeBannerItemsInjectable; diff --git a/src/extensions/registries/welcome-banner-registry.ts b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts similarity index 91% rename from src/extensions/registries/welcome-banner-registry.ts rename to src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts index 1102dc8a3f..ec7cf3cbdb 100644 --- a/src/extensions/registries/welcome-banner-registry.ts +++ b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts @@ -19,8 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { BaseRegistry } from "./base-registry"; - /** * WelcomeBannerRegistration is for an extension to register * Provide a Banner component to be renderered in the welcome screen. @@ -35,5 +33,3 @@ export interface WelcomeBannerRegistration { */ width?: number } - -export class WelcomeBannerRegistry extends BaseRegistry { } diff --git a/src/renderer/components/+welcome/welcome.tsx b/src/renderer/components/+welcome/welcome.tsx index f90ede88b8..56aefbfc3c 100644 --- a/src/renderer/components/+welcome/welcome.tsx +++ b/src/renderer/components/+welcome/welcome.tsx @@ -26,22 +26,24 @@ import type { IComputedValue } from "mobx"; import Carousel from "react-material-ui-carousel"; import { Icon } from "../icon"; import { productName, slackUrl } from "../../../common/vars"; -import { WelcomeBannerRegistry } from "../../../extensions/registries"; import { withInjectables } from "@ogre-tools/injectable-react"; import welcomeMenuItemsInjectable from "./welcome-menu-items/welcome-menu-items.injectable"; import type { WelcomeMenuRegistration } from "./welcome-menu-items/welcome-menu-registration"; +import welcomeBannerItemsInjectable from "./welcome-banner-items/welcome-banner-items.injectable"; +import type { WelcomeBannerRegistration } from "./welcome-banner-items/welcome-banner-registration"; export const defaultWidth = 320; interface Dependencies { welcomeMenuItems: IComputedValue + welcomeBannerItems: IComputedValue } -const NonInjectedWelcome: React.FC = ({ welcomeMenuItems }) => { - const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems(); +const NonInjectedWelcome: React.FC = ({ welcomeMenuItems, welcomeBannerItems }) => { + const welcomeBanners = welcomeBannerItems.get(); // if there is banner with specified width, use it to calculate the width of the container - const maxWidth = welcomeBanner.reduce((acc, curr) => { + const maxWidth = welcomeBanners.reduce((acc, curr) => { const currWidth = curr.width ?? 0; if (acc > currWidth) { @@ -57,10 +59,10 @@ const NonInjectedWelcome: React.FC = ({ welcomeMenuItems }) => { style={{ width: `${maxWidth}px` }} data-testid="welcome-banner-container" > - {welcomeBanner.length > 0 ? ( + {welcomeBanners.length > 0 ? ( 1} + indicators={welcomeBanners.length > 1} autoPlay={true} navButtonsAlwaysInvisible={true} indicatorIconButtonProps={{ @@ -75,7 +77,7 @@ const NonInjectedWelcome: React.FC = ({ welcomeMenuItems }) => { }} interval={8000} > - {welcomeBanner.map((item, index) => ( + {welcomeBanners.map((item, index) => ( ))} @@ -142,6 +144,7 @@ export const Welcome = withInjectables( { getProps: (di) => ({ welcomeMenuItems: di.inject(welcomeMenuItemsInjectable), + welcomeBannerItems: di.inject(welcomeBannerItemsInjectable), }), }, ); diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts index 4fddb35b3d..3130fe6984 100644 --- a/src/renderer/initializers/registries.ts +++ b/src/renderer/initializers/registries.ts @@ -33,7 +33,6 @@ export function initRegistries() { registries.KubeObjectMenuRegistry.createInstance(); registries.KubeObjectStatusRegistry.createInstance(); registries.StatusBarRegistry.createInstance(); - registries.WelcomeBannerRegistry.createInstance(); registries.WorkloadsOverviewDetailRegistry.createInstance(); registries.TopBarRegistry.createInstance(); } From d8dbe51e7a74595999d3514f4e32818689b8a848 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Tue, 28 Dec 2021 17:10:06 +0300 Subject: [PATCH 04/10] Fix close button overflow in Preferences (#4611) * Adding basic colors to tailwind theme Signed-off-by: Alex Andreev * Using tailwind inline to style close button Signed-off-by: Alex Andreev * Make Select look similar to inputs Signed-off-by: Alex Andreev * Moving styles into separate module Signed-off-by: Alex Andreev * Convert tailwind commands to css Signed-off-by: Alex Andreev --- .../layout/close-button.module.scss | 52 +++++++++++++++++++ .../components/layout/close-button.tsx | 41 +++++++++++++++ .../components/layout/setting-layout.scss | 36 +------------ .../components/layout/setting-layout.tsx | 11 ++-- src/renderer/components/select/select.scss | 7 ++- tailwind.config.js | 9 +++- 6 files changed, 111 insertions(+), 45 deletions(-) create mode 100644 src/renderer/components/layout/close-button.module.scss create mode 100644 src/renderer/components/layout/close-button.tsx diff --git a/src/renderer/components/layout/close-button.module.scss b/src/renderer/components/layout/close-button.module.scss new file mode 100644 index 0000000000..57a7296339 --- /dev/null +++ b/src/renderer/components/layout/close-button.module.scss @@ -0,0 +1,52 @@ +/** + * 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. + */ + +.closeButton { + width: 35px; + height: 35px; + display: grid; + place-items: center; + cursor: pointer; + border: 2px solid var(--textColorDimmed); + border-radius: 50%; + + &:hover { + background-color: #72767d25; + } + + &:active { + transform: translateY(1px); + } +} + +.icon { + color: var(--textColorAccent); + opacity: 0.6; +} + +.esc { + text-align: center; + margin-top: var(--margin); + font-weight: bold; + user-select: none; + color: var(--textColorDimmed); + pointer-events: none; +} \ No newline at end of file diff --git a/src/renderer/components/layout/close-button.tsx b/src/renderer/components/layout/close-button.tsx new file mode 100644 index 0000000000..9bdf78f0bd --- /dev/null +++ b/src/renderer/components/layout/close-button.tsx @@ -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 styles from "./close-button.module.scss"; + +import React, { HTMLAttributes } from "react"; +import { Icon } from "../icon"; + +interface Props extends HTMLAttributes { +} + +export function CloseButton(props: Props) { + return ( +
    +
    + +
    + +
    + ); +} diff --git a/src/renderer/components/layout/setting-layout.scss b/src/renderer/components/layout/setting-layout.scss index 03472f5579..abb8db3e48 100644 --- a/src/renderer/components/layout/setting-layout.scss +++ b/src/renderer/components/layout/setting-layout.scss @@ -138,41 +138,7 @@ } > .toolsRegion { - .fixedTools { - position: fixed; - top: 60px; - - .closeBtn { - width: 35px; - height: 35px; - display: grid; - place-items: center; - border: 2px solid var(--textColorDimmed); - border-radius: 50%; - cursor: pointer; - - &:hover { - background-color: #72767d4d; - } - - &:active { - transform: translateY(1px); - } - - .Icon { - color: var(--textColorTertiary); - } - } - - .esc { - text-align: center; - margin-top: 4px; - font-weight: 600; - font-size: 14px; - color: var(--textColorDimmed); - pointer-events: none; - } - } + width: 45px; } } diff --git a/src/renderer/components/layout/setting-layout.tsx b/src/renderer/components/layout/setting-layout.tsx index 282eed0dcb..96fad8ea1f 100644 --- a/src/renderer/components/layout/setting-layout.tsx +++ b/src/renderer/components/layout/setting-layout.tsx @@ -25,8 +25,8 @@ import React from "react"; import { observer } from "mobx-react"; import { cssNames, IClassName } from "../../utils"; import { navigation } from "../../navigation"; -import { Icon } from "../icon"; import { catalogURL } from "../../../common/routes"; +import { CloseButton } from "./close-button"; export interface SettingLayoutProps extends React.DOMAttributes { className?: IClassName; @@ -104,13 +104,8 @@ export class SettingLayout extends React.Component {
    { this.props.provideBackButtonNavigation && ( -
    -
    - -
    - +
    +
    ) } diff --git a/src/renderer/components/select/select.scss b/src/renderer/components/select/select.scss index 8f5a3fa0e9..7e731a9aca 100644 --- a/src/renderer/components/select/select.scss +++ b/src/renderer/components/select/select.scss @@ -228,10 +228,15 @@ html { } .Select { + &__value-container { + margin-top: 2px; + margin-bottom: 2px; + } + &__control { box-shadow: 0 0 0 1px var(--inputControlBorder); background: var(--inputControlBackground); - border-radius: 5px; + border-radius: var(--border-radius); } &__single-value { diff --git a/tailwind.config.js b/tailwind.config.js index 1d2f2c9506..7357650c87 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -26,7 +26,14 @@ module.exports = { fontFamily: { sans: ["Roboto", "Helvetica", "Arial", "sans-serif"], }, - extend: {}, + extend: { + colors: { + textAccent: "var(--textColorAccent)", + textPrimary: "var(--textColorPrimary)", + textTertiary: "var(--textColorTertiary)", + textDimmed: "var(--textColorDimmed)", + }, + }, }, variants: { extend: {}, From 5bdfea6e31a2575c9d3507355ac018f979e966b6 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Thu, 30 Dec 2021 13:58:28 +0300 Subject: [PATCH 05/10] Native switch component (#4610) * Switch component initial draft Signed-off-by: Alex Andreev * Add onClick event Signed-off-by: Alex Andreev * onClick fine-tunings Signed-off-by: Alex Andreev * Fine-tuning styles Signed-off-by: Alex Andreev * Adding tests Signed-off-by: Alex Andreev * Fix light theme thumb color Signed-off-by: Alex Andreev * Using native switch in places Signed-off-by: Alex Andreev * Removing material ui switcher Signed-off-by: Alex Andreev * Revert "Removing material ui switcher" This reverts commit 6b9e0a090c7ff239fd7fbe071a16ff04448ab8c0. * Mark Switcher and FormSwitch as deprecated Signed-off-by: Alex Andreev * Cleaning up Signed-off-by: Alex Andreev * Using theme-light mixin Signed-off-by: Alex Andreev * Fix fetching values from onChange callback Signed-off-by: Alex Andreev * Add custon onChange event with checked prop Signed-off-by: Alex Andreev * Check for onChange() availability Signed-off-by: Alex Andreev * Fix show minimap label Signed-off-by: Alex Andreev --- .../components/+preferences/application.tsx | 31 ++--- .../components/+preferences/editor.tsx | 17 +-- .../+preferences/kubectl-binaries.tsx | 18 +-- .../components/+preferences/proxy.tsx | 16 +-- .../switch/__tests__/switch.test.tsx | 67 ++++++++++ .../components/switch/form-switcher.tsx | 3 + src/renderer/components/switch/index.ts | 1 + .../components/switch/switch.module.scss | 121 ++++++++++++++++++ src/renderer/components/switch/switch.tsx | 38 ++++++ src/renderer/components/switch/switcher.tsx | 3 + 10 files changed, 262 insertions(+), 53 deletions(-) create mode 100644 src/renderer/components/switch/__tests__/switch.test.tsx create mode 100644 src/renderer/components/switch/switch.module.scss create mode 100644 src/renderer/components/switch/switch.tsx diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx index 40859cd5e0..1e3c75b68f 100644 --- a/src/renderer/components/+preferences/application.tsx +++ b/src/renderer/components/+preferences/application.tsx @@ -27,7 +27,7 @@ import { ThemeStore } from "../../theme.store"; import { UserStore } from "../../../common/user-store"; import { Input } from "../input"; import { isWindows } from "../../../common/vars"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; import moment from "moment-timezone"; import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers"; import { action } from "mobx"; @@ -86,16 +86,12 @@ export const Application = observer(() => {
    - userStore.terminalCopyOnSelect = v.target.checked} - name="terminalCopyOnSelect" - /> - } - /> + userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect} + > + Copy on select and paste on right-click +

    @@ -135,16 +131,9 @@ export const Application = observer(() => {
    - userStore.openAtLogin = v.target.checked} - name="startup" - /> - } - label="Automatically start Lens on login" - /> + userStore.openAtLogin = !userStore.openAtLogin}> + Automatically start Lens on login +

    diff --git a/src/renderer/components/+preferences/editor.tsx b/src/renderer/components/+preferences/editor.tsx index 6f346eb63d..523f9bd278 100644 --- a/src/renderer/components/+preferences/editor.tsx +++ b/src/renderer/components/+preferences/editor.tsx @@ -21,7 +21,7 @@ import { observer } from "mobx-react"; import React from "react"; import { UserStore } from "../../../common/user-store"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; import { Select } from "../select"; import { SubTitle } from "../layout/sub-title"; import { SubHeader } from "../layout/sub-header"; @@ -45,15 +45,12 @@ export const Editor = observer(() => {
    - Show minimap} - control={ - editorConfiguration.minimap.enabled = checked} - /> - } - /> + editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled} + > + Show minimap +
    Position diff --git a/src/renderer/components/+preferences/kubectl-binaries.tsx b/src/renderer/components/+preferences/kubectl-binaries.tsx index bffc69e3c0..13a0299ee3 100644 --- a/src/renderer/components/+preferences/kubectl-binaries.tsx +++ b/src/renderer/components/+preferences/kubectl-binaries.tsx @@ -26,7 +26,7 @@ import { getDefaultKubectlDownloadPath, UserStore } from "../../../common/user-s import { observer } from "mobx-react"; import { bundledKubectlPath } from "../../../main/kubectl"; import { SelectOption, Select } from "../select"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; import { packageMirrors } from "../../../common/user-store/preferences-helpers"; export const KubectlBinaries = observer(() => { @@ -48,16 +48,12 @@ export const KubectlBinaries = observer(() => { <>
    - userStore.downloadKubectlBinaries = v.target.checked} - name="kubectl-download" - /> - } - label="Download kubectl binaries matching the Kubernetes cluster version" - /> + userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries} + > + Download kubectl binaries matching the Kubernetes cluster version +
    diff --git a/src/renderer/components/+preferences/proxy.tsx b/src/renderer/components/+preferences/proxy.tsx index b85bd5e0e1..f25c1fbc9b 100644 --- a/src/renderer/components/+preferences/proxy.tsx +++ b/src/renderer/components/+preferences/proxy.tsx @@ -24,10 +24,11 @@ import React from "react"; import { UserStore } from "../../../common/user-store"; import { Input } from "../input"; import { SubTitle } from "../layout/sub-title"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; export const LensProxy = observer(() => { const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || ""); + const store = UserStore.getInstance(); return (
    @@ -50,16 +51,9 @@ export const LensProxy = observer(() => {
    - UserStore.getInstance().allowUntrustedCAs = v.target.checked} - name="startup" - /> - } - label="Allow untrusted Certificate Authorities" - /> + store.allowUntrustedCAs = !store.allowUntrustedCAs}> + Allow untrusted Certificate Authorities + This will make Lens to trust ANY certificate authority without any validations.{" "} Needed with some corporate proxies that do certificate re-writing.{" "} diff --git a/src/renderer/components/switch/__tests__/switch.test.tsx b/src/renderer/components/switch/__tests__/switch.test.tsx new file mode 100644 index 0000000000..d5187e1417 --- /dev/null +++ b/src/renderer/components/switch/__tests__/switch.test.tsx @@ -0,0 +1,67 @@ +/** + * 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 { fireEvent, render } from "@testing-library/react"; +import "@testing-library/jest-dom/extend-expect"; +import { Switch } from ".."; + +describe("", () => { + it("renders w/o errors", () => { + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("render label text", () => { + const { getByLabelText } = render(Test label); + + expect(getByLabelText("Test label")).toBeTruthy(); + }); + + it("passes disabled and checked attributes to input", () => { + const { container } = render(); + const checkbox = container.querySelector("input[type=checkbox]"); + + expect(checkbox).toHaveAttribute("disabled"); + expect(checkbox).toHaveAttribute("checked"); + }); + + it("onClick event fired", () => { + const onClick = jest.fn(); + const { getByTestId } = render(); + const switcher = getByTestId("switch"); + + fireEvent.click(switcher); + + expect(onClick).toHaveBeenCalled(); + }); + + it("onClick event not fired for disabled item", () => { + const onClick = jest.fn(); + const { getByTestId } = render(); + const switcher = getByTestId("switch"); + + fireEvent.click(switcher); + + expect(onClick).not.toHaveBeenCalled(); + }); +}); diff --git a/src/renderer/components/switch/form-switcher.tsx b/src/renderer/components/switch/form-switcher.tsx index 14df95f676..6499fb0b0f 100644 --- a/src/renderer/components/switch/form-switcher.tsx +++ b/src/renderer/components/switch/form-switcher.tsx @@ -35,6 +35,9 @@ const useStyles = makeStyles({ }, }); +/** + * @deprecated Use instead from "../switch.tsx". + */ export function FormSwitch(props: FormControlLabelProps) { const classes = useStyles(); diff --git a/src/renderer/components/switch/index.ts b/src/renderer/components/switch/index.ts index 60d44f3324..6987ce5b38 100644 --- a/src/renderer/components/switch/index.ts +++ b/src/renderer/components/switch/index.ts @@ -21,3 +21,4 @@ export * from "./switcher"; export * from "./form-switcher"; +export * from "./switch"; diff --git a/src/renderer/components/switch/switch.module.scss b/src/renderer/components/switch/switch.module.scss new file mode 100644 index 0000000000..b890919c61 --- /dev/null +++ b/src/renderer/components/switch/switch.module.scss @@ -0,0 +1,121 @@ +/** + * 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. + */ + +.Switch { + --thumb-size: 2rem; + --thumb-color: hsl(0 0% 100%); + --thumb-color-highlight: hsl(0 0% 100% / 25%); + + --track-size: calc(var(--thumb-size) * 2); + --track-padding: 2px; + --track-color-inactive: hsl(80 0% 35%); + --track-color-active: hsl(110, 60%, 60%); + + --thumb-position: 0%; + --thumb-transition-duration: .25s; + + --hover-highlight-size: 0; + + display: flex; + align-items: center; + gap: 2ch; + justify-content: space-between; + cursor: pointer; + user-select: none; + color: var(--textColorAccent); + font-weight: 500; + + & > input { + padding: var(--track-padding); + background: var(--track-color-inactive); + inline-size: var(--track-size); + block-size: var(--thumb-size); + border-radius: var(--track-size); + + appearance: none; + pointer-events: none; + border: none; + outline-offset: 5px; + box-sizing: content-box; + + flex-shrink: 0; + display: grid; + align-items: center; + grid: [track] 1fr / [track] 1fr; + + transition: background-color .25s ease; + + &::before { + content: ""; + cursor: pointer; + pointer-events: auto; + grid-area: track; + inline-size: var(--thumb-size); + block-size: var(--thumb-size); + background: var(--thumb-color); + box-shadow: 0 0 0 var(--hover-highlight-size) var(--thumb-color-highlight); + border-radius: 50%; + transform: translateX(var(--thumb-position)); + transition: + transform var(--thumb-transition-duration) ease, + box-shadow .25s ease; + } + + &:not(:disabled):hover::before { + --hover-highlight-size: .5rem; + } + + &:checked { + background: var(--track-color-active); + --thumb-position: 100%; + } + + &:disabled { + --track-color-inactive: hsl(80 0% 30%); + --thumb-color: transparent; + + &::before { + cursor: not-allowed; + box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 40%); + } + } + + &:focus-visible { + box-shadow: 0 0 0 2px var(--blue); + } + } + + &.disabled { + cursor: not-allowed; + } +} + +@include theme-light { + .Switch { + --thumb-color-highlight: hsl(0 0% 0% / 25%); + + & > input { + &:disabled { + --track-color-inactive: hsl(80 0% 80%); + } + } + } +} diff --git a/src/renderer/components/switch/switch.tsx b/src/renderer/components/switch/switch.tsx new file mode 100644 index 0000000000..7bf5f2b3be --- /dev/null +++ b/src/renderer/components/switch/switch.tsx @@ -0,0 +1,38 @@ +/** + * 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 "./switch.module.scss"; + +import React, { ChangeEvent, HTMLProps } from "react"; +import { cssNames } from "../../utils"; + +interface Props extends Omit, "onChange"> { + onChange?: (checked: boolean, event: ChangeEvent) => void; +} + +export function Switch({ children, disabled, onChange, ...props }: Props) { + return ( + + ); +} diff --git a/src/renderer/components/switch/switcher.tsx b/src/renderer/components/switch/switcher.tsx index 749e5134c9..136175e394 100644 --- a/src/renderer/components/switch/switcher.tsx +++ b/src/renderer/components/switch/switcher.tsx @@ -31,6 +31,9 @@ interface Props extends SwitchProps { classes: Styles; } +/** + * @deprecated Use instead from "../switch.tsx". + */ export const Switcher = withStyles((theme: Theme) => createStyles({ root: { From 6ca58570b02ab9af804af84ce0763c2b7065c806 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Wed, 8 Dec 2021 16:15:11 +0200 Subject: [PATCH 06/10] Replace TopBarRegistry with reactive solution Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen --- .../extension-loader/extension-loader.ts | 1 - src/extensions/lens-renderer-extension.ts | 3 +- src/extensions/registries/index.ts | 1 - .../+welcome/__test__/welcome.test.tsx | 7 -- .../cluster-manager/cluster-manager.tsx | 2 +- .../top-bar-items/top-bar-items.injectable.ts | 37 +++++++++ .../layout/top-bar/top-bar-registration.d.ts} | 7 -- .../top-bar-win-linux.test.tsx} | 32 +++++--- .../top-bar.module.scss} | 0 .../top-bar.test.tsx} | 30 ++++--- .../{topbar.tsx => top-bar/top-bar.tsx} | 82 ++++++++++--------- src/renderer/initializers/registries.ts | 1 - 12 files changed, 124 insertions(+), 79 deletions(-) create mode 100644 src/renderer/components/layout/top-bar/top-bar-items/top-bar-items.injectable.ts rename src/{extensions/registries/topbar-registry.ts => renderer/components/layout/top-bar/top-bar-registration.d.ts} (88%) rename src/renderer/components/layout/{__tests__/topbar-win-linux.test.tsx => top-bar/top-bar-win-linux.test.tsx} (83%) rename src/renderer/components/layout/{topbar.module.scss => top-bar/top-bar.module.scss} (100%) rename src/renderer/components/layout/{__tests__/topbar.test.tsx => top-bar/top-bar.test.tsx} (84%) rename src/renderer/components/layout/{topbar.tsx => top-bar/top-bar.tsx} (76%) diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index db3e924209..00fb397079 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -264,7 +264,6 @@ export class ExtensionLoader { registries.StatusBarRegistry.getInstance().add(extension.statusBarItems), registries.CommandRegistry.getInstance().add(extension.commands), registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), - registries.TopBarRegistry.getInstance().add(extension.topBarItems), ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 2ccb2cfe7a..9f7b947d82 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -26,6 +26,7 @@ import type { CatalogEntity } from "../common/catalog"; import type { Disposer } from "../common/utils"; import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry"; import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry"; +import type { TopBarRegistration } from "../renderer/components/layout/top-bar/top-bar-registration"; import type { KubernetesCluster } from "../common/catalog-entities"; import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/welcome-menu-items/welcome-menu-registration"; import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration"; @@ -45,7 +46,7 @@ export class LensRendererExtension extends LensExtension { welcomeMenus: WelcomeMenuRegistration[] = []; welcomeBanners: WelcomeBannerRegistration[] = []; catalogEntityDetailItems: registries.CatalogEntityDetailRegistration[] = []; - topBarItems: registries.TopBarRegistration[] = []; + topBarItems: TopBarRegistration[] = []; async navigate

    (pageId?: string, params?: P) { const { navigate } = await import("../renderer/navigation"); diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 2cd822f77a..76f6c05d11 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -32,5 +32,4 @@ export * from "./command-registry"; export * from "./entity-setting-registry"; export * from "./catalog-entity-detail-registry"; export * from "./workloads-overview-detail-registry"; -export * from "./topbar-registry"; export * from "./protocol-handler"; diff --git a/src/renderer/components/+welcome/__test__/welcome.test.tsx b/src/renderer/components/+welcome/__test__/welcome.test.tsx index f7ae85bfea..74cf65b20d 100644 --- a/src/renderer/components/+welcome/__test__/welcome.test.tsx +++ b/src/renderer/components/+welcome/__test__/welcome.test.tsx @@ -24,7 +24,6 @@ import { screen } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { defaultWidth, Welcome } from "../welcome"; import { computed } from "mobx"; -import { TopBarRegistry } from "../../../../extensions/registries"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import type { DiRender } from "../../test-utils/renderFor"; import { renderFor } from "../../test-utils/renderFor"; @@ -62,12 +61,6 @@ describe("", () => { }), ]), ); - - TopBarRegistry.createInstance(); - }); - - afterEach(() => { - TopBarRegistry.resetInstance(); }); it("renders registered in WelcomeBannerRegistry and hide logo", async () => { diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 04af444c30..300c9b2eb4 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -39,8 +39,8 @@ import { DeleteClusterDialog } from "../delete-cluster-dialog"; import { reaction } from "mobx"; import { navigation } from "../../navigation"; import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync"; -import { TopBar } from "../layout/topbar"; import { catalogURL, getPreviousTabUrl } from "../../../common/routes"; +import { TopBar } from "../layout/top-bar/top-bar"; @observer export class ClusterManager extends React.Component { diff --git a/src/renderer/components/layout/top-bar/top-bar-items/top-bar-items.injectable.ts b/src/renderer/components/layout/top-bar/top-bar-items/top-bar-items.injectable.ts new file mode 100644 index 0000000000..9c5bd3d1e8 --- /dev/null +++ b/src/renderer/components/layout/top-bar/top-bar-items/top-bar-items.injectable.ts @@ -0,0 +1,37 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import rendererExtensionsInjectable from "../../../../../extensions/renderer-extensions.injectable"; + +const topBarItemsInjectable = getInjectable({ + instantiate: (di) => { + const extensions = di.inject(rendererExtensionsInjectable); + + return computed(() => + extensions.get().flatMap((extension) => extension.topBarItems), + ); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default topBarItemsInjectable; diff --git a/src/extensions/registries/topbar-registry.ts b/src/renderer/components/layout/top-bar/top-bar-registration.d.ts similarity index 88% rename from src/extensions/registries/topbar-registry.ts rename to src/renderer/components/layout/top-bar/top-bar-registration.d.ts index 37b55faaaa..5e470d4d26 100644 --- a/src/extensions/registries/topbar-registry.ts +++ b/src/renderer/components/layout/top-bar/top-bar-registration.d.ts @@ -18,10 +18,6 @@ * 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 React from "react"; -import { BaseRegistry } from "./base-registry"; - interface TopBarComponents { Item: React.ComponentType; } @@ -29,6 +25,3 @@ interface TopBarComponents { export interface TopBarRegistration { components: TopBarComponents; } - -export class TopBarRegistry extends BaseRegistry { -} diff --git a/src/renderer/components/layout/__tests__/topbar-win-linux.test.tsx b/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx similarity index 83% rename from src/renderer/components/layout/__tests__/topbar-win-linux.test.tsx rename to src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx index 1307e181cd..6d8014004e 100644 --- a/src/renderer/components/layout/__tests__/topbar-win-linux.test.tsx +++ b/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx @@ -20,23 +20,29 @@ */ import React from "react"; -import { render, fireEvent } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; -import { TopBar } from "../topbar"; -import { TopBarRegistry } from "../../../../extensions/registries"; +import { TopBar } from "./top-bar"; import { IpcMainWindowEvents } from "../../../../main/window-manager"; import { broadcastMessage } from "../../../../common/ipc"; import * as vars from "../../../../common/vars"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import { DiRender, renderFor } from "../../test-utils/renderFor"; -const mockConfig = vars as { isWindows: boolean, isLinux: boolean }; +const mockConfig = vars as { isWindows: boolean; isLinux: boolean }; jest.mock("../../../../common/ipc"); jest.mock("../../../../common/vars", () => { + const SemVer = require("semver").SemVer; + + const versionStub = new SemVer("1.0.0"); + return { __esModule: true, isWindows: null, isLinux: null, + appSemVer: versionStub, }; }); @@ -57,20 +63,20 @@ jest.mock("@electron/remote", () => { }; }); -describe(" in Windows and Linux", () => { - beforeEach(() => { - TopBarRegistry.createInstance(); - }); +describe(" in Windows and Linux", () => { + let render: DiRender; - afterEach(() => { - TopBarRegistry.resetInstance(); + beforeEach(() => { + const di = getDiForUnitTesting(); + + render = renderFor(di); }); it("shows window controls on Windows", () => { mockConfig.isWindows = true; mockConfig.isLinux = false; - const { getByTestId } = render(); + const { getByTestId } = render(); expect(getByTestId("window-menu")).toBeInTheDocument(); expect(getByTestId("window-minimize")).toBeInTheDocument(); @@ -82,7 +88,7 @@ describe(" in Windows and Linux", () => { mockConfig.isWindows = false; mockConfig.isLinux = true; - const { getByTestId } = render(); + const { getByTestId } = render(); expect(getByTestId("window-menu")).toBeInTheDocument(); expect(getByTestId("window-minimize")).toBeInTheDocument(); @@ -93,7 +99,7 @@ describe(" in Windows and Linux", () => { it("triggers ipc events on click", () => { mockConfig.isWindows = true; - const { getByTestId } = render(); + const { getByTestId } = render(); const menu = getByTestId("window-menu"); const minimize = getByTestId("window-minimize"); diff --git a/src/renderer/components/layout/topbar.module.scss b/src/renderer/components/layout/top-bar/top-bar.module.scss similarity index 100% rename from src/renderer/components/layout/topbar.module.scss rename to src/renderer/components/layout/top-bar/top-bar.module.scss diff --git a/src/renderer/components/layout/__tests__/topbar.test.tsx b/src/renderer/components/layout/top-bar/top-bar.test.tsx similarity index 84% rename from src/renderer/components/layout/__tests__/topbar.test.tsx rename to src/renderer/components/layout/top-bar/top-bar.test.tsx index 110ddef800..a2a93c0c4c 100644 --- a/src/renderer/components/layout/__tests__/topbar.test.tsx +++ b/src/renderer/components/layout/top-bar/top-bar.test.tsx @@ -20,14 +20,23 @@ */ import React from "react"; -import { render, fireEvent } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; -import { TopBar } from "../topbar"; -import { TopBarRegistry } from "../../../../extensions/registries"; +import { TopBar } from "./top-bar"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; +import { DiRender, renderFor } from "../../test-utils/renderFor"; +import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable"; +import { computed } from "mobx"; jest.mock("../../../../common/vars", () => { + const SemVer = require("semver").SemVer; + + const versionStub = new SemVer("1.0.0"); + return { isMac: true, + appSemVer: versionStub, }; }); @@ -76,12 +85,13 @@ jest.mock("@electron/remote", () => { }); describe("", () => { - beforeEach(() => { - TopBarRegistry.createInstance(); - }); + let di: ConfigurableDependencyInjectionContainer; + let render: DiRender; - afterEach(() => { - TopBarRegistry.resetInstance(); + beforeEach(() => { + di = getDiForUnitTesting(); + + render = renderFor(di); }); it("renders w/o errors", () => { @@ -129,13 +139,13 @@ describe("", () => { const testId = "testId"; const text = "an item"; - TopBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ + di.override(topBarItemsInjectable, () => computed(() => [ { components: { Item: () => {text}, }, }, - ]); + ])); const { getByTestId } = render(); diff --git a/src/renderer/components/layout/topbar.tsx b/src/renderer/components/layout/top-bar/top-bar.tsx similarity index 76% rename from src/renderer/components/layout/topbar.tsx rename to src/renderer/components/layout/top-bar/top-bar.tsx index 4705c6993c..d960cbe8f9 100644 --- a/src/renderer/components/layout/topbar.tsx +++ b/src/renderer/components/layout/top-bar/top-bar.tsx @@ -19,22 +19,28 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import styles from "./topbar.module.scss"; +import styles from "./top-bar.module.scss"; import React, { useEffect, useMemo, useRef } from "react"; import { observer } from "mobx-react"; -import { TopBarRegistry } from "../../../extensions/registries"; -import { Icon } from "../icon"; +import type { IComputedValue } from "mobx"; +import { Icon } from "../../icon"; import { webContents, getCurrentWindow } from "@electron/remote"; import { observable } from "mobx"; -import { broadcastMessage, ipcRendererOn } from "../../../common/ipc"; -import { watchHistoryState } from "../../remote-helpers/history-updater"; -import { isActiveRoute, navigate } from "../../navigation"; -import { catalogRoute, catalogURL } from "../../../common/routes"; -import { IpcMainWindowEvents } from "../../../main/window-manager"; -import { isLinux, isWindows } from "../../../common/vars"; -import { cssNames } from "../../utils"; +import { broadcastMessage, ipcRendererOn } from "../../../../common/ipc"; +import { watchHistoryState } from "../../../remote-helpers/history-updater"; +import { isActiveRoute, navigate } from "../../../navigation"; +import { catalogRoute, catalogURL } from "../../../../common/routes"; +import { IpcMainWindowEvents } from "../../../../main/window-manager"; +import { isLinux, isWindows } from "../../../../common/vars"; +import { cssNames } from "../../../utils"; +import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import type { TopBarRegistration } from "./top-bar-registration"; -interface Props extends React.HTMLAttributes { +interface Props extends React.HTMLAttributes {} + +interface Dependencies { + items: IComputedValue; } const prevEnabled = observable.box(false); @@ -48,34 +54,10 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => { nextEnabled.set(state); }); -export const TopBar = observer(({ children, ...rest }: Props) => { +const NonInjectedTopBar: React.FC = (({ items, children, ...rest }) => { const elem = useRef(); const window = useMemo(() => getCurrentWindow(), []); - const renderRegisteredItems = () => { - const items = TopBarRegistry.getInstance().getItems(); - - if (!Array.isArray(items)) { - return null; - } - - return ( -

    - {items.map((registration, index) => { - if (!registration?.components?.Item) { - return null; - } - - return ( -
    - -
    - ); - })} -
    - ); - }; - const openContextMenu = () => { broadcastMessage(IpcMainWindowEvents.OPEN_CONTEXT_MENU); }; @@ -156,7 +138,7 @@ export const TopBar = observer(({ children, ...rest }: Props) => { />
    - {renderRegisteredItems()} + {renderRegisteredItems(items.get())} {children} {(isWindows || isLinux) && (
    @@ -174,3 +156,29 @@ export const TopBar = observer(({ children, ...rest }: Props) => {
    ); }); + +const renderRegisteredItems = (items: TopBarRegistration[]) => ( +
    + {items.map((registration, index) => { + if (!registration?.components?.Item) { + return null; + } + + return ( +
    + +
    + ); + })} +
    +); + + + +export const TopBar = withInjectables(observer(NonInjectedTopBar), { + getProps: (di, props) => ({ + items: di.inject(topBarItemsInjectable), + + ...props, + }), +}); diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts index 3130fe6984..a27c47153b 100644 --- a/src/renderer/initializers/registries.ts +++ b/src/renderer/initializers/registries.ts @@ -34,5 +34,4 @@ export function initRegistries() { registries.KubeObjectStatusRegistry.createInstance(); registries.StatusBarRegistry.createInstance(); registries.WorkloadsOverviewDetailRegistry.createInstance(); - registries.TopBarRegistry.createInstance(); } From 71388a0ea32795c067d52f84572eeced0daf4f64 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Fri, 31 Dec 2021 08:47:51 +0200 Subject: [PATCH 07/10] Switch typing of a component to props instead of React.FC Signed-off-by: Janne Savolainen --- src/renderer/components/layout/top-bar/top-bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/layout/top-bar/top-bar.tsx b/src/renderer/components/layout/top-bar/top-bar.tsx index d960cbe8f9..a0a785666b 100644 --- a/src/renderer/components/layout/top-bar/top-bar.tsx +++ b/src/renderer/components/layout/top-bar/top-bar.tsx @@ -54,7 +54,7 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => { nextEnabled.set(state); }); -const NonInjectedTopBar: React.FC = (({ items, children, ...rest }) => { +const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) => { const elem = useRef(); const window = useMemo(() => getCurrentWindow(), []); From 1db805b45160a48f7c7b9965e73e44cfb28a57dc Mon Sep 17 00:00:00 2001 From: Juho Heikka Date: Fri, 31 Dec 2021 15:50:01 +0200 Subject: [PATCH 08/10] Extension tray menu items (#4619) * Add extension ability to add tray menu items. Signed-off-by: Juho Heikka * Add tray menu extension documentation Signed-off-by: Juho Heikka * Add tests to tray menu items. Fix autorun infinite loop. Signed-off-by: Juho Heikka * Fix documentation Signed-off-by: Juho Heikka * Remove unnecessary slice() Signed-off-by: Juho Heikka * Define a type for tray menu registration Signed-off-by: Juho Heikka * Change TrayMenuRegistration not to leak or depend on Electron Menu API Signed-off-by: Juho Heikka * Update trayMenus Extension API documentation Signed-off-by: Juho Heikka * Refactor all tests to use runInAction Signed-off-by: Juho Heikka --- .../capabilities/common-capabilities.md | 27 +++- docs/extensions/guides/main-extension.md | 34 ++++- src/extensions/lens-main-extension.ts | 3 +- src/main/index.ts | 6 +- .../menu/electron-menu-items.injectable.ts | 3 +- src/main/tray/tray-menu-items.injectable.ts | 36 +++++ src/main/tray/tray-menu-items.test.ts | 136 ++++++++++++++++++ src/main/tray/tray-menu-registration.d.ts | 30 ++++ src/main/{ => tray}/tray.ts | 46 ++++-- 9 files changed, 298 insertions(+), 23 deletions(-) create mode 100644 src/main/tray/tray-menu-items.injectable.ts create mode 100644 src/main/tray/tray-menu-items.test.ts create mode 100644 src/main/tray/tray-menu-registration.d.ts rename src/main/{ => tray}/tray.ts (70%) diff --git a/docs/extensions/capabilities/common-capabilities.md b/docs/extensions/capabilities/common-capabilities.md index 8ba03253c5..e7b585af66 100644 --- a/docs/extensions/capabilities/common-capabilities.md +++ b/docs/extensions/capabilities/common-capabilities.md @@ -37,9 +37,9 @@ export default class ExampleMainExtension extends Main.LensExtension { } ``` -### App Menus +### Menus -This extension can register custom app menus that will be displayed on OS native menus. +This extension can register custom app and tray menus that will be displayed on OS native menus. Example: @@ -56,6 +56,29 @@ export default class ExampleMainExtension extends Main.LensExtension { } } ] + + trayMenus = [ + { + label: "My links", + submenu: [ + { + label: "Lens", + click() { + Main.Navigation.navigate("https://k8slens.dev"); + } + }, + { + type: "separator" + }, + { + label: "Lens Github", + click() { + Main.Navigation.navigate("https://github.com/lensapp/lens"); + } + } + ] + } + ] } ``` diff --git a/docs/extensions/guides/main-extension.md b/docs/extensions/guides/main-extension.md index fec937db17..a0e20880bf 100644 --- a/docs/extensions/guides/main-extension.md +++ b/docs/extensions/guides/main-extension.md @@ -3,7 +3,7 @@ The Main Extension API is the interface to Lens's main process. Lens runs in both main and renderer processes. The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items and [protocol handlers](protocol-handlers.md), and run custom code in Lens's main process. -It also provides convenient methods for navigating to built-in Lens pages and extension pages, as well as adding and removing sources of catalog entities. +It also provides convenient methods for navigating to built-in Lens pages and extension pages, as well as adding and removing sources of catalog entities. ## `Main.LensExtension` Class @@ -45,7 +45,6 @@ For more details on accessing Lens state data, please see the [Stores](../stores ### `appMenus` The Main Extension API allows you to customize the UI application menu. -Note that this is the only UI feature that the Main Extension API allows you to customize. The following example demonstrates adding an item to the **Help** menu. ``` typescript @@ -65,7 +64,7 @@ export default class SamplePageMainExtension extends Main.LensExtension { ``` `appMenus` is an array of objects that satisfy the `MenuRegistration` interface. -`MenuRegistration` extends React's `MenuItemConstructorOptions` interface. +`MenuRegistration` extends Electron's `MenuItemConstructorOptions` interface. The properties of the appMenus array objects are defined as follows: * `parentId` is the name of the menu where your new menu item will be listed. @@ -96,6 +95,35 @@ export default class SamplePageMainExtension extends Main.LensExtension { When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`. This page would be defined in your extension's `Renderer.LensExtension` implementation (See [`Renderer.LensExtension`](renderer-extension.md)). +### `trayMenus` + +`trayMenus` is an array of `TrayMenuRegistration` objects. Most importantly you can define a `label` and a `click` handler. Other properties are `submenu`, `enabled`, `toolTip`, `id` and `type`. + +``` typescript +interface TrayMenuRegistration { + label?: string; + click?: (menuItem: TrayMenuRegistration) => void; + id?: string; + type?: "normal" | "separator" | "submenu" + toolTip?: string; + enabled?: boolean; + submenu?: TrayMenuRegistration[] +} +``` + +The following example demonstrates how tray menus can be added from extension: + +``` typescript +import { Main } from "@k8slens/extensions"; + +export default class SampleTrayMenuMainExtension extends Main.LensExtension { + trayMenus = [{ + label: "menu from the extension", + click: () => { console.log("tray menu clicked!") } + }] +} +``` + ### `addCatalogSource()` and `removeCatalogSource()` Methods The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities). diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index c0c0a5674a..00f08ca5e5 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -25,9 +25,10 @@ import { catalogEntityRegistry } from "../main/catalog"; import type { CatalogEntity } from "../common/catalog"; import type { IObservableArray } from "mobx"; import type { MenuRegistration } from "../main/menu/menu-registration"; - +import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration"; export class LensMainExtension extends LensExtension { appMenus: MenuRegistration[] = []; + trayMenus: TrayMenuRegistration[] = []; async navigate(pageId?: string, params?: Record, frameId?: number) { return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId); diff --git a/src/main/index.ts b/src/main/index.ts index 1f7b26e773..f40b92aa21 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -60,7 +60,7 @@ import { SentryInit } from "../common/sentry"; import { ensureDir } from "fs-extra"; import { Router } from "./router"; import { initMenu } from "./menu/menu"; -import { initTray } from "./tray"; +import { initTray } from "./tray/tray"; import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions"; import { AppPaths } from "../common/app-paths"; import { ShellSession } from "./shell-session/shell-session"; @@ -68,6 +68,7 @@ import { getDi } from "./getDi"; import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable"; import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable"; import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable"; +import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable"; const di = getDi(); @@ -104,6 +105,7 @@ mangleProxyEnv(); logger.debug("[APP-MAIN] initializing ipc main handlers"); const menuItems = di.inject(electronMenuItemsInjectable); +const trayMenuItems = di.inject(trayMenuItemsInjectable); initializers.initIpcMainHandlers(menuItems); @@ -244,7 +246,7 @@ app.on("ready", async () => { onQuitCleanup.push( initMenu(windowManager, menuItems), - initTray(windowManager), + initTray(windowManager, trayMenuItems), () => ShellSession.cleanup(), ); diff --git a/src/main/menu/electron-menu-items.injectable.ts b/src/main/menu/electron-menu-items.injectable.ts index dd70812d89..d0e5a650cf 100644 --- a/src/main/menu/electron-menu-items.injectable.ts +++ b/src/main/menu/electron-menu-items.injectable.ts @@ -29,8 +29,7 @@ const electronMenuItemsInjectable = getInjectable({ const extensions = di.inject(mainExtensionsInjectable); return computed(() => - extensions.get().flatMap((extension) => extension.appMenus), - ); + extensions.get().flatMap((extension) => extension.appMenus)); }, }); diff --git a/src/main/tray/tray-menu-items.injectable.ts b/src/main/tray/tray-menu-items.injectable.ts new file mode 100644 index 0000000000..8a31cc6af5 --- /dev/null +++ b/src/main/tray/tray-menu-items.injectable.ts @@ -0,0 +1,36 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import mainExtensionsInjectable from "../../extensions/main-extensions.injectable"; + +const trayItemsInjectable = getInjectable({ + lifecycle: lifecycleEnum.singleton, + + instantiate: (di) => { + const extensions = di.inject(mainExtensionsInjectable); + + return computed(() => + extensions.get().flatMap(extension => extension.trayMenus)); + }, +}); + +export default trayItemsInjectable; diff --git a/src/main/tray/tray-menu-items.test.ts b/src/main/tray/tray-menu-items.test.ts new file mode 100644 index 0000000000..b46bce3671 --- /dev/null +++ b/src/main/tray/tray-menu-items.test.ts @@ -0,0 +1,136 @@ +/** + * 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 { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; +import { LensMainExtension } from "../../extensions/lens-main-extension"; +import trayItemsInjectable from "./tray-menu-items.injectable"; +import type { IComputedValue } from "mobx"; +import { computed, ObservableMap, runInAction } from "mobx"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import mainExtensionsInjectable from "../../extensions/main-extensions.injectable"; +import type { TrayMenuRegistration } from "./tray-menu-registration"; + +describe("tray-menu-items", () => { + let di: ConfigurableDependencyInjectionContainer; + let trayMenuItems: IComputedValue; + let extensionsStub: ObservableMap; + + beforeEach(() => { + di = getDiForUnitTesting(); + + extensionsStub = new ObservableMap(); + + di.override( + mainExtensionsInjectable, + () => computed(() => [...extensionsStub.values()]), + ); + + trayMenuItems = di.inject(trayItemsInjectable); + }); + + it("does not have any items yet", () => { + expect(trayMenuItems.get()).toHaveLength(0); + }); + + describe("when extension is enabled", () => { + beforeEach(() => { + const someExtension = new SomeTestExtension({ + id: "some-extension-id", + trayMenus: [{ label: "tray-menu-from-some-extension" }], + }); + + runInAction(() => { + extensionsStub.set("some-extension-id", someExtension); + }); + }); + + it("has tray menu items", () => { + expect(trayMenuItems.get()).toEqual([ + { + label: "tray-menu-from-some-extension", + }, + ]); + }); + + it("when disabling extension, does not have tray menu items", () => { + runInAction(() => { + extensionsStub.delete("some-extension-id"); + }); + + expect(trayMenuItems.get()).toHaveLength(0); + }); + + describe("when other extension is enabled", () => { + beforeEach(() => { + const someOtherExtension = new SomeTestExtension({ + id: "some-extension-id", + trayMenus: [{ label: "some-label-from-second-extension" }], + }); + + runInAction(() => { + extensionsStub.set("some-other-extension-id", someOtherExtension); + }); + }); + + it("has tray menu items for both extensions", () => { + expect(trayMenuItems.get()).toEqual([ + { + label: "tray-menu-from-some-extension", + }, + + { + label: "some-label-from-second-extension", + }, + ]); + }); + + it("when extension is disabled, still returns tray menu items for extensions that are enabled", () => { + runInAction(() => { + extensionsStub.delete("some-other-extension-id"); + }); + + expect(trayMenuItems.get()).toEqual([ + { + label: "tray-menu-from-some-extension", + }, + ]); + }); + }); + }); +}); + +class SomeTestExtension extends LensMainExtension { + constructor({ id, trayMenus }: { + id: string; + trayMenus: TrayMenuRegistration[]; + }) { + super({ + id, + absolutePath: "irrelevant", + isBundled: false, + isCompatible: false, + isEnabled: false, + manifest: { name: id, version: "some-version" }, + manifestPath: "irrelevant", + }); + + this.trayMenus = trayMenus; + } +} diff --git a/src/main/tray/tray-menu-registration.d.ts b/src/main/tray/tray-menu-registration.d.ts new file mode 100644 index 0000000000..0cf7fe611b --- /dev/null +++ b/src/main/tray/tray-menu-registration.d.ts @@ -0,0 +1,30 @@ +/** + * 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. + */ + +export interface TrayMenuRegistration { + label?: string; + click?: (menuItem: TrayMenuRegistration) => void; + id?: string; + type?: "normal" | "separator" | "submenu" + toolTip?: string; + enabled?: boolean; + submenu?: TrayMenuRegistration[] +} diff --git a/src/main/tray.ts b/src/main/tray/tray.ts similarity index 70% rename from src/main/tray.ts rename to src/main/tray/tray.ts index 850d19fe21..ac3e140e3b 100644 --- a/src/main/tray.ts +++ b/src/main/tray/tray.ts @@ -20,16 +20,18 @@ */ import path from "path"; -import packageInfo from "../../package.json"; +import packageInfo from "../../../package.json"; import { Menu, Tray } from "electron"; -import { autorun } from "mobx"; -import { showAbout } from "./menu/menu"; -import { checkForUpdates, isAutoUpdateEnabled } from "./app-updater"; -import type { WindowManager } from "./window-manager"; -import logger from "./logger"; -import { isDevelopment, isWindows, productName } from "../common/vars"; -import { exitApp } from "./exit-app"; -import { preferencesURL } from "../common/routes"; +import { autorun, IComputedValue } from "mobx"; +import { showAbout } from "../menu/menu"; +import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater"; +import type { WindowManager } from "../window-manager"; +import logger from "../logger"; +import { isDevelopment, isWindows, productName } from "../../common/vars"; +import { exitApp } from "../exit-app"; +import { preferencesURL } from "../../common/routes"; +import { toJS } from "../../common/utils"; +import type { TrayMenuRegistration } from "./tray-menu-registration"; const TRAY_LOG_PREFIX = "[TRAY]"; @@ -44,7 +46,10 @@ export function getTrayIcon(): string { ); } -export function initTray(windowManager: WindowManager) { +export function initTray( + windowManager: WindowManager, + trayMenuItems: IComputedValue, +) { const icon = getTrayIcon(); tray = new Tray(icon); @@ -62,7 +67,7 @@ export function initTray(windowManager: WindowManager) { const disposers = [ autorun(() => { try { - const menu = createTrayMenu(windowManager); + const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get())); tray.setContextMenu(menu); } catch (error) { @@ -78,8 +83,21 @@ export function initTray(windowManager: WindowManager) { }; } -function createTrayMenu(windowManager: WindowManager): Menu { - const template: Electron.MenuItemConstructorOptions[] = [ +function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions { + return { + ...trayItem, + submenu: trayItem.submenu ? trayItem.submenu.map(getMenuItemConstructorOptions) : undefined, + click: trayItem.click ? () => { + trayItem.click(trayItem); + } : undefined, + }; +} + +function createTrayMenu( + windowManager: WindowManager, + extensionTrayItems: TrayMenuRegistration[], +): Menu { + let template: Electron.MenuItemConstructorOptions[] = [ { label: `Open ${productName}`, click() { @@ -108,6 +126,8 @@ function createTrayMenu(windowManager: WindowManager): Menu { }); } + template = template.concat(extensionTrayItems.map(getMenuItemConstructorOptions)); + return Menu.buildFromTemplate(template.concat([ { label: `About ${productName}`, From 02056a6090944caabb8154ba891f84a9a653bd42 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Tue, 4 Jan 2022 15:43:45 +0200 Subject: [PATCH 09/10] Fix prometheus operator metrics work out of the box (#4617) Signed-off-by: Lauri Nevala --- src/main/prometheus/operator.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/prometheus/operator.ts b/src/main/prometheus/operator.ts index 83638e4ec1..140b432baf 100644 --- a/src/main/prometheus/operator.ts +++ b/src/main/prometheus/operator.ts @@ -38,7 +38,7 @@ export class PrometheusOperator extends PrometheusProvider { case "cluster": switch (queryName) { case "memoryUsage": - return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`); + return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`); case "workloadMemoryUsage": return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`; case "memoryRequests": @@ -50,7 +50,7 @@ export class PrometheusOperator extends PrometheusProvider { case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`; + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`; case "cpuRequests": return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`; case "cpuLimits": @@ -66,31 +66,31 @@ export class PrometheusOperator extends PrometheusProvider { case "podAllocatableCapacity": return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`; case "fsSize": - return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`; + return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`; case "fsUsage": - return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`; + return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`; } break; case "nodes": switch (queryName) { case "memoryUsage": - return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`; + return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`; case "workloadMemoryUsage": - return `sum(container_memory_working_set_bytes{container!=""}) by (node)`; + return `sum(container_memory_working_set_bytes{container!="POD", container!=""}) by (node)`; case "memoryCapacity": return `sum(kube_node_status_capacity{resource="memory"}) by (node)`; case "memoryAllocatableCapacity": return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`; case "cpuUsage": - return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`; + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`; case "cpuCapacity": return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; case "cpuAllocatableCapacity": return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; case "fsSize": - return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`; + return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`; case "fsUsage": - return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`; + return `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`; } break; case "pods": From 2a31c5a0d5c65f7b46a94ec6d5c016556a5dbbe9 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 4 Jan 2022 10:20:41 -0500 Subject: [PATCH 10/10] Removing JS adding #terminal-init so that unit tests don't have global state (#4627) --- src/renderer/components/app.scss | 9 +++++++++ src/renderer/components/dock/terminal.ts | 14 +++----------- src/renderer/template.html | 1 + 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/renderer/components/app.scss b/src/renderer/components/app.scss index 0482dd0dd8..40ab38ffdb 100755 --- a/src/renderer/components/app.scss +++ b/src/renderer/components/app.scss @@ -91,6 +91,15 @@ html, body { overflow: hidden; } +#terminal-init { + position: absolute; + top: 0; + left: 0; + height: 0; + visibility: hidden; + overflow: hidden; +} + #app { height: 100%; min-height: 100%; diff --git a/src/renderer/components/dock/terminal.ts b/src/renderer/components/dock/terminal.ts index e88bfc067e..383accab20 100644 --- a/src/renderer/components/dock/terminal.ts +++ b/src/renderer/components/dock/terminal.ts @@ -34,17 +34,9 @@ import { clipboard } from "electron"; import logger from "../../../common/logger"; export class Terminal { - public static readonly spawningPool = (() => { - // terminal element must be in DOM before attaching via xterm.open(elem) - // https://xtermjs.org/docs/api/terminal/classes/terminal/#open - const pool = document.createElement("div"); - - pool.className = "terminal-init"; - pool.style.cssText = "position: absolute; top: 0; left: 0; height: 0; visibility: hidden; overflow: hidden"; - document.body.appendChild(pool); - - return pool; - })(); + public static get spawningPool() { + return document.getElementById("terminal-init"); + } static async preloadFonts() { const fontPath = require("../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires diff --git a/src/renderer/template.html b/src/renderer/template.html index c7df1ee507..fd4d35c5a6 100755 --- a/src/renderer/template.html +++ b/src/renderer/template.html @@ -6,6 +6,7 @@
    +