Expose a way to reactively control visibility of preference tabs and tray menu items through Extension API (#5789)

This commit is contained in:
Janne Savolainen authored and GitHub committed 2022-07-06 08:51:59 -04:00
1 parent 544bbdc952
commit eb6cc70143
30 files changed
+1295 -194

No files matched your search

@@ -95,6 +95,7 @@ class NonInjectedCatalogEntityDrawerMenu<T extends CatalogEntity> extends React.
return (
<MenuActions
id="menu-actions-for-catalog-entity-drawer-menu"
className={cssNames("CatalogEntityDrawerMenu", className)}
toolbar
{...menuProps}
+1 -1
View File
@@ -198,7 +198,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
};
return (
<MenuActions onOpen={onOpen}>
<MenuActions id="menu-actions-for-catalog" onOpen={onOpen}>
<MenuItem
key="open-details"
onClick={() => this.props.catalogEntityStore.selectedItemId.set(entity.getId())}
@@ -102,7 +102,10 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
</div>
),
actions: (
<MenuActions usePortal toolbar={false}>
<MenuActions
id="menu-actions-for-installed-extensions"
usePortal
toolbar={false}>
{isCompatible && (
<>
{isEnabled ? (
@@ -78,6 +78,7 @@ class NonInjectedHelmReleaseMenu extends React.Component<HelmReleaseMenuProps &
return (
<MenuActions
id="menu-actions-for-release-menu"
{...menuProps}
className={cssNames("HelmReleaseMenu", className)}
removeAction={this.remove}
@@ -67,7 +67,7 @@ class NonInjectedPortForwardMenu<Props extends PortForwardMenuProps & Dependenci
<Icon
material="stop"
tooltip="Stop port-forward"
interactive={toolbar}
interactive={toolbar}
/>
<span className="title">Stop</span>
</MenuItem>
@@ -79,7 +79,7 @@ class NonInjectedPortForwardMenu<Props extends PortForwardMenuProps & Dependenci
<Icon
material="play_arrow"
tooltip="Start port-forward"
interactive={toolbar}
interactive={toolbar}
/>
<span className="title">Start</span>
</MenuItem>
@@ -98,7 +98,7 @@ class NonInjectedPortForwardMenu<Props extends PortForwardMenuProps & Dependenci
<Icon
material="open_in_browser"
interactive={toolbar}
tooltip="Open in browser"
tooltip="Open in browser"
/>
<span className="title">Open</span>
</MenuItem>
@@ -107,7 +107,7 @@ class NonInjectedPortForwardMenu<Props extends PortForwardMenuProps & Dependenci
<Icon
material="edit"
tooltip="Change port or protocol"
interactive={toolbar}
interactive={toolbar}
/>
<span className="title">Edit</span>
</MenuItem>
@@ -121,6 +121,7 @@ class NonInjectedPortForwardMenu<Props extends PortForwardMenuProps & Dependenci
return (
<MenuActions
id="menu-actions-for-port-forward-menu"
{...menuProps}
className={cssNames("PortForwardMenu", className)}
removeAction={this.remove}
@@ -3,9 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IComputedValue } from "mobx";
export interface AppPreferenceTabRegistration {
title: string;
id: string;
orderNumber?: number;
visible?: IComputedValue<boolean>;
}
@@ -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,
}),
});
@@ -89,6 +89,7 @@ export class ClusterIconSetting extends React.Component<ClusterIconSettingProps>
/>
</div>
<MenuActions
id="menu-actions-for-cluster-icon-settings"
toolbar={false}
autoCloseOnSelect={true}
triggerIcon={{ material: "more_horiz" }}
+1
View File
@@ -161,6 +161,7 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
<div className={cssNames("toolbar flex gaps align-center box grow", { "pl-0": tabs.length == 0 })}>
<div className="dock-menu box grow">
<MenuActions
id="menu-actions-for-dock"
usePortal
triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: "New tab" }}
closeOnScroll={false}
@@ -343,6 +343,7 @@ class NonInjectedItemListLayoutContent<
return (
<MenuActions
id="menu-actions-for-item-object-list-content"
className="ItemListLayoutVisibilityMenu"
toolbar={false}
autoCloseOnSelect={false}
@@ -6,6 +6,7 @@ exports[`kube-object-menu given kube object renders 1`] = `
<div>
<ul
class="Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
id="menu-actions-for-kube-object-menu-for-some-uid"
>
<li>
Some menu item
@@ -45,6 +46,7 @@ exports[`kube-object-menu given kube object when removing kube object renders 1`
<div>
<ul
class="Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
id="menu-actions-for-kube-object-menu-for-some-uid"
>
<li>
Some menu item
@@ -136,6 +138,7 @@ exports[`kube-object-menu given kube object when rerendered with different kube
<div>
<ul
class="Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
id="menu-actions-for-kube-object-menu-for-some-other-uid"
>
<li
class="MenuItem"
@@ -172,6 +175,7 @@ exports[`kube-object-menu given kube object when rerendered with different kube
<div>
<ul
class="Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
id="menu-actions-for-kube-object-menu-for-some-other-uid"
>
<li
class="MenuItem"
@@ -260,6 +264,7 @@ exports[`kube-object-menu given kube object with namespace when removing kube ob
<div>
<ul
class="Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
id="menu-actions-for-kube-object-menu-for-some-uid"
>
<li>
Some menu item
@@ -351,6 +356,7 @@ exports[`kube-object-menu given kube object without namespace when removing kube
<div>
<ul
class="Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
id="menu-actions-for-kube-object-menu-for-some-uid"
>
<li>
Some menu item
@@ -441,6 +447,7 @@ exports[`kube-object-menu given no kube object, renders 1`] = `
<div>
<ul
class="Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
id="menu-actions-for-kube-object-menu-for-undefined"
/>
</div>
</body>
@@ -187,6 +187,7 @@ class NonInjectedKubeObjectMenu<Kube extends KubeObject> extends React.Component
return (
<MenuActions
id={`menu-actions-for-kube-object-menu-for-${object?.getId()}`}
className={cssNames("KubeObjectMenu", className)}
onOpen={object ? () => this.emitOnContextMenuOpen(object) : undefined}
{...menuProps}
@@ -49,6 +49,8 @@ const extensionSidebarItemRegistratorInjectable = getInjectable({
? `${extension.sanitizedExtensionId}-${registration.parentId}`
: null,
...(registration.visible ? { isVisible: registration.visible } : {}),
title: registration.title,
getIcon: registration.components.Icon
? () => <registration.components.Icon />
@@ -13,12 +13,12 @@ import type { IconProps } from "../icon";
import { Icon } from "../icon";
import type { MenuProps } from "./menu";
import { Menu, MenuItem } from "./menu";
import uniqueId from "lodash/uniqueId";
import isString from "lodash/isString";
import type { TooltipDecoratorProps } from "../tooltip";
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import openConfirmDialogInjectable from "../confirm-dialog/open.injectable";
import getRandomIdInjectable from "../../../common/utils/get-random-id.injectable";
export interface MenuActionsProps extends Partial<MenuProps> {
className?: string;
@@ -38,6 +38,7 @@ export interface MenuActionsProps extends Partial<MenuProps> {
*/
removeAction?: () => void | Promise<void>;
onOpen?: () => void;
id?: string;
}
interface Dependencies {
@@ -50,9 +51,6 @@ class NonInjectedMenuActions extends React.Component<MenuActionsProps & Dependen
removeConfirmationMessage: "Remove item?",
};
// TODO: Make deterministic
public id = uniqueId("menu_actions_");
@observable isOpen = !!this.props.toolbar;
toggle = () => {
@@ -100,10 +98,10 @@ class NonInjectedMenuActions extends React.Component<MenuActionsProps & Dependen
if (isValidElement<HTMLElement>(triggerIcon)) {
className = cssNames(triggerIcon.props.className, { active: this.isOpen });
return React.cloneElement(triggerIcon, { id: this.id, className });
return React.cloneElement(triggerIcon, { id: this.props.id, className });
}
const iconProps: IconProps & TooltipDecoratorProps = {
id: this.id,
id: this.props.id,
interactive: true,
material: isString(triggerIcon) ? triggerIcon : undefined,
active: this.isOpen,
@@ -131,7 +129,7 @@ class NonInjectedMenuActions extends React.Component<MenuActionsProps & Dependen
{this.renderTriggerIcon()}
<Menu
htmlFor={this.id}
htmlFor={this.props.id}
isOpen={this.isOpen}
open={this.toggle}
close={this.toggle}
@@ -175,7 +173,8 @@ class NonInjectedMenuActions extends React.Component<MenuActionsProps & Dependen
export const MenuActions = withInjectables<Dependencies, MenuActionsProps>(NonInjectedMenuActions, {
getProps: (di, props) => ({
...props,
id: di.inject(getRandomIdInjectable)(),
openConfirmDialog: di.inject(openConfirmDialogInjectable),
...props,
}),
});
@@ -78,6 +78,9 @@ export interface ApplicationBuilder {
enable: EnableExtensions<LensMainExtension>;
disable: DisableExtensions<LensMainExtension>;
};
enable: (...extensions: { renderer: LensRendererExtension; main: LensMainExtension }[]) => void;
disable: (...extensions: { renderer: LensRendererExtension; main: LensMainExtension }[]) => void;
};
allowKubeResource: (resourceName: KubeResource) => ApplicationBuilder;
@@ -234,6 +237,11 @@ export const getApplicationBuilder = () => {
};
};
const enableRendererExtension = enableExtensionsFor(rendererExtensionsState, rendererDi);
const enableMainExtension = enableExtensionsFor(mainExtensionsState, mainDi);
const disableRendererExtension = disableExtensionsFor(rendererExtensionsState, rendererDi);
const disableMainExtension = disableExtensionsFor(mainExtensionsState, mainDi);
const builder: ApplicationBuilder = {
dis,
@@ -400,13 +408,29 @@ export const getApplicationBuilder = () => {
extensions: {
renderer: {
enable: enableExtensionsFor(rendererExtensionsState, rendererDi),
disable: disableExtensionsFor(rendererExtensionsState, rendererDi),
enable: enableRendererExtension,
disable: disableRendererExtension,
},
main: {
enable: enableExtensionsFor(mainExtensionsState, mainDi),
disable: disableExtensionsFor(mainExtensionsState, mainDi),
enable: enableMainExtension,
disable: disableMainExtension,
},
enable: (...extensions) => {
const rendererExtensions = extensions.map(extension => extension.renderer);
const mainExtensions = extensions.map(extension => extension.main);
enableRendererExtension(...rendererExtensions);
enableMainExtension(...mainExtensions);
},
disable: (...extensions) => {
const rendererExtensions = extensions.map(extension => extension.renderer);
const mainExtensions = extensions.map(extension => extension.main);
disableRendererExtension(...rendererExtensions);
disableMainExtension(...mainExtensions);
},
},
@@ -0,0 +1,103 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Mutable } from "type-fest";
import fileSystemProvisionerStoreInjectable from "../../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable";
import { lensExtensionDependencies } from "../../../extensions/lens-extension";
import type { ApplicationBuilder } from "./get-application-builder";
import { LensMainExtension } from "../../../extensions/lens-main-extension";
import navigateForExtensionInjectable from "../../../main/start-main-application/lens-window/navigate-for-extension.injectable";
import { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
import getExtensionPageParametersInjectable from "../../routes/get-extension-page-parameters.injectable";
import navigateToRouteInjectable from "../../routes/navigate-to-route.injectable";
import routesInjectable from "../../routes/routes.injectable";
import catalogEntityRegistryForMainInjectable from "../../../main/catalog/entity-registry.injectable";
import catalogEntityRegistryForRendererInjectable from "../../api/catalog/entity/registry.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
export class TestExtensionMain extends LensMainExtension {}
export class TestExtensionRenderer extends LensRendererExtension {}
export interface FakeExtensionOptions {
id: string;
name: string;
rendererOptions?: Partial<LensRendererExtension>;
mainOptions?: Partial<LensMainExtension>;
}
export type GetExtensionFake = (arg: FakeExtensionOptions) => {
main: TestExtensionMain;
renderer: TestExtensionRenderer;
};
export const getExtensionFakeFor =
(builder: ApplicationBuilder): GetExtensionFake =>
({ id, name, mainOptions = {}, rendererOptions = {}}) => {
const mainInstance = getExtensionFakeForMain(builder.dis.mainDi, id, name, mainOptions);
const rendererInstance = getExtensionFakeForRenderer(builder.dis.rendererDi, id, name, rendererOptions);
return { main: mainInstance, renderer: rendererInstance };
};
const getExtensionFakeForMain = (di: DiContainer, id: string, name: string, options: Partial<LensMainExtension>) => {
const instance = new TestExtensionMain({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: {
name,
version: "1.0.0",
engines: {
lens: "^5.5.0",
},
},
manifestPath: "irrelevant",
});
Object.assign(instance, options);
(instance as Mutable<LensMainExtension>)[lensExtensionDependencies] = {
fileSystemProvisionerStore: di.inject(
fileSystemProvisionerStoreInjectable,
),
entityRegistry: di.inject(catalogEntityRegistryForMainInjectable),
navigate: di.inject(navigateForExtensionInjectable),
};
return instance;
};
const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: string, options: Partial<LensRendererExtension>) => {
const instance = new TestExtensionRenderer({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: {
name,
version: "1.0.0",
engines: {
lens: "^5.5.0",
},
},
manifestPath: "irrelevant",
});
Object.assign(instance, options);
(instance as Mutable<LensRendererExtension>)[lensExtensionDependencies] = {
categoryRegistry: di.inject(catalogCategoryRegistryInjectable),
entityRegistry: di.inject(catalogEntityRegistryForRendererInjectable),
fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable),
getExtensionPageParameters: di.inject(getExtensionPageParametersInjectable),
navigateToRoute: di.inject(navigateToRouteInjectable),
routes: di.inject(routesInjectable),
};
return instance;
};
@@ -2,16 +2,10 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Mutable, SetRequired } from "type-fest";
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
import fileSystemProvisionerStoreInjectable from "../../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable";
import { lensExtensionDependencies } from "../../../extensions/lens-extension";
import type { SetRequired } from "type-fest";
import { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable";
import getExtensionPageParametersInjectable from "../../routes/get-extension-page-parameters.injectable";
import navigateToRouteInjectable from "../../routes/navigate-to-route.injectable";
import routesInjectable from "../../routes/routes.injectable";
import type { ApplicationBuilder } from "./get-application-builder";
import { getExtensionFakeFor } from "./get-extension-fake";
export class TestExtension extends LensRendererExtension {}
@@ -19,36 +13,16 @@ export type FakeExtensionData = SetRequired<Partial<LensRendererExtension>, "id"
export type GetRendererExtensionFake = (fakeExtensionData: FakeExtensionData) => TestExtension;
export const getRendererExtensionFakeFor = (builder: ApplicationBuilder): GetRendererExtensionFake => (
function getRendererExtensionFake({ id, name, ...rest }) {
const instance = new TestExtension({
export const getRendererExtensionFakeFor = (
builder: ApplicationBuilder,
): GetRendererExtensionFake => {
const getExtensionFake = getExtensionFakeFor(builder);
return ({ id, name, ...rest }) =>
getExtensionFake({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: {
name,
version: "1.0.0",
engines: {
lens: "^5.5.0",
},
},
manifestPath: "irrelevant",
});
Object.assign(instance, rest);
(instance as Mutable<LensRendererExtension>)[lensExtensionDependencies] = {
categoryRegistry: builder.dis.rendererDi.inject(catalogCategoryRegistryInjectable),
entityRegistry: builder.dis.rendererDi.inject(catalogEntityRegistryInjectable),
fileSystemProvisionerStore: builder.dis.rendererDi.inject(fileSystemProvisionerStoreInjectable),
getExtensionPageParameters: builder.dis.rendererDi.inject(getExtensionPageParametersInjectable),
navigateToRoute: builder.dis.rendererDi.inject(navigateToRouteInjectable),
routes: builder.dis.rendererDi.inject(routesInjectable),
};
return instance;
}
);
name,
rendererOptions: rest,
}).renderer;
};
@@ -402,7 +402,7 @@ exports[`<ClusterFrame /> given cluster with list nodes and namespaces permissio
>
<i
class="Icon material interactive focusable small"
id="tooltip_target_58"
id="tooltip_target_56"
tabindex="0"
>
<span
@@ -427,7 +427,7 @@ exports[`<ClusterFrame /> given cluster with list nodes and namespaces permissio
>
<i
class="Icon new-dock-tab material interactive focusable"
id="menu_actions_59"
id="menu-actions-for-dock"
tabindex="0"
>
<span
@@ -441,7 +441,7 @@ exports[`<ClusterFrame /> given cluster with list nodes and namespaces permissio
</div>
<i
class="Icon material interactive focusable"
id="tooltip_target_61"
id="tooltip_target_58"
tabindex="0"
>
<span
@@ -454,7 +454,7 @@ exports[`<ClusterFrame /> given cluster with list nodes and namespaces permissio
</i>
<i
class="Icon material interactive focusable"
id="tooltip_target_62"
id="tooltip_target_59"
tabindex="0"
>
<span
@@ -907,7 +907,7 @@ exports[`<ClusterFrame /> given cluster with list nodes and namespaces permissio
>
<i
class="Icon new-dock-tab material interactive focusable"
id="menu_actions_17"
id="menu-actions-for-dock"
tabindex="0"
>
<span
@@ -921,7 +921,7 @@ exports[`<ClusterFrame /> given cluster with list nodes and namespaces permissio
</div>
<i
class="Icon material interactive focusable"
id="tooltip_target_19"
id="tooltip_target_18"
tabindex="0"
>
<span
@@ -934,7 +934,7 @@ exports[`<ClusterFrame /> given cluster with list nodes and namespaces permissio
</i>
<i
class="Icon material interactive focusable"
id="tooltip_target_20"
id="tooltip_target_19"
tabindex="0"
>
<span
@@ -1418,7 +1418,7 @@ exports[`<ClusterFrame /> given cluster without list nodes, but with namespaces
>
<i
class="Icon material interactive focusable small"
id="tooltip_target_98"
id="tooltip_target_94"
tabindex="0"
>
<span
@@ -1443,7 +1443,7 @@ exports[`<ClusterFrame /> given cluster without list nodes, but with namespaces
>
<i
class="Icon new-dock-tab material interactive focusable"
id="menu_actions_99"
id="menu-actions-for-dock"
tabindex="0"
>
<span
@@ -1457,7 +1457,7 @@ exports[`<ClusterFrame /> given cluster without list nodes, but with namespaces
</div>
<i
class="Icon material interactive focusable"
id="tooltip_target_101"
id="tooltip_target_96"
tabindex="0"
>
<span
@@ -1470,7 +1470,7 @@ exports[`<ClusterFrame /> given cluster without list nodes, but with namespaces
</i>
<i
class="Icon material interactive focusable"
id="tooltip_target_102"
id="tooltip_target_97"
tabindex="0"
>
<span