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

Merge pull request #4527 from jansav/make-menu-registry-obsolete

Remove MenuRegistry in favour of dependency injection
This commit is contained in:
Janne Savolainen 2021-12-16 15:11:30 +02:00 committed by GitHub
commit 3418c0acfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 2597 additions and 886 deletions

View File

@ -196,8 +196,8 @@
"@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.1",
"@ogre-tools/injectable": "^1.3.0",
"@ogre-tools/injectable-react": "^1.3.1",
"@ogre-tools/injectable": "^1.4.1",
"@ogre-tools/injectable-react": "^1.4.1",
"@sentry/electron": "^2.5.4",
"@sentry/integrations": "^6.15.0",
"abort-controller": "^3.0.0",

View File

@ -21,13 +21,13 @@
import { match, matchPath } from "react-router";
import { countBy } from "lodash";
import { iter, Singleton } from "../utils";
import { iter } from "../utils";
import { pathToRegexp } from "path-to-regexp";
import logger from "../../main/logger";
import type Url from "url-parse";
import { RoutingError, RoutingErrorType } from "./error";
import { ExtensionsStore } from "../../extensions/extensions-store";
import { ExtensionLoader } from "../../extensions/extension-loader";
import type { ExtensionLoader as ExtensionLoaderType } from "../../extensions/extension-loader/extension-loader";
import type { LensExtension } from "../../extensions/lens-extension";
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
import { when } from "mobx";
@ -78,7 +78,11 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R
}
}
export abstract class LensProtocolRouter extends Singleton {
interface Dependencies {
extensionLoader: ExtensionLoaderType
}
export abstract class LensProtocolRouter {
// Map between path schemas and the handlers
protected internalRoutes = new Map<string, RouteHandler>();
@ -86,6 +90,8 @@ export abstract class LensProtocolRouter extends Singleton {
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
constructor(protected dependencies: Dependencies) {}
/**
* Attempts to route the given URL to all internal routes that have been registered
* @param url the parsed URL that initiated the `lens://` protocol
@ -180,15 +186,20 @@ export abstract class LensProtocolRouter extends Singleton {
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
const name = [publisher, partialName].filter(Boolean).join("/");
const extensionLoader = ExtensionLoader.getInstance();
const extensionLoader = this.dependencies.extensionLoader;
try {
/**
* Note, if `getInstanceByName` returns `null` that means we won't be getting an instance
*/
await when(() => extensionLoader.getInstanceByName(name) !== (void 0), { timeout: 5_000 });
} catch(error) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`);
await when(() => extensionLoader.getInstanceByName(name) !== void 0, {
timeout: 5_000,
});
} catch (error) {
logger.info(
`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`,
);
return name;
}
@ -233,7 +244,6 @@ export abstract class LensProtocolRouter extends Singleton {
// remove the extension name from the path name so we don't need to match on it anymore
url.set("pathname", url.pathname.slice(extension.name.length + 1));
try {
const handlers = iter.map(extension.protocolHandlers, ({ pathSchema, handler }) => [pathSchema, handler] as [string, RouteHandler]);

View File

@ -27,6 +27,9 @@ import { ExtensionDiscovery } from "../extension-discovery";
import os from "os";
import { Console } from "console";
import { AppPaths } from "../../common/app-paths";
import type { ExtensionLoader } from "../extension-loader";
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
jest.setTimeout(60_000);
@ -62,10 +65,16 @@ console = new Console(process.stdout, process.stderr); // fix mockFS
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
describe("ExtensionDiscovery", () => {
let extensionLoader: ExtensionLoader;
beforeEach(() => {
ExtensionDiscovery.resetInstance();
ExtensionsStore.resetInstance();
ExtensionsStore.createInstance();
const di = getDiForUnitTesting();
extensionLoader = di.inject(extensionLoaderInjectable);
});
describe("with mockFs", () => {
@ -98,7 +107,9 @@ describe("ExtensionDiscovery", () => {
(mockWatchInstance) as any,
);
const extensionDiscovery = ExtensionDiscovery.createInstance();
const extensionDiscovery = ExtensionDiscovery.createInstance(
extensionLoader,
);
// Need to force isLoaded to be true so that the file watching is started
extensionDiscovery.isLoaded = true;
@ -140,7 +151,9 @@ describe("ExtensionDiscovery", () => {
mockedWatch.mockImplementationOnce(() =>
(mockWatchInstance) as any,
);
const extensionDiscovery = ExtensionDiscovery.createInstance();
const extensionDiscovery = ExtensionDiscovery.createInstance(
extensionLoader,
);
// Need to force isLoaded to be true so that the file watching is started
extensionDiscovery.isLoaded = true;

View File

@ -19,11 +19,13 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ExtensionLoader } from "../extension-loader";
import type { ExtensionLoader } from "../extension-loader";
import { ipcRenderer } from "electron";
import { ExtensionsStore } from "../extensions-store";
import { Console } from "console";
import { stdout, stderr } from "process";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
console = new Console(stdout, stderr);
@ -128,13 +130,15 @@ jest.mock(
);
describe("ExtensionLoader", () => {
let extensionLoader: ExtensionLoader;
beforeEach(() => {
ExtensionLoader.resetInstance();
const di = getDiForUnitTesting();
extensionLoader = di.inject(extensionLoaderInjectable);
});
it.only("renderer updates extension after ipc broadcast", async (done) => {
const extensionLoader = ExtensionLoader.createInstance();
it.only("renderer updates extension after ipc broadcast", async done => {
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`);
await extensionLoader.init();
@ -178,8 +182,6 @@ describe("ExtensionLoader", () => {
// Disable sending events in this test
(ipcRenderer.on as any).mockImplementation();
const extensionLoader = ExtensionLoader.createInstance();
await extensionLoader.init();
expect(ExtensionsStore.getInstance().mergeState).not.toHaveBeenCalled();
@ -194,6 +196,7 @@ describe("ExtensionLoader", () => {
"manifest/path2": {
enabled: true,
name: "TestExtension2",
}});
},
});
});
});

View File

@ -32,7 +32,7 @@ import logger from "../main/logger";
import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store";
import { extensionInstaller } from "./extension-installer";
import { ExtensionsStore } from "./extensions-store";
import { ExtensionLoader } from "./extension-loader";
import type { ExtensionLoader } from "./extension-loader";
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
import { isProduction } from "../common/vars";
import { isCompatibleBundledExtension, isCompatibleExtension } from "./extension-compatibility";
@ -99,7 +99,7 @@ export class ExtensionDiscovery extends Singleton {
public events = new EventEmitter();
constructor() {
constructor(protected extensionLoader: ExtensionLoader) {
super();
makeObservable(this);
@ -277,7 +277,7 @@ export class ExtensionDiscovery extends Singleton {
* @param extensionId The ID of the extension to uninstall.
*/
async uninstallExtension(extensionId: LensExtensionId): Promise<void> {
const { manifest, absolutePath } = this.extensions.get(extensionId) ?? ExtensionLoader.getInstance().getExtension(extensionId);
const { manifest, absolutePath } = this.extensions.get(extensionId) ?? this.extensionLoader.getExtension(extensionId);
logger.info(`${logModule} Uninstalling ${manifest.name}`);

View File

@ -0,0 +1,31 @@
/**
* 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 { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { ExtensionLoader } from "./extension-loader";
const extensionLoaderInjectable: Injectable<ExtensionLoader> = {
getDependencies: () => ({}),
instantiate: () => new ExtensionLoader(),
lifecycle: lifecycleEnum.singleton,
};
export default extensionLoaderInjectable;

View File

@ -24,17 +24,16 @@ import { EventEmitter } from "events";
import { isEqual } from "lodash";
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
import path from "path";
import { AppPaths } from "../common/app-paths";
import { ClusterStore } from "../common/cluster-store";
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../common/ipc";
import { Disposer, getHostedClusterId, Singleton, toJS } from "../common/utils";
import logger from "../main/logger";
import type { InstalledExtension } from "./extension-discovery";
import { ExtensionsStore } from "./extensions-store";
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension";
import type { LensMainExtension } from "./lens-main-extension";
import type { LensRendererExtension } from "./lens-renderer-extension";
import * as registries from "./registries";
import { AppPaths } from "../../common/app-paths";
import { ClusterStore } from "../../common/cluster-store";
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc";
import { Disposer, getHostedClusterId, toJS } from "../../common/utils";
import logger from "../../main/logger";
import type { InstalledExtension } from "../extension-discovery";
import { ExtensionsStore } from "../extensions-store";
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
import type { LensRendererExtension } from "../lens-renderer-extension";
import * as registries from "../registries";
export function extensionPackagesRoot() {
return path.join(AppPaths.get("userData"));
@ -45,7 +44,7 @@ const logModule = "[EXTENSIONS-LOADER]";
/**
* Loads installed extensions to the Lens application
*/
export class ExtensionLoader extends Singleton {
export class ExtensionLoader {
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
protected instances = observable.map<LensExtensionId, LensExtension>();
@ -77,7 +76,6 @@ export class ExtensionLoader extends Singleton {
}
constructor() {
super();
makeObservable(this);
observe(this.instances, change => {
switch (change.type) {
@ -97,6 +95,10 @@ export class ExtensionLoader extends Singleton {
});
}
@computed get enabledExtensionInstances() : LensExtension[] {
return [...this.instances.values()].filter(extension => extension.isEnabled);
}
@computed get userExtensions(): Map<LensExtensionId, InstalledExtension> {
const extensions = this.toJSON();
@ -248,25 +250,7 @@ export class ExtensionLoader extends Singleton {
}
loadOnMain() {
registries.MenuRegistry.createInstance();
logger.debug(`${logModule}: load on main`);
this.autoInitExtensions(async (extension: LensMainExtension) => {
// Each .add returns a function to remove the item
const removeItems = [
registries.MenuRegistry.getInstance().add(extension.appMenus),
];
this.events.on("remove", (removedExtension: LensRendererExtension) => {
if (removedExtension.id === extension.id) {
removeItems.forEach(remove => {
remove();
});
}
});
return removeItems;
});
this.autoInitExtensions(() => Promise.resolve([]));
}
loadOnClusterManagerRenderer() {

View File

@ -18,7 +18,5 @@
* 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 { getInjectedComponent } from "@ogre-tools/injectable-react";
import KubeObjectMenuInjectable from "./kube-object-menu.injectable";
export const KubeObjectMenu = getInjectedComponent(KubeObjectMenuInjectable);
export * from "./extension-loader";

View File

@ -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 { Injectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed, IComputedValue } from "mobx";
import type { LensExtension } from "./lens-extension";
import type { ExtensionLoader } from "./extension-loader";
import extensionLoaderInjectable from "./extension-loader/extension-loader.injectable";
const extensionsInjectable: Injectable<
IComputedValue<LensExtension[]>,
{ extensionLoader: ExtensionLoader }
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
lifecycle: lifecycleEnum.singleton,
instantiate: ({ extensionLoader }) =>
computed(() => extensionLoader.enabledExtensionInstances),
};
export default extensionsInjectable;

View File

@ -0,0 +1,54 @@
/**
* 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 glob from "glob";
import { memoize } from "lodash/fp";
import {
createContainer,
ConfigurableDependencyInjectionContainer,
} from "@ogre-tools/injectable";
export const getDiForUnitTesting = () => {
const di: ConfigurableDependencyInjectionContainer = createContainer();
getInjectableFilePaths()
.map(key => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const injectable = require(key).default;
return {
id: key,
...injectable,
aliases: [injectable, ...(injectable.aliases || [])],
};
})
.forEach(injectable => di.register(injectable));
di.preventSideEffects();
return di;
};
const getInjectableFilePaths = memoize(() => [
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]);

View File

@ -20,7 +20,7 @@
*/
import type { InstalledExtension } from "./extension-discovery";
import { action, observable, makeObservable } from "mobx";
import { action, observable, makeObservable, computed } from "mobx";
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger";
import type { ProtocolHandlerRegistration } from "./registries";
@ -47,7 +47,12 @@ export class LensExtension {
protocolHandlers: ProtocolHandlerRegistration[] = [];
@observable private isEnabled = false;
@observable private _isEnabled = false;
@computed get isEnabled() {
return this._isEnabled;
}
[Disposers] = disposer();
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
@ -83,13 +88,13 @@ export class LensExtension {
@action
async enable(register: (ext: LensExtension) => Promise<Disposer[]>) {
if (this.isEnabled) {
if (this._isEnabled) {
return;
}
try {
await this.onActivate();
this.isEnabled = true;
this._isEnabled = true;
this[Disposers].push(...await register(this));
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
@ -100,11 +105,11 @@ export class LensExtension {
@action
async disable() {
if (!this.isEnabled) {
if (!this._isEnabled) {
return;
}
this.isEnabled = false;
this._isEnabled = false;
try {
await this.onDeactivate();

View File

@ -24,7 +24,7 @@ import { WindowManager } from "../main/window-manager";
import { catalogEntityRegistry } from "../main/catalog";
import type { CatalogEntity } from "../common/catalog";
import type { IObservableArray } from "mobx";
import type { MenuRegistration } from "./registries";
import type { MenuRegistration } from "../main/menu/menu-registration";
export class LensMainExtension extends LensExtension {
appMenus: MenuRegistration[] = [];

View File

@ -23,7 +23,6 @@
export * from "./page-registry";
export * from "./page-menu-registry";
export * from "./menu-registry";
export * from "./app-preference-registry";
export * from "./status-bar-registry";
export * from "./kube-object-detail-registry";

34
src/main/getDi.ts Normal file
View File

@ -0,0 +1,34 @@
/**
* 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 { createContainer } from "@ogre-tools/injectable";
export const getDi = () =>
createContainer(
getRequireContextForMainCode,
getRequireContextForCommonExtensionCode,
);
const getRequireContextForMainCode = () =>
require.context("./", true, /\.injectable\.(ts|tsx)$/);
const getRequireContextForCommonExtensionCode = () =>
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/);

View File

@ -0,0 +1,55 @@
/**
* 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 glob from "glob";
import { memoize } from "lodash/fp";
import {
createContainer,
ConfigurableDependencyInjectionContainer,
} from "@ogre-tools/injectable";
export const getDiForUnitTesting = () => {
const di: ConfigurableDependencyInjectionContainer = createContainer();
getInjectableFilePaths()
.map(key => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const injectable = require(key).default;
return {
id: key,
...injectable,
aliases: [injectable, ...(injectable.aliases || [])],
};
})
.forEach(injectable => di.register(injectable));
di.preventSideEffects();
return di;
};
const getInjectableFilePaths = memoize(() => [
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]);

View File

@ -36,11 +36,9 @@ import { mangleProxyEnv } from "./proxy-env";
import { registerFileProtocol } from "../common/register-protocol";
import logger from "./logger";
import { appEventBus } from "../common/event-bus";
import { ExtensionLoader } from "../extensions/extension-loader";
import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery";
import type { LensExtensionId } from "../extensions/lens-extension";
import { installDeveloperTools } from "./developer-tools";
import { LensProtocolRouterMain } from "./protocol-handler";
import { disposer, getAppVersion, getAppVersionFromProxyServer, storedKubeConfigFolder } from "../common/utils";
import { bindBroadcastHandlers, ipcMainOn } from "../common/ipc";
import { startUpdateChecking } from "./app-updater";
@ -61,11 +59,17 @@ import { FilesystemProvisionerStore } from "./extension-filesystem";
import { SentryInit } from "../common/sentry";
import { ensureDir } from "fs-extra";
import { Router } from "./router";
import { initMenu } from "./menu";
import { initMenu } from "./menu/menu";
import { initTray } from "./tray";
import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions";
import { AppPaths } from "../common/app-paths";
import { ShellSession } from "./shell-session/shell-session";
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";
const di = getDi();
injectSystemCAs();
@ -75,7 +79,6 @@ const onQuitCleanup = disposer();
SentryInit();
app.setName(appName);
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
if (app.setAsDefaultProtocolClient("lens")) {
@ -99,7 +102,10 @@ configurePackages();
mangleProxyEnv();
logger.debug("[APP-MAIN] initializing ipc main handlers");
initializers.initIpcMainHandlers();
const menuItems = di.inject(electronMenuItemsInjectable);
initializers.initIpcMainHandlers(menuItems);
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
@ -107,14 +113,14 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") {
logger.debug("[APP-MAIN] Lens protocol routing main");
const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable);
if (!app.requestSingleInstanceLock()) {
app.exit();
} else {
const lprm = LensProtocolRouterMain.createInstance();
for (const arg of process.argv) {
if (arg.toLowerCase().startsWith("lens://")) {
lprm.route(arg);
lensProtocolRouterMain.route(arg);
}
}
}
@ -122,11 +128,9 @@ if (!app.requestSingleInstanceLock()) {
app.on("second-instance", (event, argv) => {
logger.debug("second-instance message");
const lprm = LensProtocolRouterMain.createInstance();
for (const arg of argv) {
if (arg.toLowerCase().startsWith("lens://")) {
lprm.route(arg);
lensProtocolRouterMain.route(arg);
}
}
@ -223,10 +227,12 @@ app.on("ready", async () => {
return app.exit();
}
initializers.initRegistries();
const extensionDiscovery = ExtensionDiscovery.createInstance();
const extensionLoader = di.inject(extensionLoaderInjectable);
extensionLoader.init();
const extensionDiscovery = ExtensionDiscovery.createInstance(extensionLoader);
ExtensionLoader.createInstance().init();
extensionDiscovery.init();
// Start the app without showing the main window when auto starting on login
@ -237,7 +243,7 @@ app.on("ready", async () => {
const windowManager = WindowManager.createInstance();
onQuitCleanup.push(
initMenu(windowManager),
initMenu(windowManager, menuItems),
initTray(windowManager),
() => ShellSession.cleanup(),
);
@ -253,7 +259,7 @@ app.on("ready", async () => {
await ensureDir(storedKubeConfigFolder());
KubeconfigSyncManager.getInstance().startSync();
startUpdateChecking();
LensProtocolRouterMain.getInstance().rendererLoaded = true;
lensProtocolRouterMain.rendererLoaded = true;
});
logger.info("🧩 Initializing extensions");
@ -268,13 +274,13 @@ app.on("ready", async () => {
// Subscribe to extensions that are copied or deleted to/from the extensions folder
extensionDiscovery.events
.on("add", (extension: InstalledExtension) => {
ExtensionLoader.getInstance().addExtension(extension);
extensionLoader.addExtension(extension);
})
.on("remove", (lensExtensionId: LensExtensionId) => {
ExtensionLoader.getInstance().removeExtension(lensExtensionId);
extensionLoader.removeExtension(lensExtensionId);
});
ExtensionLoader.getInstance().initExtensions(extensions);
extensionLoader.initExtensions(extensions);
} catch (error) {
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
console.error(error);
@ -309,7 +315,6 @@ app.on("will-quit", (event) => {
// This is called when the close button of the main window is clicked
const lprm = LensProtocolRouterMain.getInstance(false);
logger.info("APP:QUIT");
appEventBus.emit({ name: "app", action: "close" });
@ -317,11 +322,9 @@ app.on("will-quit", (event) => {
KubeconfigSyncManager.getInstance(false)?.stopSync();
onCloseCleanup();
if (lprm) {
// This is set to false here so that LPRM can wait to send future lens://
// requests until after it loads again
lprm.rendererLoaded = false;
}
// This is set to false here so that LPRM can wait to send future lens://
// requests until after it loads again
lensProtocolRouterMain.rendererLoaded = false;
if (blockQuit) {
// Quit app on Cmd+Q (MacOS)
@ -331,7 +334,7 @@ app.on("will-quit", (event) => {
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
}
lprm?.cleanup();
lensProtocolRouterMain.cleanup();
onQuitCleanup();
});
@ -340,7 +343,7 @@ app.on("open-url", (event, rawUrl) => {
// lens:// protocol handler
event.preventDefault();
LensProtocolRouterMain.getInstance().route(rawUrl);
lensProtocolRouterMain.route(rawUrl);
});
/**

View File

@ -18,8 +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.
*/
export * from "./registries";
export * from "./metrics-providers";
export * from "./ipc";
export * from "./cluster-metadata-detectors";

View File

@ -34,9 +34,11 @@ import { IpcMainWindowEvents, WindowManager } from "../window-manager";
import path from "path";
import { remove } from "fs-extra";
import { AppPaths } from "../../common/app-paths";
import { getAppMenu } from "../menu";
import { getAppMenu } from "../menu/menu";
import type { MenuRegistration } from "../menu/menu-registration";
import type { IComputedValue } from "mobx";
export function initIpcMainHandlers() {
export function initIpcMainHandlers(electronMenuItems: IComputedValue<MenuRegistration[]>) {
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
.getById(clusterId)
@ -151,7 +153,7 @@ export function initIpcMainHandlers() {
});
ipcMainOn(IpcMainWindowEvents.OPEN_CONTEXT_MENU, async (event) => {
const menu = Menu.buildFromTemplate(getAppMenu(WindowManager.getInstance()));
const menu = Menu.buildFromTemplate(getAppMenu(WindowManager.getInstance(), electronMenuItems.get()));
const options = {
...BrowserWindow.fromWebContents(event.sender),
// Center of the topbar menu icon

View File

@ -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 { Injectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed, IComputedValue } from "mobx";
import type { LensMainExtension } from "../../extensions/lens-main-extension";
import extensionsInjectable from "../../extensions/extensions.injectable";
import type { MenuRegistration } from "./menu-registration";
const electronMenuItemsInjectable: Injectable<
IComputedValue<MenuRegistration[]>,
{ extensions: IComputedValue<LensMainExtension[]> }
> = {
lifecycle: lifecycleEnum.singleton,
getDependencies: di => ({
extensions: di.inject(extensionsInjectable),
}),
instantiate: ({ extensions }) =>
computed(() => extensions.get().flatMap(extension => extension.appMenus)),
};
export default electronMenuItemsInjectable;

View File

@ -0,0 +1,132 @@
/**
* 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 electronMenuItemsInjectable from "./electron-menu-items.injectable";
import type { IComputedValue } from "mobx";
import { computed, ObservableMap, runInAction } from "mobx";
import type { MenuRegistration } from "./menu-registration";
import extensionsInjectable from "../../extensions/extensions.injectable";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
describe("electron-menu-items", () => {
let di: ConfigurableDependencyInjectionContainer;
let electronMenuItems: IComputedValue<MenuRegistration[]>;
let extensionsStub: ObservableMap<string, LensMainExtension>;
beforeEach(() => {
di = getDiForUnitTesting();
extensionsStub = new ObservableMap();
di.override(
extensionsInjectable,
computed(() => [...extensionsStub.values()]),
);
electronMenuItems = di.inject(electronMenuItemsInjectable);
});
it("does not have any items yet", () => {
expect(electronMenuItems.get()).toHaveLength(0);
});
describe("when extension is enabled", () => {
beforeEach(() => {
const someExtension = new SomeTestExtension({
id: "some-extension-id",
appMenus: [{ parentId: "some-parent-id-from-first-extension" }],
});
runInAction(() => {
extensionsStub.set("some-extension-id", someExtension);
});
});
it("has menu items", () => {
expect(electronMenuItems.get()).toEqual([
{
parentId: "some-parent-id-from-first-extension",
},
]);
});
it("when disabling extension, does not have menu items", () => {
extensionsStub.delete("some-extension-id");
expect(electronMenuItems.get()).toHaveLength(0);
});
describe("when other extension is enabled", () => {
beforeEach(() => {
const someOtherExtension = new SomeTestExtension({
id: "some-extension-id",
appMenus: [{ parentId: "some-parent-id-from-second-extension" }],
});
extensionsStub.set("some-other-extension-id", someOtherExtension);
});
it("has menu items for both extensions", () => {
expect(electronMenuItems.get()).toEqual([
{
parentId: "some-parent-id-from-first-extension",
},
{
parentId: "some-parent-id-from-second-extension",
},
]);
});
it("when extension is disabled, still returns menu items for extensions that are enabled", () => {
runInAction(() => {
extensionsStub.delete("some-other-extension-id");
});
expect(electronMenuItems.get()).toEqual([
{
parentId: "some-parent-id-from-first-extension",
},
]);
});
});
});
});
class SomeTestExtension extends LensMainExtension {
constructor({ id, appMenus }: {
id: string;
appMenus: MenuRegistration[];
}) {
super({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: { name: id, version: "some-version" },
manifestPath: "irrelevant",
});
this.appMenus = appMenus;
}
}

View File

@ -18,15 +18,8 @@
* 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.
*/
// Extensions API -> Global menu customizations
import type { MenuItemConstructorOptions } from "electron";
import { BaseRegistry } from "./base-registry";
export interface MenuRegistration extends MenuItemConstructorOptions {
parentId: string;
}
export class MenuRegistry extends BaseRegistry<MenuRegistration> {
}

View File

@ -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 { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
import { autorun } from "mobx";
import type { WindowManager } from "./window-manager";
import { appName, isMac, isWindows, docsUrl, supportUrl, productName } from "../common/vars";
import { MenuRegistry } from "../extensions/registries/menu-registry";
import logger from "./logger";
import { exitApp } from "./exit-app";
import { broadcastMessage } from "../common/ipc";
import * as packageJson from "../../package.json";
import { preferencesURL, extensionsURL, addClusterURL, catalogURL, welcomeURL } from "../common/routes";
import { checkForUpdates, isAutoUpdateEnabled } from "./app-updater";
import { autorun, IComputedValue } from "mobx";
import type { WindowManager } from "../window-manager";
import { appName, isMac, isWindows, docsUrl, supportUrl, productName } from "../../common/vars";
import logger from "../logger";
import { exitApp } from "../exit-app";
import { broadcastMessage } from "../../common/ipc";
import * as packageJson from "../../../package.json";
import { preferencesURL, extensionsURL, addClusterURL, catalogURL, welcomeURL } from "../../common/routes";
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
import type { MenuRegistration } from "./menu-registration";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
@ -37,8 +36,11 @@ interface MenuItemsOpts extends MenuItemConstructorOptions {
submenu?: MenuItemConstructorOptions[];
}
export function initMenu(windowManager: WindowManager) {
return autorun(() => buildMenu(windowManager), {
export function initMenu(
windowManager: WindowManager,
electronMenuItems: IComputedValue<MenuRegistration[]>,
) {
return autorun(() => buildMenu(windowManager, electronMenuItems.get()), {
delay: 100,
});
}
@ -61,7 +63,10 @@ export function showAbout(browserWindow: BrowserWindow) {
});
}
export function getAppMenu(windowManager: WindowManager) {
export function getAppMenu(
windowManager: WindowManager,
electronMenuItems: MenuRegistration[],
) {
function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) {
return check ? [] : menuItems;
}
@ -302,14 +307,17 @@ export function getAppMenu(windowManager: WindowManager) {
]);
// Modify menu from extensions-api
for (const { parentId, ...menuItem } of MenuRegistry.getInstance().getItems()) {
if (!appMenu.has(parentId)) {
logger.error(`[MENU]: cannot register menu item for parentId=${parentId}, parent item doesn't exist`, { menuItem });
for (const menuItem of electronMenuItems) {
if (!appMenu.has(menuItem.parentId)) {
logger.error(
`[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`,
{ menuItem },
);
continue;
}
appMenu.get(parentId).submenu.push(menuItem);
appMenu.get(menuItem.parentId).submenu.push(menuItem);
}
if (!isMac) {
@ -320,6 +328,11 @@ export function getAppMenu(windowManager: WindowManager) {
}
export function buildMenu(windowManager: WindowManager) {
Menu.setApplicationMenu(Menu.buildFromTemplate(getAppMenu(windowManager)));
export function buildMenu(
windowManager: WindowManager,
electronMenuItems: MenuRegistration[],
) {
Menu.setApplicationMenu(
Menu.buildFromTemplate(getAppMenu(windowManager, electronMenuItems)),
);
}

View File

@ -25,11 +25,15 @@ import { broadcastMessage } from "../../../common/ipc";
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
import { delay, noop } from "../../../common/utils";
import { LensExtension } from "../../../extensions/main-api";
import { ExtensionLoader } from "../../../extensions/extension-loader";
import { ExtensionsStore } from "../../../extensions/extensions-store";
import { LensProtocolRouterMain } from "../router";
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
import mockFs from "mock-fs";
import { AppPaths } from "../../../common/app-paths";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import extensionLoaderInjectable
from "../../../extensions/extension-loader/extension-loader.injectable";
import lensProtocolRouterMainInjectable
from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
jest.mock("../../../common/ipc");
@ -58,14 +62,22 @@ function throwIfDefined(val: any): void {
}
describe("protocol router tests", () => {
// TODO: This test suite is using any to access protected property.
// Unit tests are allowed to only public interfaces.
let extensionLoader: any;
let lpr: LensProtocolRouterMain;
beforeEach(() => {
const di = getDiForUnitTesting();
extensionLoader = di.inject(extensionLoaderInjectable);
mockFs({
"tmp": {},
});
ExtensionsStore.createInstance();
ExtensionLoader.createInstance();
const lpr = LensProtocolRouterMain.createInstance();
lpr = di.inject(lensProtocolRouterMainInjectable);
lpr.rendererLoaded = true;
});
@ -74,15 +86,11 @@ describe("protocol router tests", () => {
jest.clearAllMocks();
ExtensionsStore.resetInstance();
ExtensionLoader.resetInstance();
LensProtocolRouterMain.resetInstance();
mockFs.restore();
});
it("should throw on non-lens URLS", async () => {
try {
const lpr = LensProtocolRouterMain.getInstance();
expect(await lpr.route("https://google.ca")).toBeUndefined();
} catch (error) {
expect(error).toBeInstanceOf(Error);
@ -91,8 +99,6 @@ describe("protocol router tests", () => {
it("should throw when host not internal or extension", async () => {
try {
const lpr = LensProtocolRouterMain.getInstance();
expect(await lpr.route("lens://foobar")).toBeUndefined();
} catch (error) {
expect(error).toBeInstanceOf(Error);
@ -113,14 +119,13 @@ describe("protocol router tests", () => {
isCompatible: true,
absolutePath: "/foo/bar",
});
const lpr = LensProtocolRouterMain.getInstance();
ext.protocolHandlers.push({
pathSchema: "/",
handler: noop,
});
(ExtensionLoader.getInstance() as any).instances.set(extId, ext);
extensionLoader.instances.set(extId, ext);
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" });
lpr.addInternalHandler("/", noop);
@ -143,7 +148,6 @@ describe("protocol router tests", () => {
});
it("should call handler if matches", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called = false;
lpr.addInternalHandler("/page", () => { called = true; });
@ -159,7 +163,6 @@ describe("protocol router tests", () => {
});
it("should call most exact handler", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0;
lpr.addInternalHandler("/page", () => { called = 1; });
@ -178,7 +181,6 @@ describe("protocol router tests", () => {
it("should call most exact handler for an extension", async () => {
let called: any = 0;
const lpr = LensProtocolRouterMain.getInstance();
const extId = uuid.v4();
const ext = new LensExtension({
id: extId,
@ -202,7 +204,7 @@ describe("protocol router tests", () => {
handler: params => { called = params.pathname.id; },
});
(ExtensionLoader.getInstance() as any).instances.set(extId, ext);
extensionLoader.instances.set(extId, ext);
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
try {
@ -217,7 +219,6 @@ describe("protocol router tests", () => {
});
it("should work with non-org extensions", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0;
{
@ -241,7 +242,7 @@ describe("protocol router tests", () => {
handler: params => { called = params.pathname.id; },
});
(ExtensionLoader.getInstance() as any).instances.set(extId, ext);
extensionLoader.instances.set(extId, ext);
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
}
@ -266,7 +267,7 @@ describe("protocol router tests", () => {
handler: () => { called = 1; },
});
(ExtensionLoader.getInstance() as any).instances.set(extId, ext);
extensionLoader.instances.set(extId, ext);
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "icecream" });
}
@ -286,13 +287,10 @@ describe("protocol router tests", () => {
});
it("should throw if urlSchema is invalid", () => {
const lpr = LensProtocolRouterMain.getInstance();
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
});
it("should call most exact handler with 3 found handlers", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0;
lpr.addInternalHandler("/", () => { called = 2; });
@ -311,7 +309,6 @@ describe("protocol router tests", () => {
});
it("should call most exact handler with 2 found handlers", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0;
lpr.addInternalHandler("/", () => { called = 2; });

View File

@ -19,4 +19,4 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export * from "./router";
export * from "./lens-protocol-router-main/lens-protocol-router-main";

View File

@ -0,0 +1,40 @@
/**
* 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 { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
import type { Dependencies } from "./lens-protocol-router-main";
import { LensProtocolRouterMain } from "./lens-protocol-router-main";
const lensProtocolRouterMainInjectable: Injectable<
LensProtocolRouterMain,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: dependencies => new LensProtocolRouterMain(dependencies),
lifecycle: lifecycleEnum.singleton,
};
export default lensProtocolRouterMainInjectable;

View File

@ -19,15 +19,16 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import logger from "../logger";
import * as proto from "../../common/protocol-handler";
import logger from "../../logger";
import * as proto from "../../../common/protocol-handler";
import URLParse from "url-parse";
import type { LensExtension } from "../../extensions/lens-extension";
import { broadcastMessage } from "../../common/ipc";
import type { LensExtension } from "../../../extensions/lens-extension";
import { broadcastMessage } from "../../../common/ipc";
import { observable, when, makeObservable } from "mobx";
import { ProtocolHandlerInvalid, RouteAttempt } from "../../common/protocol-handler";
import { disposer, noop } from "../../common/utils";
import { WindowManager } from "../window-manager";
import { ProtocolHandlerInvalid, RouteAttempt } from "../../../common/protocol-handler";
import { disposer, noop } from "../../../common/utils";
import { WindowManager } from "../../window-manager";
import type { ExtensionLoader } from "../../../extensions/extension-loader";
export interface FallbackHandler {
(name: string): Promise<boolean>;
@ -50,6 +51,10 @@ function checkHost<Query>(url: URLParse<Query>): boolean {
}
}
export interface Dependencies {
extensionLoader: ExtensionLoader
}
export class LensProtocolRouterMain extends proto.LensProtocolRouter {
private missingExtensionHandlers: FallbackHandler[] = [];
@ -57,8 +62,8 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
protected disposers = disposer();
constructor() {
super();
constructor(protected dependencies: Dependencies) {
super(dependencies);
makeObservable(this);
}

View File

@ -23,7 +23,7 @@ import path from "path";
import packageInfo from "../../package.json";
import { Menu, Tray } from "electron";
import { autorun } from "mobx";
import { showAbout } from "./menu";
import { showAbout } from "./menu/menu";
import { checkForUpdates, isAutoUpdateEnabled } from "./app-updater";
import type { WindowManager } from "./window-manager";
import logger from "./logger";

View File

@ -34,7 +34,6 @@ import { isMac, isDevelopment } from "../common/vars";
import { ClusterStore } from "../common/cluster-store";
import { UserStore } from "../common/user-store";
import { ExtensionDiscovery } from "../extensions/extension-discovery";
import { ExtensionLoader } from "../extensions/extension-loader";
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
import { DefaultProps } from "./mui-base-theme";
@ -53,6 +52,13 @@ import { registerCustomThemes } from "./components/monaco-editor";
import { getDi } from "./components/getDi";
import { DiContextProvider } from "@ogre-tools/injectable-react";
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
import type { ExtensionLoader } from "../extensions/extension-loader";
import bindProtocolAddRouteHandlersInjectable
from "./protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable";
import type { LensProtocolRouterRenderer } from "./protocol-handler";
import lensProtocolRouterRendererInjectable
from "./protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable";
if (process.isMainFrame) {
SentryInit();
@ -73,7 +79,14 @@ async function attachChromeDebugger() {
}
type AppComponent = React.ComponentType & {
init(rootElem: HTMLElement): Promise<void>;
// TODO: This static method is criminal as it has no direct relation with component
init(
rootElem: HTMLElement,
extensionLoader: ExtensionLoader,
bindProtocolAddRouteHandlers?: () => void,
lensProtocolRouterRendererInjectable?: LensProtocolRouterRenderer
): Promise<void>;
};
export async function bootstrap(comp: () => Promise<AppComponent>, di: DependencyInjectionContainer) {
@ -116,14 +129,17 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
logger.info(`${logPrefix} initializing Catalog`);
initializers.initCatalog();
const extensionLoader = di.inject(extensionLoaderInjectable);
logger.info(`${logPrefix} initializing IpcRendererListeners`);
initializers.initIpcRendererListeners();
initializers.initIpcRendererListeners(extensionLoader);
logger.info(`${logPrefix} initializing StatusBarRegistry`);
initializers.initStatusBarRegistry();
ExtensionLoader.createInstance().init();
ExtensionDiscovery.createInstance().init();
extensionLoader.init();
ExtensionDiscovery.createInstance(extensionLoader).init();
// ClusterStore depends on: UserStore
const clusterStore = ClusterStore.createInstance();
@ -151,7 +167,10 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
// init app's dependencies if any
const App = await comp();
await App.init(rootElem);
const bindProtocolAddRouteHandlers = di.inject(bindProtocolAddRouteHandlersInjectable);
const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable);
await App.init(rootElem, extensionLoader, bindProtocolAddRouteHandlers, lensProtocolRouterRenderer);
render(
<DiContextProvider value={{ di }}>

View File

@ -35,7 +35,7 @@ import { isAllowedResource } from "../common/utils/allowed-resource";
import logger from "../main/logger";
import { webFrame } from "electron";
import { ClusterPageRegistry, getExtensionPageUrl } from "../extensions/registries/page-registry";
import { ExtensionLoader } from "../extensions/extension-loader";
import type { ExtensionLoader } from "../extensions/extension-loader";
import { appEventBus } from "../common/event-bus";
import { requestMain } from "../common/ipc";
import { clusterSetFrameIdHandler } from "../common/cluster-ipc";
@ -86,7 +86,7 @@ export class ClusterFrame extends React.Component {
makeObservable(this);
}
static async init(rootElem: HTMLElement) {
static async init(rootElem: HTMLElement, extensionLoader: ExtensionLoader) {
catalogEntityRegistry.init();
const frameId = webFrame.routingId;
@ -101,7 +101,8 @@ export class ClusterFrame extends React.Component {
catalogEntityRegistry.activeEntity = ClusterFrame.clusterId;
ExtensionLoader.getInstance().loadOnClusterRenderer();
extensionLoader.loadOnClusterRenderer();
setTimeout(() => {
appEventBus.emit({
name: "cluster",

View File

@ -20,18 +20,22 @@
*/
import "@testing-library/jest-dom/extend-expect";
import { fireEvent, render, waitFor } from "@testing-library/react";
import { fireEvent, waitFor } from "@testing-library/react";
import fse from "fs-extra";
import React from "react";
import { UserStore } from "../../../../common/user-store";
import { ExtensionDiscovery } from "../../../../extensions/extension-discovery";
import { ExtensionLoader } from "../../../../extensions/extension-loader";
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
import { ConfirmDialog } from "../../confirm-dialog";
import { ExtensionInstallationStateStore } from "../extension-install.store";
import { Extensions } from "../extensions";
import mockFs from "mock-fs";
import { mockWindow } from "../../../../../__mocks__/windowMock";
import { AppPaths } from "../../../../common/app-paths";
import extensionLoaderInjectable
from "../../../../extensions/extension-loader/extension-loader.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { DiRender, renderFor } from "../../test-utils/renderFor";
mockWindow();
@ -73,14 +77,23 @@ jest.mock("electron", () => ({
AppPaths.init();
describe("Extensions", () => {
let extensionLoader: ExtensionLoader;
let render: DiRender;
beforeEach(async () => {
const di = getDiForUnitTesting();
render = renderFor(di);
extensionLoader = di.inject(extensionLoaderInjectable);
mockFs({
"tmp": {},
});
ExtensionInstallationStateStore.reset();
ExtensionLoader.createInstance().addExtension({
extensionLoader.addExtension({
id: "extensionId",
manifest: {
name: "test",
@ -92,7 +105,11 @@ describe("Extensions", () => {
isEnabled: true,
isCompatible: true,
});
ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve());
const extensionDiscovery = ExtensionDiscovery.createInstance(extensionLoader);
extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve());
UserStore.createInstance();
});
@ -100,7 +117,6 @@ describe("Extensions", () => {
mockFs.restore();
UserStore.resetInstance();
ExtensionDiscovery.resetInstance();
ExtensionLoader.resetInstance();
});
it("disables uninstall and disable buttons while uninstalling", async () => {

View File

@ -0,0 +1,35 @@
/**
* 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 { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { attemptInstallByInfo, ExtensionInfo } from "./attempt-install-by-info";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
const attemptInstallByInfoInjectable: Injectable<(extensionInfo: ExtensionInfo) => Promise<void>, {}> = {
getDependencies: di => ({
attemptInstall: di.inject(attemptInstallInjectable),
}),
instantiate: attemptInstallByInfo,
lifecycle: lifecycleEnum.singleton,
};
export default attemptInstallByInfoInjectable;

View File

@ -0,0 +1,123 @@
/**
* 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 { ExtensionInstallationStateStore } from "../extension-install.store";
import { downloadFile, downloadJson, ExtendableDisposer } from "../../../../common/utils";
import { Notifications } from "../../notifications";
import { ConfirmDialog } from "../../confirm-dialog";
import React from "react";
import path from "path";
import { SemVer } from "semver";
import URLParse from "url-parse";
import type { InstallRequest } from "../attempt-install/install-request";
import lodash from "lodash";
export interface ExtensionInfo {
name: string;
version?: string;
requireConfirmation?: boolean;
}
export interface Dependencies {
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>
}
export const attemptInstallByInfo = ({ attemptInstall }: Dependencies) => async ({
name,
version,
requireConfirmation = false,
}: ExtensionInfo) => {
const disposer = ExtensionInstallationStateStore.startPreInstall();
const registryUrl = new URLParse("https://registry.npmjs.com")
.set("pathname", name)
.toString();
const { promise } = downloadJson({ url: registryUrl });
const json = await promise.catch(console.error);
if (
!json ||
json.error ||
typeof json.versions !== "object" ||
!json.versions
) {
const message = json?.error ? `: ${json.error}` : "";
Notifications.error(
`Failed to get registry information for that extension${message}`,
);
return disposer();
}
if (version) {
if (!json.versions[version]) {
if (json["dist-tags"][version]) {
version = json["dist-tags"][version];
} else {
Notifications.error(
<p>
The <em>{name}</em> extension does not have a version or tag{" "}
<code>{version}</code>.
</p>,
);
return disposer();
}
}
} else {
const versions = Object.keys(json.versions)
.map(
version =>
new SemVer(version, { loose: true, includePrerelease: true }),
)
// ignore pre-releases for auto picking the version
.filter(version => version.prerelease.length === 0);
version = lodash.reduce(versions, (prev, curr) =>
prev.compareMain(curr) === -1 ? curr : prev,
).format();
}
if (requireConfirmation) {
const proceed = await ConfirmDialog.confirm({
message: (
<p>
Are you sure you want to install{" "}
<b>
{name}@{version}
</b>
?
</p>
),
labelCancel: "Cancel",
labelOk: "Install",
});
if (!proceed) {
return disposer();
}
}
const url = json.versions[version].dist.tarball;
const fileName = path.basename(url);
const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 });
return attemptInstall({ fileName, dataP }, disposer);
};

View File

@ -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 type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import type { ExtendableDisposer } from "../../../../common/utils";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable";
import type { Dependencies } from "./attempt-install";
import { attemptInstall } from "./attempt-install";
import type { InstallRequest } from "./install-request";
import unpackExtensionInjectable from "./unpack-extension/unpack-extension.injectable";
const attemptInstallInjectable: Injectable<
(request: InstallRequest, d?: ExtendableDisposer) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
uninstallExtension: di.inject(uninstallExtensionInjectable),
unpackExtension: di.inject(unpackExtensionInjectable),
}),
instantiate: attemptInstall,
lifecycle: lifecycleEnum.singleton,
};
export default attemptInstallInjectable;

View File

@ -0,0 +1,138 @@
/**
* 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 {
Disposer,
disposer,
ExtendableDisposer,
} from "../../../../common/utils";
import {
ExtensionInstallationState,
ExtensionInstallationStateStore,
} from "../extension-install.store";
import { Notifications } from "../../notifications";
import { Button } from "../../button";
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import React from "react";
import fse from "fs-extra";
import { shell } from "electron";
import {
createTempFilesAndValidate,
InstallRequestValidated,
} from "./create-temp-files-and-validate/create-temp-files-and-validate";
import { getExtensionDestFolder } from "./get-extension-dest-folder/get-extension-dest-folder";
import type { InstallRequest } from "./install-request";
export interface Dependencies {
extensionLoader: ExtensionLoader;
uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
unpackExtension: (
request: InstallRequestValidated,
disposeDownloading: Disposer,
) => Promise<void>;
}
export const attemptInstall =
({ extensionLoader, uninstallExtension, unpackExtension }: Dependencies) =>
async (request: InstallRequest, d?: ExtendableDisposer): Promise<void> => {
const dispose = disposer(
ExtensionInstallationStateStore.startPreInstall(),
d,
);
const validatedRequest = await createTempFilesAndValidate(request);
if (!validatedRequest) {
return dispose();
}
const { name, version, description } = validatedRequest.manifest;
const curState = ExtensionInstallationStateStore.getInstallationState(
validatedRequest.id,
);
if (curState !== ExtensionInstallationState.IDLE) {
dispose();
return void Notifications.error(
<div className="flex column gaps">
<b>Extension Install Collision:</b>
<p>
The <em>{name}</em> extension is currently {curState.toLowerCase()}.
</p>
<p>Will not proceed with this current install request.</p>
</div>,
);
}
const extensionFolder = getExtensionDestFolder(name);
const folderExists = await fse.pathExists(extensionFolder);
if (!folderExists) {
// install extension if not yet exists
await unpackExtension(validatedRequest, dispose);
} else {
const {
manifest: { version: oldVersion },
} = extensionLoader.getExtension(validatedRequest.id);
// otherwise confirmation required (re-install / update)
const removeNotification = Notifications.info(
<div className="InstallingExtensionNotification flex gaps align-center">
<div className="flex column gaps">
<p>
Install extension{" "}
<b>
{name}@{version}
</b>
?
</p>
<p>
Description: <em>{description}</em>
</p>
<div
className="remove-folder-warning"
onClick={() => shell.openPath(extensionFolder)}
>
<b>Warning:</b> {name}@{oldVersion} will be removed before
installation.
</div>
</div>
<Button
autoFocus
label="Install"
onClick={async () => {
removeNotification();
if (await uninstallExtension(validatedRequest.id)) {
await unpackExtension(validatedRequest, dispose);
} else {
dispose();
}
}}
/>
</div>,
{
onClose: dispose,
},
);
}
};

View File

@ -0,0 +1,101 @@
/**
* 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 { validatePackage } from "../validate-package/validate-package";
import { ExtensionDiscovery } from "../../../../../extensions/extension-discovery";
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
import logger from "../../../../../main/logger";
import { Notifications } from "../../../notifications";
import path from "path";
import fse from "fs-extra";
import React from "react";
import os from "os";
import type {
LensExtensionId,
LensExtensionManifest,
} from "../../../../../extensions/lens-extension";
import type { InstallRequest } from "../install-request";
export interface InstallRequestValidated {
fileName: string;
data: Buffer;
id: LensExtensionId;
manifest: LensExtensionManifest;
tempFile: string; // temp system path to packed extension for unpacking
}
export async function createTempFilesAndValidate({
fileName,
dataP,
}: InstallRequest): Promise<InstallRequestValidated | null> {
// copy files to temp
await fse.ensureDir(getExtensionPackageTemp());
// validate packages
const tempFile = getExtensionPackageTemp(fileName);
try {
const data = await dataP;
if (!data) {
return null;
}
await fse.writeFile(tempFile, data);
const manifest = await validatePackage(tempFile);
const id = path.join(
ExtensionDiscovery.getInstance().nodeModulesPath,
manifest.name,
"package.json",
);
return {
fileName,
data,
manifest,
tempFile,
id,
};
} catch (error) {
const message = getMessageFromError(error);
logger.info(
`[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`,
{ error },
);
Notifications.error(
<div className="flex column gaps">
<p>
Installing <em>{fileName}</em> has failed, skipping.
</p>
<p>
Reason: <em>{message}</em>
</p>
</div>,
);
}
return null;
}
function getExtensionPackageTemp(fileName = "") {
return path.join(os.tmpdir(), "lens-extensions", fileName);
}

View File

@ -0,0 +1,28 @@
/**
* 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 { ExtensionDiscovery } from "../../../../../extensions/extension-discovery";
import { sanitizeExtensionName } from "../../../../../extensions/lens-extension";
import path from "path";
export const getExtensionDestFolder = (name: string) => path.join(
ExtensionDiscovery.getInstance().localFolderPath,
sanitizeExtensionName(name),
);

View File

@ -18,9 +18,7 @@
* 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 * as registries from "../../extensions/registries";
export function initRegistries() {
registries.MenuRegistry.createInstance();
export interface InstallRequest {
fileName: string;
dataP: Promise<Buffer | null>;
}

View File

@ -0,0 +1,43 @@
/**
* 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 { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { Dependencies, unpackExtension } from "./unpack-extension";
import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate";
import type { Disposer } from "../../../../../common/utils";
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
const unpackExtensionInjectable: Injectable<
(
request: InstallRequestValidated,
disposeDownloading?: Disposer,
) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: unpackExtension,
lifecycle: lifecycleEnum.singleton,
};
export default unpackExtensionInjectable;

View File

@ -0,0 +1,112 @@
/**
* 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 { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate";
import { Disposer, extractTar, noop } from "../../../../../common/utils";
import { ExtensionInstallationStateStore } from "../../extension-install.store";
import { extensionDisplayName } from "../../../../../extensions/lens-extension";
import logger from "../../../../../main/logger";
import type { ExtensionLoader } from "../../../../../extensions/extension-loader";
import { Notifications } from "../../../notifications";
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
import { getExtensionDestFolder } from "../get-extension-dest-folder/get-extension-dest-folder";
import path from "path";
import fse from "fs-extra";
import { when } from "mobx";
import React from "react";
export interface Dependencies {
extensionLoader: ExtensionLoader
}
export const unpackExtension = ({ extensionLoader }: Dependencies) => async (
request: InstallRequestValidated,
disposeDownloading?: Disposer,
) => {
const {
id,
fileName,
tempFile,
manifest: { name, version },
} = request;
ExtensionInstallationStateStore.setInstalling(id);
disposeDownloading?.();
const displayName = extensionDisplayName(name, version);
const extensionFolder = getExtensionDestFolder(name);
const unpackingTempFolder = path.join(
path.dirname(tempFile),
`${path.basename(tempFile)}-unpacked`,
);
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
try {
// extract to temp folder first
await fse.remove(unpackingTempFolder).catch(noop);
await fse.ensureDir(unpackingTempFolder);
await extractTar(tempFile, { cwd: unpackingTempFolder });
// move contents to extensions folder
const unpackedFiles = await fse.readdir(unpackingTempFolder);
let unpackedRootFolder = unpackingTempFolder;
if (unpackedFiles.length === 1) {
// check if %extension.tgz was packed with single top folder,
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
}
await fse.ensureDir(extensionFolder);
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
// wait for the loader has actually install it
await when(() => extensionLoader.userExtensions.has(id));
// Enable installed extensions by default.
extensionLoader.setIsEnabled(id, true);
Notifications.ok(
<p>
Extension <b>{displayName}</b> successfully installed!
</p>,
);
} catch (error) {
const message = getMessageFromError(error);
logger.info(
`[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`,
{ error },
);
Notifications.error(
<p>
Installing extension <b>{displayName}</b> has failed: <em>{message}</em>
</p>,
);
} finally {
// Remove install state once finished
ExtensionInstallationStateStore.clearInstalling(id);
// clean up
fse.remove(unpackingTempFolder).catch(noop);
fse.unlink(tempFile).catch(noop);
}
};

View File

@ -0,0 +1,63 @@
/**
* 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 { LensExtensionManifest } from "../../../../../extensions/lens-extension";
import { listTarEntries, readFileFromTar } from "../../../../../common/utils";
import { manifestFilename } from "../../../../../extensions/extension-discovery";
import path from "path";
export const validatePackage = async (
filePath: string,
): Promise<LensExtensionManifest> => {
const tarFiles = await listTarEntries(filePath);
// tarball from npm contains single root folder "package/*"
const firstFile = tarFiles[0];
if (!firstFile) {
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
}
const rootFolder = path.normalize(firstFile).split(path.sep)[0];
const packedInRootFolder = tarFiles.every(entry =>
entry.startsWith(rootFolder),
);
const manifestLocation = packedInRootFolder
? path.join(rootFolder, manifestFilename)
: manifestFilename;
if (!tarFiles.includes(manifestLocation)) {
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
}
const manifest = await readFileFromTar<LensExtensionManifest>({
tarPath: filePath,
filePath: manifestLocation,
parseJson: true,
});
if (!manifest.main && !manifest.renderer) {
throw new Error(
`${manifestFilename} must specify "main" and/or "renderer" fields`,
);
}
return manifest;
};

View File

@ -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 type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { attemptInstalls, Dependencies } from "./attempt-installs";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
const attemptInstallsInjectable: Injectable<
(filePaths: string[]) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
attemptInstall: di.inject(attemptInstallInjectable),
}),
instantiate: attemptInstalls,
lifecycle: lifecycleEnum.singleton,
};
export default attemptInstallsInjectable;

View File

@ -0,0 +1,44 @@
/**
* 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 { readFileNotify } from "../read-file-notify/read-file-notify";
import path from "path";
import type { InstallRequest } from "../attempt-install/install-request";
export interface Dependencies {
attemptInstall: (request: InstallRequest) => Promise<void>;
}
export const attemptInstalls =
({ attemptInstall }: Dependencies) =>
async (filePaths: string[]): Promise<void> => {
const promises: Promise<void>[] = [];
for (const filePath of filePaths) {
promises.push(
attemptInstall({
fileName: path.basename(filePath),
dataP: readFileNotify(filePath),
}),
);
}
await Promise.allSettled(promises);
};

View File

@ -0,0 +1,42 @@
/**
* 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 { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import {
confirmUninstallExtension,
Dependencies,
} from "./confirm-uninstall-extension";
import type { InstalledExtension } from "../../../../extensions/extension-discovery";
import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable";
const confirmUninstallExtensionInjectable: Injectable<
(extension: InstalledExtension) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
uninstallExtension: di.inject(uninstallExtensionInjectable),
}),
instantiate: confirmUninstallExtension,
lifecycle: lifecycleEnum.singleton,
};
export default confirmUninstallExtensionInjectable;

View File

@ -0,0 +1,51 @@
/**
* 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 type { InstalledExtension } from "../../../../extensions/extension-discovery";
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import { extensionDisplayName } from "../../../../extensions/lens-extension";
import { ConfirmDialog } from "../../confirm-dialog";
export interface Dependencies {
uninstallExtension: (id: LensExtensionId) => Promise<void>;
}
export const confirmUninstallExtension =
({ uninstallExtension }: Dependencies) =>
async (extension: InstalledExtension): Promise<void> => {
const displayName = extensionDisplayName(
extension.manifest.name,
extension.manifest.version,
);
const confirmed = await ConfirmDialog.confirm({
message: (
<p>
Are you sure you want to uninstall extension <b>{displayName}</b>?
</p>
),
labelOk: "Yes",
labelCancel: "No",
});
if (confirmed) {
await uninstallExtension(extension.id);
}
};

View File

@ -0,0 +1,40 @@
/**
* 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 { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import { Dependencies, disableExtension } from "./disable-extension";
const disableExtensionInjectable: Injectable<
(id: LensExtensionId) => void,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: disableExtension,
lifecycle: lifecycleEnum.singleton,
};
export default disableExtensionInjectable;

View File

@ -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 type { LensExtensionId } from "../../../../extensions/lens-extension";
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
export interface Dependencies {
extensionLoader: ExtensionLoader;
}
export const disableExtension =
({ extensionLoader }: Dependencies) =>
(id: LensExtensionId) => {
const extension = extensionLoader.getExtension(id);
if (extension) {
extension.isEnabled = false;
}
};

View File

@ -0,0 +1,40 @@
/**
* 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 { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import { Dependencies, enableExtension } from "./enable-extension";
const enableExtensionInjectable: Injectable<
(id: LensExtensionId) => void,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: enableExtension,
lifecycle: lifecycleEnum.singleton,
};
export default enableExtensionInjectable;

View File

@ -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 type { LensExtensionId } from "../../../../extensions/lens-extension";
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
export interface Dependencies {
extensionLoader: ExtensionLoader;
}
export const enableExtension =
({ extensionLoader }: Dependencies) =>
(id: LensExtensionId) => {
const extension = extensionLoader.getExtension(id);
if (extension) {
extension.isEnabled = true;
}
};

View File

@ -20,474 +20,49 @@
*/
import "./extensions.scss";
import { shell } from "electron";
import fse from "fs-extra";
import _ from "lodash";
import { makeObservable, observable, reaction, when } from "mobx";
import {
IComputedValue,
makeObservable,
observable,
reaction,
when,
} from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import os from "os";
import path from "path";
import React from "react";
import { SemVer } from "semver";
import URLParse from "url-parse";
import { Disposer, disposer, downloadFile, downloadJson, ExtendableDisposer, extractTar, listTarEntries, noop, readFileFromTar } from "../../../common/utils";
import { ExtensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery";
import { ExtensionLoader } from "../../../extensions/extension-loader";
import { extensionDisplayName, LensExtensionId, LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension";
import logger from "../../../main/logger";
import { Button } from "../button";
import { ConfirmDialog } from "../confirm-dialog";
import { DropFileInput, InputValidators } from "../input";
import { Notifications } from "../notifications";
import { ExtensionInstallationState, ExtensionInstallationStateStore } from "./extension-install.store";
import type { InstalledExtension } from "../../../extensions/extension-discovery";
import { DropFileInput } from "../input";
import { ExtensionInstallationStateStore } from "./extension-install.store";
import { Install } from "./install";
import { InstalledExtensions } from "./installed-extensions";
import { Notice } from "./notice";
import { SettingLayout } from "../layout/setting-layout";
import { docsUrl } from "../../../common/vars";
import { dialog } from "../../remote-helpers";
import { AppPaths } from "../../../common/app-paths";
function getMessageFromError(error: any): string {
if (!error || typeof error !== "object") {
return "an error has occurred";
}
if (error.message) {
return String(error.message);
}
if (error.err) {
return String(error.err);
}
const rawMessage = String(error);
if (rawMessage === String({})) {
return "an error has occurred";
}
return rawMessage;
}
interface ExtensionInfo {
name: string;
version?: string;
requireConfirmation?: boolean;
}
interface InstallRequest {
fileName: string;
dataP: Promise<Buffer | null>;
}
interface InstallRequestValidated {
fileName: string;
data: Buffer;
id: LensExtensionId;
manifest: LensExtensionManifest;
tempFile: string; // temp system path to packed extension for unpacking
}
function setExtensionEnabled(id: LensExtensionId, isEnabled: boolean): void {
const extension = ExtensionLoader.getInstance().getExtension(id);
if (extension) {
extension.isEnabled = isEnabled;
}
}
function enableExtension(id: LensExtensionId) {
setExtensionEnabled(id, true);
}
function disableExtension(id: LensExtensionId) {
setExtensionEnabled(id, false);
}
async function uninstallExtension(extensionId: LensExtensionId): Promise<boolean> {
const loader = ExtensionLoader.getInstance();
const { manifest } = loader.getExtension(extensionId);
const displayName = extensionDisplayName(manifest.name, manifest.version);
try {
logger.debug(`[EXTENSIONS]: trying to uninstall ${extensionId}`);
ExtensionInstallationStateStore.setUninstalling(extensionId);
await ExtensionDiscovery.getInstance().uninstallExtension(extensionId);
// wait for the ExtensionLoader to actually uninstall the extension
await when(() => !loader.userExtensions.has(extensionId));
Notifications.ok(
<p>Extension <b>{displayName}</b> successfully uninstalled!</p>,
);
return true;
} catch (error) {
const message = getMessageFromError(error);
logger.info(`[EXTENSION-UNINSTALL]: uninstalling ${displayName} has failed: ${error}`, { error });
Notifications.error(<p>Uninstalling extension <b>{displayName}</b> has failed: <em>{message}</em></p>);
return false;
} finally {
// Remove uninstall state on uninstall failure
ExtensionInstallationStateStore.clearUninstalling(extensionId);
}
}
async function confirmUninstallExtension(extension: InstalledExtension): Promise<void> {
const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
const confirmed = await ConfirmDialog.confirm({
message: <p>Are you sure you want to uninstall extension <b>{displayName}</b>?</p>,
labelOk: "Yes",
labelCancel: "No",
});
if (confirmed) {
await uninstallExtension(extension.id);
}
}
function getExtensionDestFolder(name: string) {
return path.join(ExtensionDiscovery.getInstance().localFolderPath, sanitizeExtensionName(name));
}
function getExtensionPackageTemp(fileName = "") {
return path.join(os.tmpdir(), "lens-extensions", fileName);
}
async function readFileNotify(filePath: string, showError = true): Promise<Buffer | null> {
try {
return await fse.readFile(filePath);
} catch (error) {
if (showError) {
const message = getMessageFromError(error);
logger.info(`[EXTENSION-INSTALL]: preloading ${filePath} has failed: ${message}`, { error });
Notifications.error(`Error while reading "${filePath}": ${message}`);
}
}
return null;
}
async function validatePackage(filePath: string): Promise<LensExtensionManifest> {
const tarFiles = await listTarEntries(filePath);
// tarball from npm contains single root folder "package/*"
const firstFile = tarFiles[0];
if (!firstFile) {
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
}
const rootFolder = path.normalize(firstFile).split(path.sep)[0];
const packedInRootFolder = tarFiles.every(entry => entry.startsWith(rootFolder));
const manifestLocation = packedInRootFolder ? path.join(rootFolder, manifestFilename) : manifestFilename;
if (!tarFiles.includes(manifestLocation)) {
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
}
const manifest = await readFileFromTar<LensExtensionManifest>({
tarPath: filePath,
filePath: manifestLocation,
parseJson: true,
});
if (!manifest.main && !manifest.renderer) {
throw new Error(`${manifestFilename} must specify "main" and/or "renderer" fields`);
}
return manifest;
}
async function createTempFilesAndValidate({ fileName, dataP }: InstallRequest): Promise<InstallRequestValidated | null> {
// copy files to temp
await fse.ensureDir(getExtensionPackageTemp());
// validate packages
const tempFile = getExtensionPackageTemp(fileName);
try {
const data = await dataP;
if (!data) {
return null;
}
await fse.writeFile(tempFile, data);
const manifest = await validatePackage(tempFile);
const id = path.join(ExtensionDiscovery.getInstance().nodeModulesPath, manifest.name, "package.json");
return {
fileName,
data,
manifest,
tempFile,
id,
};
} catch (error) {
const message = getMessageFromError(error);
logger.info(`[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`, { error });
Notifications.error(
<div className="flex column gaps">
<p>Installing <em>{fileName}</em> has failed, skipping.</p>
<p>Reason: <em>{message}</em></p>
</div>,
);
}
return null;
}
async function unpackExtension(request: InstallRequestValidated, disposeDownloading?: Disposer) {
const { id, fileName, tempFile, manifest: { name, version }} = request;
ExtensionInstallationStateStore.setInstalling(id);
disposeDownloading?.();
const displayName = extensionDisplayName(name, version);
const extensionFolder = getExtensionDestFolder(name);
const unpackingTempFolder = path.join(path.dirname(tempFile), `${path.basename(tempFile)}-unpacked`);
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
try {
// extract to temp folder first
await fse.remove(unpackingTempFolder).catch(noop);
await fse.ensureDir(unpackingTempFolder);
await extractTar(tempFile, { cwd: unpackingTempFolder });
// move contents to extensions folder
const unpackedFiles = await fse.readdir(unpackingTempFolder);
let unpackedRootFolder = unpackingTempFolder;
if (unpackedFiles.length === 1) {
// check if %extension.tgz was packed with single top folder,
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
}
await fse.ensureDir(extensionFolder);
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
// wait for the loader has actually install it
await when(() => ExtensionLoader.getInstance().userExtensions.has(id));
// Enable installed extensions by default.
ExtensionLoader.getInstance().setIsEnabled(id, true);
Notifications.ok(
<p>Extension <b>{displayName}</b> successfully installed!</p>,
);
} catch (error) {
const message = getMessageFromError(error);
logger.info(`[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`, { error });
Notifications.error(<p>Installing extension <b>{displayName}</b> has failed: <em>{message}</em></p>);
} finally {
// Remove install state once finished
ExtensionInstallationStateStore.clearInstalling(id);
// clean up
fse.remove(unpackingTempFolder).catch(noop);
fse.unlink(tempFile).catch(noop);
}
}
export async function attemptInstallByInfo({ name, version, requireConfirmation = false }: ExtensionInfo) {
const disposer = ExtensionInstallationStateStore.startPreInstall();
const registryUrl = new URLParse("https://registry.npmjs.com").set("pathname", name).toString();
const { promise } = downloadJson({ url: registryUrl });
const json = await promise.catch(console.error);
if (!json || json.error || typeof json.versions !== "object" || !json.versions) {
const message = json?.error ? `: ${json.error}` : "";
Notifications.error(`Failed to get registry information for that extension${message}`);
return disposer();
}
if (version) {
if (!json.versions[version]) {
if (json["dist-tags"][version]) {
version = json["dist-tags"][version];
} else {
Notifications.error(<p>The <em>{name}</em> extension does not have a version or tag <code>{version}</code>.</p>);
return disposer();
}
}
} else {
const versions = Object.keys(json.versions)
.map(version => new SemVer(version, { loose: true, includePrerelease: true }))
// ignore pre-releases for auto picking the version
.filter(version => version.prerelease.length === 0);
version = _.reduce(versions, (prev, curr) => (
prev.compareMain(curr) === -1
? curr
: prev
)).format();
}
if (requireConfirmation) {
const proceed = await ConfirmDialog.confirm({
message: <p>Are you sure you want to install <b>{name}@{version}</b>?</p>,
labelCancel: "Cancel",
labelOk: "Install",
});
if (!proceed) {
return disposer();
}
}
const url = json.versions[version].dist.tarball;
const fileName = path.basename(url);
const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 });
return attemptInstall({ fileName, dataP }, disposer);
}
async function attemptInstall(request: InstallRequest, d?: ExtendableDisposer): Promise<void> {
const dispose = disposer(ExtensionInstallationStateStore.startPreInstall(), d);
const validatedRequest = await createTempFilesAndValidate(request);
if (!validatedRequest) {
return dispose();
}
const { name, version, description } = validatedRequest.manifest;
const curState = ExtensionInstallationStateStore.getInstallationState(validatedRequest.id);
if (curState !== ExtensionInstallationState.IDLE) {
dispose();
return void Notifications.error(
<div className="flex column gaps">
<b>Extension Install Collision:</b>
<p>The <em>{name}</em> extension is currently {curState.toLowerCase()}.</p>
<p>Will not proceed with this current install request.</p>
</div>,
);
}
const extensionFolder = getExtensionDestFolder(name);
const folderExists = await fse.pathExists(extensionFolder);
if (!folderExists) {
// install extension if not yet exists
await unpackExtension(validatedRequest, dispose);
} else {
const { manifest: { version: oldVersion }} = ExtensionLoader.getInstance().getExtension(validatedRequest.id);
// otherwise confirmation required (re-install / update)
const removeNotification = Notifications.info(
<div className="InstallingExtensionNotification flex gaps align-center">
<div className="flex column gaps">
<p>Install extension <b>{name}@{version}</b>?</p>
<p>Description: <em>{description}</em></p>
<div className="remove-folder-warning" onClick={() => shell.openPath(extensionFolder)}>
<b>Warning:</b> {name}@{oldVersion} will be removed before installation.
</div>
</div>
<Button autoFocus label="Install" onClick={async () => {
removeNotification();
if (await uninstallExtension(validatedRequest.id)) {
await unpackExtension(validatedRequest, dispose);
} else {
dispose();
}
}}/>
</div>,
{
onClose: dispose,
},
);
}
}
async function attemptInstalls(filePaths: string[]): Promise<void> {
const promises: Promise<void>[] = [];
for (const filePath of filePaths) {
promises.push(attemptInstall({
fileName: path.basename(filePath),
dataP: readFileNotify(filePath),
}));
}
await Promise.allSettled(promises);
}
async function installOnDrop(files: File[]) {
logger.info("Install from D&D");
await attemptInstalls(files.map(({ path }) => path));
}
async function installFromInput(input: string) {
let disposer: ExtendableDisposer | undefined = undefined;
try {
// fixme: improve error messages for non-tar-file URLs
if (InputValidators.isUrl.validate(input)) {
// install via url
disposer = ExtensionInstallationStateStore.startPreInstall();
const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 });
const fileName = path.basename(input);
await attemptInstall({ fileName, dataP: promise }, disposer);
} else if (InputValidators.isPath.validate(input)) {
// install from system path
const fileName = path.basename(input);
await attemptInstall({ fileName, dataP: readFileNotify(input) });
} else if (InputValidators.isExtensionNameInstall.validate(input)) {
const [{ groups: { name, version }}] = [...input.matchAll(InputValidators.isExtensionNameInstallRegex)];
await attemptInstallByInfo({ name, version });
}
} catch (error) {
const message = getMessageFromError(error);
logger.info(`[EXTENSION-INSTALL]: installation has failed: ${message}`, { error, installPath: input });
Notifications.error(<p>Installation has failed: <b>{message}</b></p>);
} finally {
disposer?.();
}
}
const supportedFormats = ["tar", "tgz"];
async function installFromSelectFileDialog() {
const { canceled, filePaths } = await dialog.showOpenDialog({
defaultPath: AppPaths.get("downloads"),
properties: ["openFile", "multiSelections"],
message: `Select extensions to install (formats: ${supportedFormats.join(", ")}), `,
buttonLabel: "Use configuration",
filters: [
{ name: "tarball", extensions: supportedFormats },
],
});
if (!canceled) {
await attemptInstalls(filePaths);
}
}
import { withInjectables } from "@ogre-tools/injectable-react";
import userExtensionsInjectable from "./user-extensions/user-extensions.injectable";
import enableExtensionInjectable from "./enable-extension/enable-extension.injectable";
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension/confirm-uninstall-extension.injectable";
import installFromInputInjectable from "./install-from-input/install-from-input.injectable";
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog/install-from-select-file-dialog.injectable";
import type { LensExtensionId } from "../../../extensions/lens-extension";
import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable";
import { supportedExtensionFormats } from "./supported-extension-formats";
interface Props {
dependencies: {
userExtensions: IComputedValue<InstalledExtension[]>;
enableExtension: (id: LensExtensionId) => void;
disableExtension: (id: LensExtensionId) => void;
confirmUninstallExtension: (extension: InstalledExtension) => Promise<void>;
installFromInput: (input: string) => Promise<void>;
installFromSelectFileDialog: () => Promise<void>;
installOnDrop: (files: File[]) => Promise<void>;
};
}
@observer
export class Extensions extends React.Component<Props> {
class NonInjectedExtensions extends React.Component<Props> {
@observable installPath = "";
constructor(props: Props) {
@ -495,9 +70,13 @@ export class Extensions extends React.Component<Props> {
makeObservable(this);
}
get dependencies() {
return this.props.dependencies;
}
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => ExtensionLoader.getInstance().userExtensions.size, (curSize, prevSize) => {
reaction(() => this.dependencies.userExtensions.get().length, (curSize, prevSize) => {
if (curSize > prevSize) {
disposeOnUnmount(this, [
when(() => !ExtensionInstallationStateStore.anyInstalling, () => this.installPath = ""),
@ -508,10 +87,10 @@ export class Extensions extends React.Component<Props> {
}
render() {
const extensions = Array.from(ExtensionLoader.getInstance().userExtensions.values());
const userExtensions = this.dependencies.userExtensions.get();
return (
<DropFileInput onDropFiles={installOnDrop}>
<DropFileInput onDropFiles={this.dependencies.installOnDrop}>
<SettingLayout className="Extensions" contentGaps={false}>
<section>
<h1>Extensions</h1>
@ -525,20 +104,20 @@ export class Extensions extends React.Component<Props> {
</Notice>
<Install
supportedFormats={supportedFormats}
onChange={(value) => this.installPath = value}
installFromInput={() => installFromInput(this.installPath)}
installFromSelectFileDialog={installFromSelectFileDialog}
supportedFormats={supportedExtensionFormats}
onChange={value => (this.installPath = value)}
installFromInput={() => this.dependencies.installFromInput(this.installPath)}
installFromSelectFileDialog={this.dependencies.installFromSelectFileDialog}
installPath={this.installPath}
/>
{extensions.length > 0 && <hr/>}
{userExtensions.length > 0 && <hr />}
<InstalledExtensions
extensions={extensions}
enable={enableExtension}
disable={disableExtension}
uninstall={confirmUninstallExtension}
extensions={userExtensions}
enable={this.dependencies.enableExtension}
disable={this.dependencies.disableExtension}
uninstall={this.dependencies.confirmUninstallExtension}
/>
</section>
</SettingLayout>
@ -546,3 +125,21 @@ export class Extensions extends React.Component<Props> {
);
}
}
export const Extensions = withInjectables(NonInjectedExtensions, {
getProps: di => ({
dependencies: {
userExtensions: di.inject(userExtensionsInjectable),
enableExtension: di.inject(enableExtensionInjectable),
disableExtension: di.inject(disableExtensionInjectable),
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
installFromInput: di.inject(installFromInputInjectable),
installOnDrop: di.inject(installOnDropInjectable),
installFromSelectFileDialog: di.inject(
installFromSelectFileDialogInjectable,
),
},
}),
});

View File

@ -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.
*/
export function getMessageFromError(error: any): string {
if (!error || typeof error !== "object") {
return "an error has occurred";
}
if (error.message) {
return String(error.message);
}
if (error.err) {
return String(error.err);
}
const rawMessage = String(error);
if (rawMessage === String({})) {
return "an error has occurred";
}
return rawMessage;
}

View File

@ -0,0 +1,42 @@
/**
* 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 { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
import type { Dependencies } from "./install-from-input";
import { installFromInput } from "./install-from-input";
import attemptInstallByInfoInjectable
from "../attempt-install-by-info/attempt-install-by-info.injectable";
const installFromInputInjectable: Injectable<
(input: string) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
attemptInstall: di.inject(attemptInstallInjectable),
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
}),
instantiate: installFromInput,
lifecycle: lifecycleEnum.singleton,
};
export default installFromInputInjectable;

View File

@ -0,0 +1,68 @@
/**
* 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 { downloadFile, ExtendableDisposer } from "../../../../common/utils";
import { InputValidators } from "../../input";
import { ExtensionInstallationStateStore } from "../extension-install.store";
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
import logger from "../../../../main/logger";
import { Notifications } from "../../notifications";
import path from "path";
import React from "react";
import { readFileNotify } from "../read-file-notify/read-file-notify";
import type { InstallRequest } from "../attempt-install/install-request";
import type { ExtensionInfo } from "../attempt-install-by-info/attempt-install-by-info";
export interface Dependencies {
attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise<void>,
attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise<void>
}
export const installFromInput = ({ attemptInstall, attemptInstallByInfo }: Dependencies) => async (input: string) => {
let disposer: ExtendableDisposer | undefined = undefined;
try {
// fixme: improve error messages for non-tar-file URLs
if (InputValidators.isUrl.validate(input)) {
// install via url
disposer = ExtensionInstallationStateStore.startPreInstall();
const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 });
const fileName = path.basename(input);
await attemptInstall({ fileName, dataP: promise }, disposer);
} else if (InputValidators.isPath.validate(input)) {
// install from system path
const fileName = path.basename(input);
await attemptInstall({ fileName, dataP: readFileNotify(input) });
} else if (InputValidators.isExtensionNameInstall.validate(input)) {
const [{ groups: { name, version }}] = [...input.matchAll(InputValidators.isExtensionNameInstallRegex)];
await attemptInstallByInfo({ name, version });
}
} catch (error) {
const message = getMessageFromError(error);
logger.info(`[EXTENSION-INSTALL]: installation has failed: ${message}`, { error, installPath: input });
Notifications.error(<p>Installation has failed: <b>{message}</b></p>);
} finally {
disposer?.();
}
};

View File

@ -0,0 +1,35 @@
/**
* 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 { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { Dependencies, installFromSelectFileDialog } from "./install-from-select-file-dialog";
import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable";
const installFromSelectFileDialogInjectable: Injectable<() => Promise<void>, Dependencies> = {
getDependencies: di => ({
attemptInstalls: di.inject(attemptInstallsInjectable),
}),
instantiate: installFromSelectFileDialog,
lifecycle: lifecycleEnum.singleton,
};
export default installFromSelectFileDialogInjectable;

View File

@ -0,0 +1,45 @@
/**
* 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 { dialog } from "../../../remote-helpers";
import { AppPaths } from "../../../../common/app-paths";
import { supportedExtensionFormats } from "../supported-extension-formats";
export interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void>
}
export const installFromSelectFileDialog =
({ attemptInstalls }: Dependencies) =>
async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
defaultPath: AppPaths.get("downloads"),
properties: ["openFile", "multiSelections"],
message: `Select extensions to install (formats: ${supportedExtensionFormats.join(
", ",
)}), `,
buttonLabel: "Use configuration",
filters: [{ name: "tarball", extensions: supportedExtensionFormats }],
});
if (!canceled) {
await attemptInstalls(filePaths);
}
};

View File

@ -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 type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { Dependencies, installOnDrop } from "./install-on-drop";
import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable";
const installOnDropInjectable: Injectable<
(files: File[]) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
attemptInstalls: di.inject(attemptInstallsInjectable),
}),
instantiate: installOnDrop,
lifecycle: lifecycleEnum.singleton,
};
export default installOnDropInjectable;

View File

@ -0,0 +1,32 @@
/**
* 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 logger from "../../../../main/logger";
export interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void>;
}
export const installOnDrop =
({ attemptInstalls }: Dependencies) =>
async (files: File[]) => {
logger.info("Install from D&D");
await attemptInstalls(files.map(({ path }) => path));
};

View File

@ -0,0 +1,39 @@
/**
* 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 fse from "fs-extra";
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
import logger from "../../../../main/logger";
import { Notifications } from "../../notifications";
export const readFileNotify = async (filePath: string, showError = true): Promise<Buffer | null> => {
try {
return await fse.readFile(filePath);
} catch (error) {
if (showError) {
const message = getMessageFromError(error);
logger.info(`[EXTENSION-INSTALL]: preloading ${filePath} has failed: ${message}`, { error });
Notifications.error(`Error while reading "${filePath}": ${message}`);
}
}
return null;
};

View File

@ -0,0 +1,21 @@
/**
* 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 const supportedExtensionFormats = ["tar", "tgz"];

View File

@ -0,0 +1,40 @@
/**
* 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 { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import extensionLoaderInjectable
from "../../../../extensions/extension-loader/extension-loader.injectable";
import { Dependencies, uninstallExtension } from "./uninstall-extension";
const uninstallExtensionInjectable: Injectable<
(extensionId: LensExtensionId) => Promise<boolean>,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: uninstallExtension,
lifecycle: lifecycleEnum.singleton,
};
export default uninstallExtensionInjectable;

View File

@ -0,0 +1,76 @@
/**
* 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 { ExtensionLoader } from "../../../../extensions/extension-loader";
import { extensionDisplayName, LensExtensionId } from "../../../../extensions/lens-extension";
import logger from "../../../../main/logger";
import { ExtensionInstallationStateStore } from "../extension-install.store";
import { ExtensionDiscovery } from "../../../../extensions/extension-discovery";
import { Notifications } from "../../notifications";
import React from "react";
import { when } from "mobx";
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
export interface Dependencies {
extensionLoader: ExtensionLoader
}
export const uninstallExtension =
({ extensionLoader }: Dependencies) =>
async (extensionId: LensExtensionId): Promise<boolean> => {
const { manifest } = extensionLoader.getExtension(extensionId);
const displayName = extensionDisplayName(manifest.name, manifest.version);
try {
logger.debug(`[EXTENSIONS]: trying to uninstall ${extensionId}`);
ExtensionInstallationStateStore.setUninstalling(extensionId);
await ExtensionDiscovery.getInstance().uninstallExtension(extensionId);
// wait for the ExtensionLoader to actually uninstall the extension
await when(() => !extensionLoader.userExtensions.has(extensionId));
Notifications.ok(
<p>
Extension <b>{displayName}</b> successfully uninstalled!
</p>,
);
return true;
} catch (error) {
const message = getMessageFromError(error);
logger.info(
`[EXTENSION-UNINSTALL]: uninstalling ${displayName} has failed: ${error}`,
{ error },
);
Notifications.error(
<p>
Uninstalling extension <b>{displayName}</b> has failed:{" "}
<em>{message}</em>
</p>,
);
return false;
} finally {
// Remove uninstall state on uninstall failure
ExtensionInstallationStateStore.clearUninstalling(extensionId);
}
};

View File

@ -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 { Injectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed, IComputedValue } from "mobx";
import type { InstalledExtension } from "../../../../extensions/extension-discovery";
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
const userExtensionsInjectable: Injectable<
IComputedValue<InstalledExtension[]>,
{ extensionLoader: ExtensionLoader }
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
lifecycle: lifecycleEnum.singleton,
instantiate: ({ extensionLoader }) =>
computed(() => [...extensionLoader.userExtensions.values()]),
};
export default userExtensionsInjectable;

View File

@ -20,12 +20,15 @@
*/
import { createContainer } from "@ogre-tools/injectable";
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
export const getDi = () => {
const di: ConfigurableDependencyInjectionContainer = createContainer(
() => require.context("./", true, /\.injectable\.(ts|tsx)$/),
export const getDi = () =>
createContainer(
getRequireContextForRendererCode,
getRequireContextForCommonExtensionCode,
);
return di;
};
const getRequireContextForRendererCode = () =>
require.context("../", true, /\.injectable\.(ts|tsx)$/);
const getRequireContextForCommonExtensionCode = () =>
require.context("../../extensions", true, /\.injectable\.(ts|tsx)$/);

View File

@ -48,6 +48,7 @@ export const getDiForUnitTesting = () => {
return di;
};
const getInjectableFilePaths = memoize(() =>
glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
);
const getInjectableFilePaths = memoize(() => [
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]);

View File

@ -20,4 +20,4 @@
*/
export type { KubeObjectMenuProps } from "./kube-object-menu";
export { KubeObjectMenu } from "./kube-object-menu-container";
export { KubeObjectMenu } from "./kube-object-menu";

View File

@ -1,60 +0,0 @@
/**
* 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 {
KubeObjectMenu,
KubeObjectMenuDependencies,
KubeObjectMenuProps,
} from "./kube-object-menu";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { lifecycleEnum, Injectable } from "@ogre-tools/injectable";
import apiManagerInjectable from "./dependencies/api-manager.injectable";
import clusterNameInjectable from "./dependencies/cluster-name.injectable";
import editResourceTabInjectable from "./dependencies/edit-resource-tab.injectable";
import hideDetailsInjectable from "./dependencies/hide-details.injectable";
import kubeObjectMenuItemsInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-items.injectable";
const KubeObjectMenuInjectable: Injectable<
JSX.Element,
KubeObjectMenuDependencies<KubeObject>,
KubeObjectMenuProps<KubeObject>
> = {
getDependencies: (di, props) => ({
clusterName: di.inject(clusterNameInjectable),
apiManager: di.inject(apiManagerInjectable),
editResourceTab: di.inject(editResourceTabInjectable),
hideDetails: di.inject(hideDetailsInjectable),
kubeObjectMenuItems: di.inject(kubeObjectMenuItemsInjectable, {
kubeObject: props.object,
}),
}),
instantiate: (dependencies, props) => (
<KubeObjectMenu {...dependencies} {...props} />
),
lifecycle: lifecycleEnum.transient,
};
export default KubeObjectMenuInjectable;

View File

@ -25,34 +25,45 @@ import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { MenuActions, MenuActionsProps } from "../menu";
import identity from "lodash/identity";
import type { ApiManager } from "../../../common/k8s-api/api-manager";
import { withInjectables } from "@ogre-tools/injectable-react";
import clusterNameInjectable from "./dependencies/cluster-name.injectable";
import editResourceTabInjectable from "./dependencies/edit-resource-tab.injectable";
import hideDetailsInjectable from "./dependencies/hide-details.injectable";
import kubeObjectMenuItemsInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-items.injectable";
import apiManagerInjectable from "./dependencies/api-manager.injectable";
export interface KubeObjectMenuDependencies<TKubeObject> {
apiManager: ApiManager;
kubeObjectMenuItems: React.ElementType[];
clusterName: string;
hideDetails: () => void;
editResourceTab: (kubeObject: TKubeObject) => void;
}
// TODO: Replace with KubeObjectMenuProps2
export interface KubeObjectMenuProps<TKubeObject> extends MenuActionsProps {
object: TKubeObject | null | undefined;
editable?: boolean;
removable?: boolean;
}
export interface KubeObjectMenuPropsAndDependencies<TKubeObject>
extends KubeObjectMenuProps<TKubeObject>,
KubeObjectMenuDependencies<TKubeObject> {}
interface KubeObjectMenuProps2 extends MenuActionsProps {
object: KubeObject | null | undefined;
editable?: boolean;
removable?: boolean;
dependencies: {
apiManager: ApiManager;
kubeObjectMenuItems: React.ElementType[];
clusterName: string;
hideDetails: () => void;
editResourceTab: (kubeObject: KubeObject) => void;
};
}
class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> {
get dependencies() {
return this.props.dependencies;
}
export class KubeObjectMenu<
TKubeObject extends KubeObject,
> extends React.Component<KubeObjectMenuPropsAndDependencies<TKubeObject>> {
get store() {
const { object } = this.props;
if (!object) return null;
return this.props.apiManager.getStore(object.selfLink);
return this.props.dependencies.apiManager.getStore(object.selfLink);
}
get isEditable() {
@ -65,13 +76,13 @@ export class KubeObjectMenu<
@boundMethod
async update() {
this.props.hideDetails();
this.props.editResourceTab(this.props.object);
this.props.dependencies.hideDetails();
this.props.dependencies.editResourceTab(this.props.object);
}
@boundMethod
async remove() {
this.props.hideDetails();
this.props.dependencies.hideDetails();
const { object, removeAction } = this.props;
if (removeAction) await removeAction();
@ -92,7 +103,7 @@ export class KubeObjectMenu<
return (
<p>
Remove {object.kind} <b>{breadcrumb}</b> from <b>{this.props.clusterName}</b>?
Remove {object.kind} <b>{breadcrumb}</b> from <b>{this.dependencies.clusterName}</b>?
</p>
);
}
@ -100,12 +111,8 @@ export class KubeObjectMenu<
getMenuItems(): React.ReactChild[] {
const { object, toolbar } = this.props;
return this.props.kubeObjectMenuItems.map((MenuItem, index) => (
<MenuItem
object={object}
toolbar={toolbar}
key={`menu-item-${index}`}
/>
return this.props.dependencies.kubeObjectMenuItems.map((MenuItem, index) => (
<MenuItem object={object} toolbar={toolbar} key={`menu-item-${index}`} />
));
}
@ -126,3 +133,20 @@ export class KubeObjectMenu<
);
}
}
export const KubeObjectMenu = withInjectables(NonInjectedKubeObjectMenu, {
getProps: (di, props) => ({
dependencies: {
clusterName: di.inject(clusterNameInjectable),
apiManager: di.inject(apiManagerInjectable),
editResourceTab: di.inject(editResourceTabInjectable),
hideDetails: di.inject(hideDetailsInjectable),
kubeObjectMenuItems: di.inject(kubeObjectMenuItemsInjectable, {
kubeObject: props.object,
}),
},
...props,
}),
});

View File

@ -20,11 +20,11 @@
*/
import { ipcRendererOn } from "../../common/ipc";
import { ExtensionLoader } from "../../extensions/extension-loader";
import type { ExtensionLoader } from "../../extensions/extension-loader";
import type { LensRendererExtension } from "../../extensions/lens-renderer-extension";
export function initIpcRendererListeners() {
export function initIpcRendererListeners(extensionLoader: ExtensionLoader) {
ipcRendererOn("extension:navigate", (event, extId: string, pageId ?: string, params?: Record<string, any>) => {
ExtensionLoader.getInstance().getInstanceById<LensRendererExtension>(extId).navigate(pageId, params);
extensionLoader.getInstanceById<LensRendererExtension>(extId).navigate(pageId, params);
});
}

View File

@ -1,116 +0,0 @@
/**
* 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 { attemptInstallByInfo } from "../components/+extensions";
import { LensProtocolRouterRenderer } from "./router";
import { navigate } from "../navigation/helpers";
import { catalogEntityRegistry } from "../api/catalog-entity-registry";
import { ClusterStore } from "../../common/cluster-store";
import { EXTENSION_NAME_MATCH, EXTENSION_PUBLISHER_MATCH, LensProtocolRouter } from "../../common/protocol-handler";
import { Notifications } from "../components/notifications";
import * as routes from "../../common/routes";
export function bindProtocolAddRouteHandlers() {
LensProtocolRouterRenderer
.getInstance()
.addInternalHandler("/preferences", ({ search: { highlight }}) => {
navigate(routes.preferencesURL({ fragment: highlight }));
})
.addInternalHandler("/", ({ tail }) => {
if (tail) {
Notifications.shortInfo(
<p>
Unknown Action for <code>lens://app/{tail}</code>.{" "}
Are you on the latest version?
</p>,
);
}
navigate(routes.catalogURL());
})
.addInternalHandler("/landing", () => {
navigate(routes.catalogURL());
})
.addInternalHandler("/landing/view/:group/:kind", ({ pathname: { group, kind }}) => {
navigate(routes.catalogURL({
params: {
group, kind,
},
}));
})
.addInternalHandler("/cluster", () => {
navigate(routes.addClusterURL());
})
.addInternalHandler("/entity/:entityId/settings", ({ pathname: { entityId }}) => {
const entity = catalogEntityRegistry.getById(entityId);
if (entity) {
navigate(routes.entitySettingsURL({ params: { entityId }}));
} else {
Notifications.shortInfo(
<p>
Unknown catalog entity <code>{entityId}</code>.
</p>,
);
}
})
// Handlers below are deprecated and only kept for backward compact purposes
.addInternalHandler("/cluster/:clusterId", ({ pathname: { clusterId }}) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
navigate(routes.clusterViewURL({ params: { clusterId }}));
} else {
Notifications.shortInfo(
<p>
Unknown catalog entity <code>{clusterId}</code>.
</p>,
);
}
})
.addInternalHandler("/cluster/:clusterId/settings", ({ pathname: { clusterId }}) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
navigate(routes.entitySettingsURL({ params: { entityId: clusterId }}));
} else {
Notifications.shortInfo(
<p>
Unknown catalog entity <code>{clusterId}</code>.
</p>,
);
}
})
.addInternalHandler("/extensions", () => {
navigate(routes.extensionsURL());
})
.addInternalHandler(`/extensions/install${LensProtocolRouter.ExtensionUrlSchema}`, ({ pathname, search: { version }}) => {
const name = [
pathname[EXTENSION_PUBLISHER_MATCH],
pathname[EXTENSION_NAME_MATCH],
].filter(Boolean)
.join("/");
navigate(routes.extensionsURL());
attemptInstallByInfo({ name, version, requireConfirmation: true });
});
}

View File

@ -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 type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import attemptInstallByInfoInjectable from "../../components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable";
import {
bindProtocolAddRouteHandlers,
Dependencies,
} from "./bind-protocol-add-route-handlers";
import lensProtocolRouterRendererInjectable
from "../lens-protocol-router-renderer/lens-protocol-router-renderer.injectable";
const bindProtocolAddRouteHandlersInjectable: Injectable<() => void, Dependencies> = {
getDependencies: di => ({
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
lensProtocolRouterRenderer: di.inject(lensProtocolRouterRendererInjectable),
}),
instantiate: bindProtocolAddRouteHandlers,
lifecycle: lifecycleEnum.singleton,
};
export default bindProtocolAddRouteHandlersInjectable;

View File

@ -0,0 +1,147 @@
/**
* 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 type { LensProtocolRouterRenderer } from "../lens-protocol-router-renderer/lens-protocol-router-renderer";
import { navigate } from "../../navigation/helpers";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { ClusterStore } from "../../../common/cluster-store";
import {
EXTENSION_NAME_MATCH,
EXTENSION_PUBLISHER_MATCH,
LensProtocolRouter,
} from "../../../common/protocol-handler";
import { Notifications } from "../../components/notifications";
import * as routes from "../../../common/routes";
import type { ExtensionInfo } from "../../components/+extensions/attempt-install-by-info/attempt-install-by-info";
export interface Dependencies {
attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise<void>;
lensProtocolRouterRenderer: LensProtocolRouterRenderer;
}
export const bindProtocolAddRouteHandlers =
({ attemptInstallByInfo, lensProtocolRouterRenderer }: Dependencies) =>
() => {
lensProtocolRouterRenderer
.addInternalHandler("/preferences", ({ search: { highlight }}) => {
navigate(routes.preferencesURL({ fragment: highlight }));
})
.addInternalHandler("/", ({ tail }) => {
if (tail) {
Notifications.shortInfo(
<p>
Unknown Action for <code>lens://app/{tail}</code>. Are you on the
latest version?
</p>,
);
}
navigate(routes.catalogURL());
})
.addInternalHandler("/landing", () => {
navigate(routes.catalogURL());
})
.addInternalHandler(
"/landing/view/:group/:kind",
({ pathname: { group, kind }}) => {
navigate(
routes.catalogURL({
params: {
group,
kind,
},
}),
);
},
)
.addInternalHandler("/cluster", () => {
navigate(routes.addClusterURL());
})
.addInternalHandler(
"/entity/:entityId/settings",
({ pathname: { entityId }}) => {
const entity = catalogEntityRegistry.getById(entityId);
if (entity) {
navigate(routes.entitySettingsURL({ params: { entityId }}));
} else {
Notifications.shortInfo(
<p>
Unknown catalog entity <code>{entityId}</code>.
</p>,
);
}
},
)
// Handlers below are deprecated and only kept for backward compact purposes
.addInternalHandler(
"/cluster/:clusterId",
({ pathname: { clusterId }}) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
navigate(routes.clusterViewURL({ params: { clusterId }}));
} else {
Notifications.shortInfo(
<p>
Unknown catalog entity <code>{clusterId}</code>.
</p>,
);
}
},
)
.addInternalHandler(
"/cluster/:clusterId/settings",
({ pathname: { clusterId }}) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
navigate(
routes.entitySettingsURL({ params: { entityId: clusterId }}),
);
} else {
Notifications.shortInfo(
<p>
Unknown catalog entity <code>{clusterId}</code>.
</p>,
);
}
},
)
.addInternalHandler("/extensions", () => {
navigate(routes.extensionsURL());
})
.addInternalHandler(
`/extensions/install${LensProtocolRouter.ExtensionUrlSchema}`,
({ pathname, search: { version }}) => {
const name = [
pathname[EXTENSION_PUBLISHER_MATCH],
pathname[EXTENSION_NAME_MATCH],
]
.filter(Boolean)
.join("/");
navigate(routes.extensionsURL());
attemptInstallByInfo({ name, version, requireConfirmation: true });
},
);
};

View File

@ -19,5 +19,5 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export * from "./router";
export * from "./app-handlers";
export { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer/lens-protocol-router-renderer";
export { bindProtocolAddRouteHandlers } from "./bind-protocol-add-route-handlers/bind-protocol-add-route-handlers";

View File

@ -0,0 +1,40 @@
/**
* 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 { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
import type { Dependencies } from "./lens-protocol-router-renderer";
import { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer";
const lensProtocolRouterRendererInjectable: Injectable<
LensProtocolRouterRenderer,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: dependencies => new LensProtocolRouterRenderer(dependencies),
lifecycle: lifecycleEnum.singleton,
};
export default lensProtocolRouterRendererInjectable;

View File

@ -21,11 +21,12 @@
import React from "react";
import { ipcRenderer } from "electron";
import * as proto from "../../common/protocol-handler";
import * as proto from "../../../common/protocol-handler";
import Url from "url-parse";
import { onCorrect } from "../../common/ipc";
import { foldAttemptResults, ProtocolHandlerInvalid, RouteAttempt } from "../../common/protocol-handler";
import { Notifications } from "../components/notifications";
import { onCorrect } from "../../../common/ipc";
import { foldAttemptResults, ProtocolHandlerInvalid, RouteAttempt } from "../../../common/protocol-handler";
import { Notifications } from "../../components/notifications";
import type { ExtensionLoader } from "../../../extensions/extension-loader";
function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] {
if (args.length !== 2) {
@ -46,7 +47,16 @@ function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] {
}
}
export interface Dependencies {
extensionLoader: ExtensionLoader
}
export class LensProtocolRouterRenderer extends proto.LensProtocolRouter {
constructor(protected dependencies: Dependencies) {
super(dependencies);
}
/**
* This function is needed to be called early on in the renderers lifetime.
*/

View File

@ -28,10 +28,9 @@ import { ClusterManager } from "./components/cluster-manager";
import { ErrorBoundary } from "./components/error-boundary";
import { Notifications } from "./components/notifications";
import { ConfirmDialog } from "./components/confirm-dialog";
import { ExtensionLoader } from "../extensions/extension-loader";
import type { ExtensionLoader } from "../extensions/extension-loader";
import { broadcastMessage } from "../common/ipc";
import { CommandContainer } from "./components/command-palette/command-container";
import { bindProtocolAddRouteHandlers, LensProtocolRouterRenderer } from "./protocol-handler";
import { registerIpcListeners } from "./ipc";
import { ipcRenderer } from "electron";
import { IpcRendererNavigationEvents } from "./navigation/events";
@ -39,6 +38,7 @@ import { catalogEntityRegistry } from "./api/catalog-entity-registry";
import logger from "../common/logger";
import { unmountComponentAtNode } from "react-dom";
import { ClusterFrameHandler } from "./components/cluster-manager/lens-views";
import type { LensProtocolRouterRenderer } from "./protocol-handler";
injectSystemCAs();
@ -47,10 +47,15 @@ export class RootFrame extends React.Component {
static readonly logPrefix = "[ROOT-FRAME]:";
static displayName = "RootFrame";
static async init(rootElem: HTMLElement) {
static async init(
rootElem: HTMLElement,
extensionLoader: ExtensionLoader,
bindProtocolAddRouteHandlers: () => void,
lensProtocolRouterRendererInjectable: LensProtocolRouterRenderer,
) {
catalogEntityRegistry.init();
ExtensionLoader.getInstance().loadOnClusterManagerRenderer();
LensProtocolRouterRenderer.createInstance().init();
extensionLoader.loadOnClusterManagerRenderer();
lensProtocolRouterRendererInjectable.init();
bindProtocolAddRouteHandlers();
window.addEventListener("offline", () => broadcastMessage("network:offline"));

View File

@ -972,28 +972,28 @@
"@nodelib/fs.scandir" "2.1.3"
fastq "^1.6.0"
"@ogre-tools/fp@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-1.0.2.tgz#26c2c5cf60aa01cc94763cc68beba7052fdadfd9"
integrity sha512-ftvi/aoi5PaojWnuhHzp0YiecUd22HzW5gErsSiKyO2bps90WI4WjgY6d9hWdlzM9eukVmwM+dC6rGNlltNHNw==
"@ogre-tools/fp@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-1.4.0.tgz#94c50378c5bc51ea1571f775e4428256f22c61b5"
integrity sha512-Eh/pK67CoYU/tJPWHeuNFEp+YdE8RPAAxZlSDAoXUDAd8sta3e+1vG7OEJlkYIJW4L8sCGKLWZu2DZ8uI6URhA==
dependencies:
lodash "^4.17.21"
"@ogre-tools/injectable-react@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-1.3.1.tgz#dec3829ac8cf295c32cfe636ca2cd39a495d56ce"
integrity sha512-5jHL9Zcb3QkrttdzqJpN6iCXaV2+fEuDNigwH6NJ3uyV1iQWuRIctnlXxfa9qtZESwaAz7o0hAwkyqEl7YSA4g==
"@ogre-tools/injectable-react@^1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-1.4.1.tgz#48d8633462189939292596a66631d6717e39e47f"
integrity sha512-SRk3QXvFCEQk4MeVG8TAomGcOt0Pf06hZ5kBh+iNIug3FLYeyWagH6OSVylZRu4u2Izd89J0taS1GmSfYDoHaA==
dependencies:
"@ogre-tools/fp" "^1.0.2"
"@ogre-tools/injectable" "^1.3.0"
"@ogre-tools/fp" "^1.4.0"
"@ogre-tools/injectable" "^1.4.1"
lodash "^4.17.21"
"@ogre-tools/injectable@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-1.3.0.tgz#87d329a81575c9345b3af5c1afb0b45537f8f70e"
integrity sha512-rBy8HSExUy1r53ATvk823GXevwultKuSn3mmyRlIj7opJDVRp7Usx0bvOPs+X169jmAZNzsT6HBXbDLXt4Jl4A==
"@ogre-tools/injectable@^1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-1.4.1.tgz#45414c6e13c870d7d84f4fa8e0dd67b33f6cc23e"
integrity sha512-vX4QXS/2d3g7oUenOKcv3mZRnJ5XewUMPsSsELjCyhL2caJlD0eB9J7y3y0eeFu/I18L8GC3DRs9o3QNshwN5Q==
dependencies:
"@ogre-tools/fp" "^1.0.2"
"@ogre-tools/fp" "^1.4.0"
lodash "^4.17.21"
"@panva/asn1.js@^1.0.0":