mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Replace global state usages of ExtensionLoader with DI
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
0b172de741
commit
c92c8f8a11
@ -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]);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
}});
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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}`);
|
||||
|
||||
|
||||
@ -18,12 +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 { getLegacySingleton } from "../../common/di-kludge/get-legacy-singleton/get-legacy-singleton";
|
||||
import extensionLoaderInjectable from "./extension-loader.injectable";
|
||||
|
||||
export * from "./extension-loader";
|
||||
|
||||
/**
|
||||
* @deprecated Switch to using di.inject(extensionLoaderInjectable)
|
||||
*/
|
||||
export const ExtensionLoader = getLegacySingleton(extensionLoaderInjectable);
|
||||
|
||||
@ -21,15 +21,15 @@
|
||||
import { Injectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { computed, IComputedValue } from "mobx";
|
||||
import type { LensExtension } from "./lens-extension";
|
||||
import { ExtensionLoader } from "./extension-loader";
|
||||
import type { ExtensionLoader as ExtensionLoaderType } from "./extension-loader/extension-loader";
|
||||
import type { ExtensionLoader } from "./extension-loader";
|
||||
import extensionLoaderInjectable from "./extension-loader/extension-loader.injectable";
|
||||
|
||||
const extensionsInjectable: Injectable<
|
||||
IComputedValue<LensExtension[]>,
|
||||
{ extensionLoader: ExtensionLoaderType }
|
||||
{ extensionLoader: ExtensionLoader }
|
||||
> = {
|
||||
getDependencies: () => ({
|
||||
extensionLoader: ExtensionLoader.getInstance(),
|
||||
getDependencies: di => ({
|
||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
|
||||
57
src/extensions/getDiForUnitTesting.ts
Normal file
57
src/extensions/getDiForUnitTesting.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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";
|
||||
import { setDiKludge } from "../common/di-kludge/di-kludge";
|
||||
|
||||
export const getDiForUnitTesting = () => {
|
||||
const di: ConfigurableDependencyInjectionContainer = createContainer();
|
||||
|
||||
setDiKludge(di);
|
||||
|
||||
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 }),
|
||||
]);
|
||||
@ -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";
|
||||
@ -68,6 +66,8 @@ 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();
|
||||
|
||||
@ -79,7 +79,6 @@ const onQuitCleanup = disposer();
|
||||
SentryInit();
|
||||
app.setName(appName);
|
||||
|
||||
|
||||
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
|
||||
|
||||
if (app.setAsDefaultProtocolClient("lens")) {
|
||||
@ -111,14 +110,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -126,11 +125,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,9 +224,12 @@ app.on("ready", async () => {
|
||||
return app.exit();
|
||||
}
|
||||
|
||||
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
|
||||
@ -258,7 +258,7 @@ app.on("ready", async () => {
|
||||
await ensureDir(storedKubeConfigFolder());
|
||||
KubeconfigSyncManager.getInstance().startSync();
|
||||
startUpdateChecking();
|
||||
LensProtocolRouterMain.getInstance().rendererLoaded = true;
|
||||
lensProtocolRouterMain.rendererLoaded = true;
|
||||
});
|
||||
|
||||
logger.info("🧩 Initializing extensions");
|
||||
@ -273,13 +273,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);
|
||||
@ -314,7 +314,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" });
|
||||
@ -322,11 +321,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)
|
||||
@ -336,7 +333,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();
|
||||
});
|
||||
|
||||
@ -345,7 +342,7 @@ app.on("open-url", (event, rawUrl) => {
|
||||
|
||||
// lens:// protocol handler
|
||||
event.preventDefault();
|
||||
LensProtocolRouterMain.getInstance().route(rawUrl);
|
||||
lensProtocolRouterMain.route(rawUrl);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -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; });
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
@ -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 }}>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -25,13 +25,16 @@ 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";
|
||||
|
||||
mockWindow();
|
||||
|
||||
@ -73,14 +76,20 @@ jest.mock("electron", () => ({
|
||||
AppPaths.init();
|
||||
|
||||
describe("Extensions", () => {
|
||||
let extensionLoader: ExtensionLoader;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting();
|
||||
|
||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
mockFs({
|
||||
"tmp": {},
|
||||
});
|
||||
|
||||
ExtensionInstallationStateStore.reset();
|
||||
|
||||
ExtensionLoader.createInstance().addExtension({
|
||||
extensionLoader.addExtension({
|
||||
id: "extensionId",
|
||||
manifest: {
|
||||
name: "test",
|
||||
@ -92,7 +101,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 +113,6 @@ describe("Extensions", () => {
|
||||
mockFs.restore();
|
||||
UserStore.resetInstance();
|
||||
ExtensionDiscovery.resetInstance();
|
||||
ExtensionLoader.resetInstance();
|
||||
});
|
||||
|
||||
it("disables uninstall and disable buttons while uninstalling", async () => {
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
};
|
||||
@ -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;
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
@ -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),
|
||||
);
|
||||
4
src/renderer/components/+extensions/attempt-install/install-request.d.ts
vendored
Normal file
4
src/renderer/components/+extensions/attempt-install/install-request.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
export interface InstallRequest {
|
||||
fileName: string;
|
||||
dataP: Promise<Buffer | null>;
|
||||
}
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
@ -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);
|
||||
};
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
@ -20,484 +20,63 @@
|
||||
*/
|
||||
|
||||
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) {
|
||||
super(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,
|
||||
),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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?.();
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
@ -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));
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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"];
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
@ -34,7 +34,7 @@ export const getDi = () => {
|
||||
};
|
||||
|
||||
const getRequireContextForRendererCode = () =>
|
||||
require.context("./", true, /\.injectable\.(ts|tsx)$/);
|
||||
require.context("../", true, /\.injectable\.(ts|tsx)$/);
|
||||
|
||||
const getRequireContextForCommonExtensionCode = () =>
|
||||
require.context("../../extensions", true, /\.injectable\.(ts|tsx)$/);
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
@ -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 });
|
||||
},
|
||||
);
|
||||
};
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
@ -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.
|
||||
*/
|
||||
@ -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"));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user