From eb6cc70143c3e061b4901689805e61b21aa7a805 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Wed, 6 Jul 2022 15:51:59 +0300 Subject: [PATCH] Expose a way to reactively control visibility of preference tabs and tray menu items through Extension API (#5789) --- .../order-of-sidebar-items.test.tsx.snap | 14 +- ...-and-tab-navigation-for-core.test.tsx.snap | 54 +- ...ab-navigation-for-extensions.test.tsx.snap | 16 +- .../visibility-of-sidebar-items.test.tsx.snap | 14 +- ...and-tab-navigation-for-extensions.test.tsx | 174 +++-- .../navigation-to-helm-charts.test.ts.snap | 8 +- ...nsion-adding-preference-tabs.test.tsx.snap | 665 ++++++++++++++++++ .../extension-adding-preference-tabs.test.tsx | 95 +++ .../tray/extension-adding-tray-items.test.tsx | 150 ++++ .../registries/page-menu-registry.ts | 2 + .../tray-menu-item-registrator.injectable.ts | 24 +- src/main/tray/tray-menu-registration.ts | 5 +- .../+catalog/catalog-entity-drawer-menu.tsx | 1 + src/renderer/components/+catalog/catalog.tsx | 2 +- .../+extensions/installed-extensions.tsx | 5 +- .../+helm-releases/release-menu.tsx | 1 + .../port-forward-menu.tsx | 9 +- .../app-preference-tab-registration.ts | 3 + ...-preferences-navigation-item.injectable.ts | 10 +- .../components/cluster-icon-settings.tsx | 1 + src/renderer/components/dock/dock.tsx | 1 + .../components/item-object-list/content.tsx | 1 + .../kube-object-menu.test.tsx.snap | 7 + .../kube-object-menu/kube-object-menu.tsx | 1 + ...on-sidebar-item-registrator.injectable.tsx | 2 + src/renderer/components/menu/menu-actions.tsx | 15 +- .../test-utils/get-application-builder.tsx | 32 +- .../test-utils/get-extension-fake.ts | 103 +++ .../test-utils/get-renderer-extension-fake.ts | 52 +- .../__snapshots__/cluster-frame.test.tsx.snap | 22 +- 30 files changed, 1295 insertions(+), 194 deletions(-) create mode 100644 src/behaviours/preferences/__snapshots__/extension-adding-preference-tabs.test.tsx.snap create mode 100644 src/behaviours/preferences/extension-adding-preference-tabs.test.tsx create mode 100644 src/behaviours/tray/extension-adding-tray-items.test.tsx create mode 100644 src/renderer/components/test-utils/get-extension-fake.ts diff --git a/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap b/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap index e1db29fca1..1d2b889363 100644 --- a/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap +++ b/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap @@ -524,7 +524,7 @@ exports[`cluster - order of sidebar items when rendered renders 1`] = ` > @@ -1028,7 +1028,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > @@ -1604,7 +1604,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > @@ -2103,7 +2103,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > @@ -2602,7 +2602,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > @@ -3060,7 +3060,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > @@ -3636,7 +3636,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > @@ -4172,7 +4172,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > diff --git a/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap b/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap index 48ce69934a..cd7e67604d 100644 --- a/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap +++ b/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap @@ -463,7 +463,7 @@ exports[`cluster - visibility of sidebar items given kube resource for route is > ({ @@ -50,11 +51,90 @@ describe("cluster - sidebar and tab navigation for extensions", () => { }); describe("given extension with cluster pages and cluster page menus", () => { - beforeEach(() => { - const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder); - const testExtension = getRendererExtensionFake(extensionStubWithSidebarItems); + let someObservable: IObservableValue; - applicationBuilder.extensions.renderer.enable(testExtension); + beforeEach(() => { + someObservable = observable.box(false); + + const getExtensionFake = getExtensionFakeFor(applicationBuilder); + + const testExtension = getExtensionFake({ + id: "some-extension-id", + name: "some-extension-name", + + rendererOptions: { + clusterPages: [ + { + components: { + Page: () => { + throw new Error("should never come here"); + }, + }, + }, + { + id: "some-child-page-id", + + components: { + Page: () =>
Some child page
, + }, + }, + { + id: "some-other-child-page-id", + + components: { + Page: () => ( +
Some other child page
+ ), + }, + }, + ], + + clusterPageMenus: [ + { + id: "some-parent-id", + title: "Parent", + + components: { + Icon: () =>
Some icon
, + }, + }, + + { + id: "some-child-id", + target: { pageId: "some-child-page-id" }, + parentId: "some-parent-id", + title: "Child 1", + + components: { + Icon: null as never, + }, + }, + + { + id: "some-other-child-id", + target: { pageId: "some-other-child-page-id" }, + parentId: "some-parent-id", + title: "Child 2", + + components: { + Icon: null as never, + }, + }, + + { + id: "some-menu-with-controlled-visibility", + title: "Some menu with controlled visibility", + visible: computed(() => someObservable.get()), + + components: { + Icon: () =>
Some icon
, + }, + }, + ], + }, + }); + + applicationBuilder.extensions.enable(testExtension); }); describe("given no state for expanded sidebar items exists, and navigated to child sidebar item, when rendered", () => { @@ -212,6 +292,26 @@ describe("cluster - sidebar and tab navigation for extensions", () => { expect(child).toBeNull(); }); + it("does not show the sidebar item that should be hidden", () => { + const child = rendered.queryByTestId( + "sidebar-item-some-extension-name-some-menu-with-controlled-visibility", + ); + + expect(child).not.toBeInTheDocument(); + }); + + it("when sidebar item becomes visible, shows the sidebar item", () => { + runInAction(() => { + someObservable.set(true); + }); + + const child = rendered.queryByTestId( + "sidebar-item-some-extension-name-some-menu-with-controlled-visibility", + ); + + expect(child).toBeInTheDocument(); + }); + describe("when a parent sidebar item is expanded", () => { beforeEach(() => { const parentLink = rendered.getByTestId( @@ -355,65 +455,3 @@ describe("cluster - sidebar and tab navigation for extensions", () => { }); }); }); - -const extensionStubWithSidebarItems: FakeExtensionData = { - id: "some-extension-id", - name: "some-extension-name", - clusterPages: [ - { - components: { - Page: () => { - throw new Error("should never come here"); - }, - }, - }, - { - id: "some-child-page-id", - - components: { - Page: () =>
Some child page
, - }, - }, - { - id: "some-other-child-page-id", - - components: { - Page: () => ( -
Some other child page
- ), - }, - }, - ], - clusterPageMenus: [ - { - id: "some-parent-id", - title: "Parent", - - components: { - Icon: () =>
Some icon
, - }, - }, - - { - id: "some-child-id", - target: { pageId: "some-child-page-id" }, - parentId: "some-parent-id", - title: "Child 1", - - components: { - Icon: null as never, - }, - }, - - { - id: "some-other-child-id", - target: { pageId: "some-other-child-page-id" }, - parentId: "some-parent-id", - title: "Child 2", - - components: { - Icon: null as never, - }, - }, - ], -}; diff --git a/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap b/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap index 8fbc3d9a49..43fdf6fc54 100644 --- a/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap +++ b/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap @@ -433,7 +433,7 @@ exports[`helm-charts - navigation to Helm charts when navigating to Helm charts > +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+
+
+ +
+
+
+

+ Application +

+
+
+ Theme + +
+
+ + +
+
+
+ Select... +
+
+ +
+
+
+ + +
+
+
+
+
+
+
+ Extension Install Registry + +
+
+ + +
+
+
+ Select... +
+
+ +
+
+
+ + +
+
+
+

+ This setting is to change the registry URL for installing extensions by name. + If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your + + .npmrc + + file or in the input below. +

+
+ +
+
+
+
+
+
+ Start-up + +
+ +
+
+
+
+ Update Channel + +
+
+ + +
+
+
+ Stable +
+
+ +
+
+
+ + +
+
+
+
+
+
+
+ Locale Timezone + +
+
+ + +
+
+
+ Select... +
+
+ +
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ + + close + + +
+ +
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; diff --git a/src/behaviours/preferences/extension-adding-preference-tabs.test.tsx b/src/behaviours/preferences/extension-adding-preference-tabs.test.tsx new file mode 100644 index 0000000000..b811a5a848 --- /dev/null +++ b/src/behaviours/preferences/extension-adding-preference-tabs.test.tsx @@ -0,0 +1,95 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { RenderResult } from "@testing-library/react"; +import type { IObservableValue } from "mobx"; +import { runInAction, computed, observable } from "mobx"; +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getExtensionFakeFor } from "../../renderer/components/test-utils/get-extension-fake"; + +describe("preferences: extension adding preference tabs", () => { + let builder: ApplicationBuilder; + + beforeEach(() => { + builder = getApplicationBuilder(); + }); + + describe("given in preferences, when extension with preference tabs is enabled", () => { + let rendered: RenderResult; + let someObservable: IObservableValue; + + beforeEach(async () => { + rendered = await builder.render(); + + builder.preferences.navigate(); + + const getExtensionFake = getExtensionFakeFor(builder); + + someObservable = observable.box(false); + + const testExtension = getExtensionFake({ + id: "some-extension-id", + name: "some-extension", + + rendererOptions: { + appPreferenceTabs: [ + { + title: "Some title", + id: "some-preference-tab-id", + orderNumber: 2, + }, + { + title: "Some other title", + id: "some-other-preference-tab-id", + orderNumber: 1, + }, + { + title: "Some title for item with controlled visibility", + id: "some-preference-tab-id-with-controlled-visibility", + orderNumber: 3, + visible: computed(() => someObservable.get()), + }, + ], + }, + }); + + builder.extensions.enable(testExtension); + + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shows tabs in order", () => { + const actual = rendered.queryAllByTestId(/tab-link-for-extension-some-extension-nav-item-(.*)/).map(x => x.dataset.testid); + + expect(actual).toEqual([ + "tab-link-for-extension-some-extension-nav-item-some-other-preference-tab-id", + "tab-link-for-extension-some-extension-nav-item-some-preference-tab-id", + ]); + }); + + it("does not show hidden tab", () => { + const actual = rendered.queryByTestId( + "tab-link-for-extension-some-extension-nav-item-some-preference-tab-id-with-controlled-visibility", + ); + + expect(actual).not.toBeInTheDocument(); + }); + + it("when item becomes visible, shows the tab", () => { + runInAction(() => { + someObservable.set(true); + }); + + const actual = rendered.queryByTestId( + "tab-link-for-extension-some-extension-nav-item-some-preference-tab-id-with-controlled-visibility", + ); + + expect(actual).toBeInTheDocument(); + }); + }); +}); diff --git a/src/behaviours/tray/extension-adding-tray-items.test.tsx b/src/behaviours/tray/extension-adding-tray-items.test.tsx new file mode 100644 index 0000000000..35e09e0f76 --- /dev/null +++ b/src/behaviours/tray/extension-adding-tray-items.test.tsx @@ -0,0 +1,150 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { IObservableValue } from "mobx"; +import { computed, runInAction, observable } from "mobx"; +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getExtensionFakeFor } from "../../renderer/components/test-utils/get-extension-fake"; + +describe("preferences: extension adding tray items", () => { + describe("when extension with tray items is enabled", () => { + let builder: ApplicationBuilder; + let someObservableForVisibility: IObservableValue; + let someObservableForEnabled: IObservableValue; + + beforeEach(async () => { + builder = getApplicationBuilder(); + + await builder.render(); + + builder.preferences.navigate(); + + const getExtensionFake = getExtensionFakeFor(builder); + + someObservableForVisibility = observable.box(false); + someObservableForEnabled = observable.box(false); + + const testExtension = getExtensionFake({ + id: "some-extension-id", + name: "some-extension", + + mainOptions: { + trayMenus: [ + { + label: "some-controlled-visibility", + click: () => {}, + visible: computed(() => someObservableForVisibility.get()), + }, + + { + label: "some-uncontrolled-visibility", + click: () => {}, + }, + + { + label: "some-controlled-enabled", + click: () => {}, + enabled: computed(() => someObservableForEnabled.get()), + }, + + { + label: "some-uncontrolled-enabled", + click: () => {}, + }, + + { + label: "some-statically-enabled", + click: () => {}, + enabled: true, + }, + + { + label: "some-statically-disabled", + click: () => {}, + enabled: false, + }, + ], + }, + }); + + builder.extensions.enable(testExtension); + }); + + it("shows item which doesn't control the visibility", () => { + expect( + builder.tray.get( + "some-uncontrolled-visibility-tray-menu-item-for-extension-some-extension", + ), + ).not.toBeNull(); + }); + + it("does not show hidden item", () => { + expect( + builder.tray.get( + "some-controlled-visibility-tray-menu-item-for-extension-some-extension", + ), + ).toBeNull(); + }); + + it("when item becomes visible, shows the item", () => { + runInAction(() => { + someObservableForVisibility.set(true); + }); + + expect( + builder.tray.get( + "some-controlled-visibility-tray-menu-item-for-extension-some-extension", + ), + ).not.toBeNull(); + }); + + + it("given item does not have enabled status, item is enabled by default", () => { + const item = builder.tray.get( + "some-uncontrolled-enabled-tray-menu-item-for-extension-some-extension", + ); + + expect(item?.enabled).toBe(true); + }); + + describe("given item has controlled enabled status and is disabled", () => { + it("is disabled", () => { + const item = builder.tray.get( + "some-controlled-enabled-tray-menu-item-for-extension-some-extension", + ); + + expect(item?.enabled).toBe(false); + }); + + it("when item becomes enabled, items is enabled", () => { + runInAction(() => { + someObservableForEnabled.set(true); + }); + + const item = builder.tray.get( + "some-controlled-enabled-tray-menu-item-for-extension-some-extension", + ); + + expect(item?.enabled).toBe(true); + }); + }); + + it("given item is statically enabled, item is enabled", () => { + const item = builder.tray.get( + "some-statically-enabled-tray-menu-item-for-extension-some-extension", + ); + + expect(item?.enabled).toBe(true); + }); + + it("given item is statically disabled, item is disabled", () => { + const item = builder.tray.get( + "some-statically-disabled-tray-menu-item-for-extension-some-extension", + ); + + expect(item?.enabled).toBe(false); + }); + }); +}); diff --git a/src/extensions/registries/page-menu-registry.ts b/src/extensions/registries/page-menu-registry.ts index d69bcfadfa..d083d729c4 100644 --- a/src/extensions/registries/page-menu-registry.ts +++ b/src/extensions/registries/page-menu-registry.ts @@ -7,6 +7,7 @@ import type { IconProps } from "../../renderer/components/icon"; import type React from "react"; import type { PageTarget } from "./page-registry"; +import type { IComputedValue } from "mobx"; export interface ClusterPageMenuRegistration { id?: string; @@ -14,6 +15,7 @@ export interface ClusterPageMenuRegistration { target?: PageTarget; title: React.ReactNode; components: ClusterPageMenuComponents; + visible?: IComputedValue; } export interface ClusterPageMenuComponents { diff --git a/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts b/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts index 152c4ea8d1..136f869b8f 100644 --- a/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts +++ b/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts @@ -16,6 +16,7 @@ import { withErrorSuppression } from "../../../common/utils/with-error-suppressi import type { WithErrorLoggingFor } from "../../../common/utils/with-error-logging/with-error-logging.injectable"; import withErrorLoggingInjectable from "../../../common/utils/with-error-logging/with-error-logging.injectable"; import getRandomIdInjectable from "../../../common/utils/get-random-id.injectable"; +import { isBoolean } from "../../../common/utils"; const trayMenuItemRegistratorInjectable = getInjectable({ id: "tray-menu-item-registrator", @@ -64,14 +65,31 @@ const toItemInjectablesFor = (extension: LensMainExtension, withErrorLoggingFor: // TODO: Find out how to improve typing so that instead of // x => withErrorSuppression(x) there could only be withErrorSuppression - x => withErrorSuppression(x), + (x) => withErrorSuppression(x), ); return decorated(registration); }, - enabled: computed(() => registration.enabled ?? true), - visible: computed(() => true), + enabled: computed(() => { + if (registration.enabled === undefined) { + return true; + } + + if (isBoolean(registration.enabled)) { + return registration.enabled; + } + + return registration.enabled.get(); + }), + + visible: computed(() => { + if (!registration.visible) { + return true; + } + + return registration.visible.get(); + }), }), injectionToken: trayMenuItemInjectionToken, diff --git a/src/main/tray/tray-menu-registration.ts b/src/main/tray/tray-menu-registration.ts index dcd7796c51..c192dd89fb 100644 --- a/src/main/tray/tray-menu-registration.ts +++ b/src/main/tray/tray-menu-registration.ts @@ -3,12 +3,15 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +import type { IComputedValue } from "mobx"; + export interface TrayMenuRegistration { label?: string; click?: (menuItem: TrayMenuRegistration) => void; id?: string; type?: "normal" | "separator" | "submenu"; toolTip?: string; - enabled?: boolean; + enabled?: boolean | IComputedValue; submenu?: TrayMenuRegistration[]; + visible?: IComputedValue; } diff --git a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx index 1cf39c4809..bc982379bc 100644 --- a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx +++ b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx @@ -95,6 +95,7 @@ class NonInjectedCatalogEntityDrawerMenu extends React. return ( { }; return ( - + this.props.catalogEntityStore.selectedItemId.set(entity.getId())} diff --git a/src/renderer/components/+extensions/installed-extensions.tsx b/src/renderer/components/+extensions/installed-extensions.tsx index 2de88642e2..e69c7c7205 100644 --- a/src/renderer/components/+extensions/installed-extensions.tsx +++ b/src/renderer/components/+extensions/installed-extensions.tsx @@ -102,7 +102,10 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
), actions: ( - + {isCompatible && ( <> {isEnabled ? ( diff --git a/src/renderer/components/+helm-releases/release-menu.tsx b/src/renderer/components/+helm-releases/release-menu.tsx index 511b104600..c0366624c8 100644 --- a/src/renderer/components/+helm-releases/release-menu.tsx +++ b/src/renderer/components/+helm-releases/release-menu.tsx @@ -78,6 +78,7 @@ class NonInjectedHelmReleaseMenu extends React.Component Stop @@ -79,7 +79,7 @@ class NonInjectedPortForwardMenu Start @@ -98,7 +98,7 @@ class NonInjectedPortForwardMenu Open @@ -107,7 +107,7 @@ class NonInjectedPortForwardMenu Edit @@ -121,6 +121,7 @@ class NonInjectedPortForwardMenu; } diff --git a/src/renderer/components/+preferences/preferences-navigation/extension-tab-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/extension-tab-preferences-navigation-item.injectable.ts index 8cd0c4895b..3fb2062ecf 100644 --- a/src/renderer/components/+preferences/preferences-navigation/extension-tab-preferences-navigation-item.injectable.ts +++ b/src/renderer/components/+preferences/preferences-navigation/extension-tab-preferences-navigation-item.injectable.ts @@ -46,7 +46,15 @@ const extensionSpecificTabNavigationItemRegistratorInjectable = getInjectable({ parent: "general", orderNumber: tab.orderNumber || 100, navigate: () => navigateToExtensionPreferences(extension.sanitizedExtensionId, tab.id), - isVisible: computed(() => true), + + isVisible: computed(() => { + if (!tab.visible) { + return true; + } + + return tab.visible.get(); + }), + isActive, }), }); diff --git a/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx b/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx index 6f0492ea0b..6807ff227b 100644 --- a/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx @@ -89,6 +89,7 @@ export class ClusterIconSetting extends React.Component />
{