1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Allow computed tray menu for extensions (#6598)

* Add computed tray menu for extensions.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Use disposer. Fix style.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Register/reregister injectables by id.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
Panu Horsmalahti 2022-11-17 18:03:25 +02:00 committed by GitHub
parent a91e3a7f8e
commit 1861fe2049
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 143 additions and 18 deletions

View File

@ -4,9 +4,11 @@
*/
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectionToken } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import type { LensExtension } from "../lens-extension";
export type ExtensionRegistrator = (extension: LensExtension) => Injectable<any, any, any>[];
export type ExtensionRegistrator = (extension: LensExtension) =>
Injectable<any, any, any>[] | IComputedValue<Injectable<any, any, any>[]>;
export const extensionRegistratorInjectionToken = getInjectionToken<ExtensionRegistrator>({
id: "extension-registrator-token",

View File

@ -2,8 +2,11 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { runInAction } from "mobx";
import { difference, find, map } from "lodash";
import { reaction, runInAction } from "mobx";
import { disposer } from "../../../common/utils/disposer";
import type { LensExtension } from "../../lens-extension";
import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token";
@ -12,6 +15,16 @@ export interface Extension {
deregister: () => void;
}
const idsToInjectables = (ids: string[], injectables: Injectable<any, any, any>[]) => ids.map(id => {
const injectable = find(injectables, { id });
if (!injectable) {
throw new Error(`Injectable ${id} not found`);
}
return injectable;
});
const extensionInjectable = getInjectable({
id: "extension",
@ -21,19 +34,42 @@ const extensionInjectable = getInjectable({
instantiate: (childDi) => {
const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken);
const reactionDisposer = disposer();
return {
register: () => {
const injectables = extensionRegistrators.flatMap((getInjectablesOfExtension) =>
getInjectablesOfExtension(instance),
);
extensionRegistrators.forEach((getInjectablesOfExtension) => {
const injectables = getInjectablesOfExtension(instance);
runInAction(() => {
childDi.register(...injectables);
reactionDisposer.push(
// injectables is either an array or a computed array, in which case
// we need to update the registered injectables with a reaction every time they change
reaction(
() => Array.isArray(injectables) ? injectables : injectables.get(),
(currentInjectables, previousInjectables = []) => {
// Register new injectables and deregister removed injectables by id
const currentIds = map(currentInjectables, "id");
const previousIds = map(previousInjectables, "id");
const idsToAdd = difference(currentIds, previousIds);
const idsToRemove = previousIds.filter(previousId => !currentIds.includes(previousId));
if (idsToRemove.length > 0) {
childDi.deregister(...idsToInjectables(idsToRemove, previousInjectables));
}
if (idsToAdd.length > 0) {
childDi.register(...idsToInjectables(idsToAdd, currentInjectables));
}
}, {
fireImmediately: true,
},
));
});
},
deregister: () => {
reactionDisposer();
runInAction(() => {
parentDi.deregister(extensionInjectable);
});

View File

@ -5,7 +5,7 @@
import { LensExtension, lensExtensionDependencies } from "./lens-extension";
import type { CatalogEntity } from "../common/catalog";
import type { IObservableArray } from "mobx";
import type { IComputedValue, IObservableArray } from "mobx";
import type { MenuRegistration } from "../features/application-menu/main/menu-registration";
import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration";
import type { ShellEnvModifier } from "../main/shell-session/shell-env-modifier/shell-env-modifier-registration";
@ -13,7 +13,7 @@ import type { LensMainExtensionDependencies } from "./lens-extension-set-depende
export class LensMainExtension extends LensExtension<LensMainExtensionDependencies> {
appMenus: MenuRegistration[] = [];
trayMenus: TrayMenuRegistration[] = [];
trayMenus: TrayMenuRegistration[] | IComputedValue<TrayMenuRegistration[]> = [];
/**
* implement this to modify the shell environment that Lens terminals are opened with. The ShellEnvModifier type has the signature

View File

@ -2,13 +2,14 @@
* 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 type { IObservableArray, IObservableValue } from "mobx";
import { computed, runInAction, observable } from "mobx";
import type { TrayMenuRegistration } from "../../main/tray/tray-menu-registration";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("preferences: extension adding tray items", () => {
describe("when extension with tray items is enabled", () => {
describe("when extension with tray items are statically defined", () => {
let builder: ApplicationBuilder;
let someObservableForVisibility: IObservableValue<boolean>;
let someObservableForEnabled: IObservableValue<boolean>;
@ -190,4 +191,88 @@ describe("preferences: extension adding tray items", () => {
expect(item?.enabled).toBe(false);
});
});
describe("when extension with tray items are dynamically defined", () => {
let builder: ApplicationBuilder;
let menuItems: IObservableArray<TrayMenuRegistration>;
beforeEach(async () => {
builder = getApplicationBuilder();
await builder.render();
builder.preferences.navigate();
menuItems = observable.array([
{
id: "some-visible",
label: "some-visible",
click: () => {},
visible: computed(() => true),
},
]);
const computedTrayMenu = computed(() => menuItems);
const testExtension = {
id: "some-extension-id",
name: "some-extension",
mainOptions: {
trayMenus: computedTrayMenu,
},
};
builder.extensions.enable(testExtension);
});
it("given item exists, it's shown", () => {
expect(
builder.tray.get(
"some-visible-tray-menu-item-for-extension-some-extension",
),
).not.toBeNull();
});
it("given item is added, it's shown", async () => {
menuItems.push({
id: "some-added",
label: "some-added",
click: () => {},
visible: computed(() => true),
});
expect(
builder.tray.get(
"some-added-tray-menu-item-for-extension-some-extension",
),
).not.toBeNull();
});
it("given item is removed, it's not shown", async () => {
menuItems.replace([]);
expect(
builder.tray.get(
"some-visible-tray-menu-item-for-extension-some-extension",
),
).toBeNull();
});
it("given items are removed and one added, it's shown", async () => {
menuItems.replace([]);
menuItems.push({
id: "some-added",
label: "some-added",
click: () => {},
visible: computed(() => true),
});
expect(
builder.tray.get(
"some-added-tray-menu-item-for-extension-some-extension",
),
).not.toBeNull();
});
});
});

View File

@ -15,8 +15,8 @@ const startReactiveTrayMenuItemsInjectable = getInjectable({
return {
id: "start-reactive-tray-menu-items",
run: async () => {
await reactiveTrayMenuItems.start();
run: () => {
reactiveTrayMenuItems.start();
},
runAfter: di.inject(startTrayInjectable),

View File

@ -25,9 +25,13 @@ const trayMenuItemRegistratorInjectable = getInjectable({
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
const getRandomId = di.inject(getRandomIdInjectable);
return mainExtension.trayMenus.flatMap(
toItemInjectablesFor(mainExtension, withErrorLoggingFor, getRandomId),
);
return computed(() => {
const trayMenus = Array.isArray(mainExtension.trayMenus) ? mainExtension.trayMenus : mainExtension.trayMenus.get();
return trayMenus.flatMap(
toItemInjectablesFor(mainExtension, withErrorLoggingFor, getRandomId),
);
});
},
injectionToken: extensionRegistratorInjectionToken,
@ -117,5 +121,3 @@ const toItemInjectablesFor = (extension: LensMainExtension, withErrorLoggingFor:
return _toItemInjectables(null);
};