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:
parent
a91e3a7f8e
commit
1861fe2049
@ -4,9 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
import type { Injectable } from "@ogre-tools/injectable";
|
import type { Injectable } from "@ogre-tools/injectable";
|
||||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
import type { LensExtension } from "../lens-extension";
|
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>({
|
export const extensionRegistratorInjectionToken = getInjectionToken<ExtensionRegistrator>({
|
||||||
id: "extension-registrator-token",
|
id: "extension-registrator-token",
|
||||||
|
|||||||
@ -2,8 +2,11 @@
|
|||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* 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 { 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 type { LensExtension } from "../../lens-extension";
|
||||||
import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token";
|
import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token";
|
||||||
|
|
||||||
@ -12,6 +15,16 @@ export interface Extension {
|
|||||||
deregister: () => void;
|
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({
|
const extensionInjectable = getInjectable({
|
||||||
id: "extension",
|
id: "extension",
|
||||||
|
|
||||||
@ -21,19 +34,42 @@ const extensionInjectable = getInjectable({
|
|||||||
|
|
||||||
instantiate: (childDi) => {
|
instantiate: (childDi) => {
|
||||||
const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken);
|
const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken);
|
||||||
|
const reactionDisposer = disposer();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
register: () => {
|
register: () => {
|
||||||
const injectables = extensionRegistrators.flatMap((getInjectablesOfExtension) =>
|
extensionRegistrators.forEach((getInjectablesOfExtension) => {
|
||||||
getInjectablesOfExtension(instance),
|
const injectables = getInjectablesOfExtension(instance);
|
||||||
);
|
|
||||||
|
|
||||||
runInAction(() => {
|
reactionDisposer.push(
|
||||||
childDi.register(...injectables);
|
// 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: () => {
|
deregister: () => {
|
||||||
|
reactionDisposer();
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
parentDi.deregister(extensionInjectable);
|
parentDi.deregister(extensionInjectable);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { LensExtension, lensExtensionDependencies } from "./lens-extension";
|
import { LensExtension, lensExtensionDependencies } from "./lens-extension";
|
||||||
import type { CatalogEntity } from "../common/catalog";
|
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 { MenuRegistration } from "../features/application-menu/main/menu-registration";
|
||||||
import type { TrayMenuRegistration } from "../main/tray/tray-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";
|
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> {
|
export class LensMainExtension extends LensExtension<LensMainExtensionDependencies> {
|
||||||
appMenus: MenuRegistration[] = [];
|
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
|
* implement this to modify the shell environment that Lens terminals are opened with. The ShellEnvModifier type has the signature
|
||||||
|
|||||||
@ -2,13 +2,14 @@
|
|||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* 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 { 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 type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
|
||||||
import { getApplicationBuilder } 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("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 builder: ApplicationBuilder;
|
||||||
let someObservableForVisibility: IObservableValue<boolean>;
|
let someObservableForVisibility: IObservableValue<boolean>;
|
||||||
let someObservableForEnabled: IObservableValue<boolean>;
|
let someObservableForEnabled: IObservableValue<boolean>;
|
||||||
@ -190,4 +191,88 @@ describe("preferences: extension adding tray items", () => {
|
|||||||
expect(item?.enabled).toBe(false);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,8 +15,8 @@ const startReactiveTrayMenuItemsInjectable = getInjectable({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: "start-reactive-tray-menu-items",
|
id: "start-reactive-tray-menu-items",
|
||||||
run: async () => {
|
run: () => {
|
||||||
await reactiveTrayMenuItems.start();
|
reactiveTrayMenuItems.start();
|
||||||
},
|
},
|
||||||
|
|
||||||
runAfter: di.inject(startTrayInjectable),
|
runAfter: di.inject(startTrayInjectable),
|
||||||
|
|||||||
@ -25,9 +25,13 @@ const trayMenuItemRegistratorInjectable = getInjectable({
|
|||||||
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
|
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
|
||||||
const getRandomId = di.inject(getRandomIdInjectable);
|
const getRandomId = di.inject(getRandomIdInjectable);
|
||||||
|
|
||||||
return mainExtension.trayMenus.flatMap(
|
return computed(() => {
|
||||||
toItemInjectablesFor(mainExtension, withErrorLoggingFor, getRandomId),
|
const trayMenus = Array.isArray(mainExtension.trayMenus) ? mainExtension.trayMenus : mainExtension.trayMenus.get();
|
||||||
);
|
|
||||||
|
return trayMenus.flatMap(
|
||||||
|
toItemInjectablesFor(mainExtension, withErrorLoggingFor, getRandomId),
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
injectionToken: extensionRegistratorInjectionToken,
|
injectionToken: extensionRegistratorInjectionToken,
|
||||||
@ -117,5 +121,3 @@ const toItemInjectablesFor = (extension: LensMainExtension, withErrorLoggingFor:
|
|||||||
|
|
||||||
return _toItemInjectables(null);
|
return _toItemInjectables(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user