mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge pull request #4527 from jansav/make-menu-registry-obsolete
Remove MenuRegistry in favour of dependency injection
This commit is contained in:
commit
3418c0acfe
@ -196,8 +196,8 @@
|
|||||||
"@hapi/call": "^8.0.1",
|
"@hapi/call": "^8.0.1",
|
||||||
"@hapi/subtext": "^7.0.3",
|
"@hapi/subtext": "^7.0.3",
|
||||||
"@kubernetes/client-node": "^0.16.1",
|
"@kubernetes/client-node": "^0.16.1",
|
||||||
"@ogre-tools/injectable": "^1.3.0",
|
"@ogre-tools/injectable": "^1.4.1",
|
||||||
"@ogre-tools/injectable-react": "^1.3.1",
|
"@ogre-tools/injectable-react": "^1.4.1",
|
||||||
"@sentry/electron": "^2.5.4",
|
"@sentry/electron": "^2.5.4",
|
||||||
"@sentry/integrations": "^6.15.0",
|
"@sentry/integrations": "^6.15.0",
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
|
|||||||
@ -21,13 +21,13 @@
|
|||||||
|
|
||||||
import { match, matchPath } from "react-router";
|
import { match, matchPath } from "react-router";
|
||||||
import { countBy } from "lodash";
|
import { countBy } from "lodash";
|
||||||
import { iter, Singleton } from "../utils";
|
import { iter } from "../utils";
|
||||||
import { pathToRegexp } from "path-to-regexp";
|
import { pathToRegexp } from "path-to-regexp";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import type Url from "url-parse";
|
import type Url from "url-parse";
|
||||||
import { RoutingError, RoutingErrorType } from "./error";
|
import { RoutingError, RoutingErrorType } from "./error";
|
||||||
import { ExtensionsStore } from "../../extensions/extensions-store";
|
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 { LensExtension } from "../../extensions/lens-extension";
|
||||||
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
|
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
|
||||||
import { when } from "mobx";
|
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
|
// Map between path schemas and the handlers
|
||||||
protected internalRoutes = new Map<string, RouteHandler>();
|
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}`;
|
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
|
* Attempts to route the given URL to all internal routes that have been registered
|
||||||
* @param url the parsed URL that initiated the `lens://` protocol
|
* @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 { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
|
||||||
const name = [publisher, partialName].filter(Boolean).join("/");
|
const name = [publisher, partialName].filter(Boolean).join("/");
|
||||||
const extensionLoader = ExtensionLoader.getInstance();
|
|
||||||
|
const extensionLoader = this.dependencies.extensionLoader;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/**
|
/**
|
||||||
* Note, if `getInstanceByName` returns `null` that means we won't be getting an instance
|
* Note, if `getInstanceByName` returns `null` that means we won't be getting an instance
|
||||||
*/
|
*/
|
||||||
await when(() => extensionLoader.getInstanceByName(name) !== (void 0), { timeout: 5_000 });
|
await when(() => extensionLoader.getInstanceByName(name) !== void 0, {
|
||||||
} catch(error) {
|
timeout: 5_000,
|
||||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`);
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.info(
|
||||||
|
`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`,
|
||||||
|
);
|
||||||
|
|
||||||
return name;
|
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
|
// 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));
|
url.set("pathname", url.pathname.slice(extension.name.length + 1));
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const handlers = iter.map(extension.protocolHandlers, ({ pathSchema, handler }) => [pathSchema, handler] as [string, RouteHandler]);
|
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 os from "os";
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import { AppPaths } from "../../common/app-paths";
|
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);
|
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>;
|
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
||||||
|
|
||||||
describe("ExtensionDiscovery", () => {
|
describe("ExtensionDiscovery", () => {
|
||||||
|
let extensionLoader: ExtensionLoader;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ExtensionDiscovery.resetInstance();
|
ExtensionDiscovery.resetInstance();
|
||||||
ExtensionsStore.resetInstance();
|
ExtensionsStore.resetInstance();
|
||||||
ExtensionsStore.createInstance();
|
ExtensionsStore.createInstance();
|
||||||
|
|
||||||
|
const di = getDiForUnitTesting();
|
||||||
|
|
||||||
|
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with mockFs", () => {
|
describe("with mockFs", () => {
|
||||||
@ -98,7 +107,9 @@ describe("ExtensionDiscovery", () => {
|
|||||||
(mockWatchInstance) as any,
|
(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
|
// Need to force isLoaded to be true so that the file watching is started
|
||||||
extensionDiscovery.isLoaded = true;
|
extensionDiscovery.isLoaded = true;
|
||||||
@ -140,7 +151,9 @@ describe("ExtensionDiscovery", () => {
|
|||||||
mockedWatch.mockImplementationOnce(() =>
|
mockedWatch.mockImplementationOnce(() =>
|
||||||
(mockWatchInstance) as any,
|
(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
|
// Need to force isLoaded to be true so that the file watching is started
|
||||||
extensionDiscovery.isLoaded = true;
|
extensionDiscovery.isLoaded = true;
|
||||||
|
|||||||
@ -19,11 +19,13 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* 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 { ipcRenderer } from "electron";
|
||||||
import { ExtensionsStore } from "../extensions-store";
|
import { ExtensionsStore } from "../extensions-store";
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
@ -128,13 +130,15 @@ jest.mock(
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe("ExtensionLoader", () => {
|
describe("ExtensionLoader", () => {
|
||||||
|
let extensionLoader: ExtensionLoader;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ExtensionLoader.resetInstance();
|
const di = getDiForUnitTesting();
|
||||||
|
|
||||||
|
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only("renderer updates extension after ipc broadcast", async (done) => {
|
it.only("renderer updates extension after ipc broadcast", async done => {
|
||||||
const extensionLoader = ExtensionLoader.createInstance();
|
|
||||||
|
|
||||||
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`);
|
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`);
|
||||||
|
|
||||||
await extensionLoader.init();
|
await extensionLoader.init();
|
||||||
@ -178,8 +182,6 @@ describe("ExtensionLoader", () => {
|
|||||||
// Disable sending events in this test
|
// Disable sending events in this test
|
||||||
(ipcRenderer.on as any).mockImplementation();
|
(ipcRenderer.on as any).mockImplementation();
|
||||||
|
|
||||||
const extensionLoader = ExtensionLoader.createInstance();
|
|
||||||
|
|
||||||
await extensionLoader.init();
|
await extensionLoader.init();
|
||||||
|
|
||||||
expect(ExtensionsStore.getInstance().mergeState).not.toHaveBeenCalled();
|
expect(ExtensionsStore.getInstance().mergeState).not.toHaveBeenCalled();
|
||||||
@ -194,6 +196,7 @@ describe("ExtensionLoader", () => {
|
|||||||
"manifest/path2": {
|
"manifest/path2": {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
name: "TestExtension2",
|
name: "TestExtension2",
|
||||||
}});
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import logger from "../main/logger";
|
|||||||
import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store";
|
import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store";
|
||||||
import { extensionInstaller } from "./extension-installer";
|
import { extensionInstaller } from "./extension-installer";
|
||||||
import { ExtensionsStore } from "./extensions-store";
|
import { ExtensionsStore } from "./extensions-store";
|
||||||
import { ExtensionLoader } from "./extension-loader";
|
import type { ExtensionLoader } from "./extension-loader";
|
||||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||||
import { isProduction } from "../common/vars";
|
import { isProduction } from "../common/vars";
|
||||||
import { isCompatibleBundledExtension, isCompatibleExtension } from "./extension-compatibility";
|
import { isCompatibleBundledExtension, isCompatibleExtension } from "./extension-compatibility";
|
||||||
@ -99,7 +99,7 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
|
|
||||||
public events = new EventEmitter();
|
public events = new EventEmitter();
|
||||||
|
|
||||||
constructor() {
|
constructor(protected extensionLoader: ExtensionLoader) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
@ -277,7 +277,7 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
* @param extensionId The ID of the extension to uninstall.
|
* @param extensionId The ID of the extension to uninstall.
|
||||||
*/
|
*/
|
||||||
async uninstallExtension(extensionId: LensExtensionId): Promise<void> {
|
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}`);
|
logger.info(`${logModule} Uninstalling ${manifest.name}`);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import type { Injectable } from "@ogre-tools/injectable";
|
||||||
|
import { lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { ExtensionLoader } from "./extension-loader";
|
||||||
|
|
||||||
|
const extensionLoaderInjectable: Injectable<ExtensionLoader> = {
|
||||||
|
getDependencies: () => ({}),
|
||||||
|
instantiate: () => new ExtensionLoader(),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default extensionLoaderInjectable;
|
||||||
@ -24,17 +24,16 @@ import { EventEmitter } from "events";
|
|||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { AppPaths } from "../common/app-paths";
|
import { AppPaths } from "../../common/app-paths";
|
||||||
import { ClusterStore } from "../common/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store";
|
||||||
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../common/ipc";
|
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc";
|
||||||
import { Disposer, getHostedClusterId, Singleton, toJS } from "../common/utils";
|
import { Disposer, getHostedClusterId, toJS } from "../../common/utils";
|
||||||
import logger from "../main/logger";
|
import logger from "../../main/logger";
|
||||||
import type { InstalledExtension } from "./extension-discovery";
|
import type { InstalledExtension } from "../extension-discovery";
|
||||||
import { ExtensionsStore } from "./extensions-store";
|
import { ExtensionsStore } from "../extensions-store";
|
||||||
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension";
|
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
|
||||||
import type { LensMainExtension } from "./lens-main-extension";
|
import type { LensRendererExtension } from "../lens-renderer-extension";
|
||||||
import type { LensRendererExtension } from "./lens-renderer-extension";
|
import * as registries from "../registries";
|
||||||
import * as registries from "./registries";
|
|
||||||
|
|
||||||
export function extensionPackagesRoot() {
|
export function extensionPackagesRoot() {
|
||||||
return path.join(AppPaths.get("userData"));
|
return path.join(AppPaths.get("userData"));
|
||||||
@ -45,7 +44,7 @@ const logModule = "[EXTENSIONS-LOADER]";
|
|||||||
/**
|
/**
|
||||||
* Loads installed extensions to the Lens application
|
* Loads installed extensions to the Lens application
|
||||||
*/
|
*/
|
||||||
export class ExtensionLoader extends Singleton {
|
export class ExtensionLoader {
|
||||||
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
||||||
protected instances = observable.map<LensExtensionId, LensExtension>();
|
protected instances = observable.map<LensExtensionId, LensExtension>();
|
||||||
|
|
||||||
@ -77,7 +76,6 @@ export class ExtensionLoader extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
observe(this.instances, change => {
|
observe(this.instances, change => {
|
||||||
switch (change.type) {
|
switch (change.type) {
|
||||||
@ -97,6 +95,10 @@ export class ExtensionLoader extends Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get enabledExtensionInstances() : LensExtension[] {
|
||||||
|
return [...this.instances.values()].filter(extension => extension.isEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
@computed get userExtensions(): Map<LensExtensionId, InstalledExtension> {
|
@computed get userExtensions(): Map<LensExtensionId, InstalledExtension> {
|
||||||
const extensions = this.toJSON();
|
const extensions = this.toJSON();
|
||||||
|
|
||||||
@ -248,25 +250,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadOnMain() {
|
loadOnMain() {
|
||||||
registries.MenuRegistry.createInstance();
|
this.autoInitExtensions(() => Promise.resolve([]));
|
||||||
|
|
||||||
logger.debug(`${logModule}: load on main`);
|
|
||||||
this.autoInitExtensions(async (extension: LensMainExtension) => {
|
|
||||||
// Each .add returns a function to remove the item
|
|
||||||
const removeItems = [
|
|
||||||
registries.MenuRegistry.getInstance().add(extension.appMenus),
|
|
||||||
];
|
|
||||||
|
|
||||||
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
|
||||||
if (removedExtension.id === extension.id) {
|
|
||||||
removeItems.forEach(remove => {
|
|
||||||
remove();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return removeItems;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOnClusterManagerRenderer() {
|
loadOnClusterManagerRenderer() {
|
||||||
@ -18,7 +18,5 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* 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.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import { getInjectedComponent } from "@ogre-tools/injectable-react";
|
|
||||||
import KubeObjectMenuInjectable from "./kube-object-menu.injectable";
|
|
||||||
|
|
||||||
export const KubeObjectMenu = getInjectedComponent(KubeObjectMenuInjectable);
|
export * from "./extension-loader";
|
||||||
41
src/extensions/extensions.injectable.ts
Normal file
41
src/extensions/extensions.injectable.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import { Injectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { computed, IComputedValue } from "mobx";
|
||||||
|
import type { LensExtension } from "./lens-extension";
|
||||||
|
import type { ExtensionLoader } from "./extension-loader";
|
||||||
|
import extensionLoaderInjectable from "./extension-loader/extension-loader.injectable";
|
||||||
|
|
||||||
|
const extensionsInjectable: Injectable<
|
||||||
|
IComputedValue<LensExtension[]>,
|
||||||
|
{ extensionLoader: ExtensionLoader }
|
||||||
|
> = {
|
||||||
|
getDependencies: di => ({
|
||||||
|
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||||
|
}),
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|
||||||
|
instantiate: ({ extensionLoader }) =>
|
||||||
|
computed(() => extensionLoader.enabledExtensionInstances),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default extensionsInjectable;
|
||||||
54
src/extensions/getDiForUnitTesting.ts
Normal file
54
src/extensions/getDiForUnitTesting.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import glob from "glob";
|
||||||
|
import { memoize } from "lodash/fp";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContainer,
|
||||||
|
ConfigurableDependencyInjectionContainer,
|
||||||
|
} from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
export const getDiForUnitTesting = () => {
|
||||||
|
const di: ConfigurableDependencyInjectionContainer = createContainer();
|
||||||
|
|
||||||
|
getInjectableFilePaths()
|
||||||
|
.map(key => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const injectable = require(key).default;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
...injectable,
|
||||||
|
aliases: [injectable, ...(injectable.aliases || [])],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
.forEach(injectable => di.register(injectable));
|
||||||
|
|
||||||
|
di.preventSideEffects();
|
||||||
|
|
||||||
|
return di;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInjectableFilePaths = memoize(() => [
|
||||||
|
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||||
|
]);
|
||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { InstalledExtension } from "./extension-discovery";
|
import type { InstalledExtension } from "./extension-discovery";
|
||||||
import { action, observable, makeObservable } from "mobx";
|
import { action, observable, makeObservable, computed } from "mobx";
|
||||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import type { ProtocolHandlerRegistration } from "./registries";
|
import type { ProtocolHandlerRegistration } from "./registries";
|
||||||
@ -47,7 +47,12 @@ export class LensExtension {
|
|||||||
|
|
||||||
protocolHandlers: ProtocolHandlerRegistration[] = [];
|
protocolHandlers: ProtocolHandlerRegistration[] = [];
|
||||||
|
|
||||||
@observable private isEnabled = false;
|
@observable private _isEnabled = false;
|
||||||
|
|
||||||
|
@computed get isEnabled() {
|
||||||
|
return this._isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
[Disposers] = disposer();
|
[Disposers] = disposer();
|
||||||
|
|
||||||
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
|
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
|
||||||
@ -83,13 +88,13 @@ export class LensExtension {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
async enable(register: (ext: LensExtension) => Promise<Disposer[]>) {
|
async enable(register: (ext: LensExtension) => Promise<Disposer[]>) {
|
||||||
if (this.isEnabled) {
|
if (this._isEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.onActivate();
|
await this.onActivate();
|
||||||
this.isEnabled = true;
|
this._isEnabled = true;
|
||||||
|
|
||||||
this[Disposers].push(...await register(this));
|
this[Disposers].push(...await register(this));
|
||||||
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
|
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
|
||||||
@ -100,11 +105,11 @@ export class LensExtension {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
async disable() {
|
async disable() {
|
||||||
if (!this.isEnabled) {
|
if (!this._isEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isEnabled = false;
|
this._isEnabled = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.onDeactivate();
|
await this.onDeactivate();
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import { WindowManager } from "../main/window-manager";
|
|||||||
import { catalogEntityRegistry } from "../main/catalog";
|
import { catalogEntityRegistry } from "../main/catalog";
|
||||||
import type { CatalogEntity } from "../common/catalog";
|
import type { CatalogEntity } from "../common/catalog";
|
||||||
import type { IObservableArray } from "mobx";
|
import type { IObservableArray } from "mobx";
|
||||||
import type { MenuRegistration } from "./registries";
|
import type { MenuRegistration } from "../main/menu/menu-registration";
|
||||||
|
|
||||||
export class LensMainExtension extends LensExtension {
|
export class LensMainExtension extends LensExtension {
|
||||||
appMenus: MenuRegistration[] = [];
|
appMenus: MenuRegistration[] = [];
|
||||||
|
|||||||
@ -23,7 +23,6 @@
|
|||||||
|
|
||||||
export * from "./page-registry";
|
export * from "./page-registry";
|
||||||
export * from "./page-menu-registry";
|
export * from "./page-menu-registry";
|
||||||
export * from "./menu-registry";
|
|
||||||
export * from "./app-preference-registry";
|
export * from "./app-preference-registry";
|
||||||
export * from "./status-bar-registry";
|
export * from "./status-bar-registry";
|
||||||
export * from "./kube-object-detail-registry";
|
export * from "./kube-object-detail-registry";
|
||||||
|
|||||||
34
src/main/getDi.ts
Normal file
34
src/main/getDi.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createContainer } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
export const getDi = () =>
|
||||||
|
createContainer(
|
||||||
|
getRequireContextForMainCode,
|
||||||
|
getRequireContextForCommonExtensionCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRequireContextForMainCode = () =>
|
||||||
|
require.context("./", true, /\.injectable\.(ts|tsx)$/);
|
||||||
|
|
||||||
|
const getRequireContextForCommonExtensionCode = () =>
|
||||||
|
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/);
|
||||||
55
src/main/getDiForUnitTesting.ts
Normal file
55
src/main/getDiForUnitTesting.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import glob from "glob";
|
||||||
|
import { memoize } from "lodash/fp";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContainer,
|
||||||
|
ConfigurableDependencyInjectionContainer,
|
||||||
|
} from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
export const getDiForUnitTesting = () => {
|
||||||
|
const di: ConfigurableDependencyInjectionContainer = createContainer();
|
||||||
|
|
||||||
|
getInjectableFilePaths()
|
||||||
|
.map(key => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const injectable = require(key).default;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
...injectable,
|
||||||
|
aliases: [injectable, ...(injectable.aliases || [])],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
.forEach(injectable => di.register(injectable));
|
||||||
|
|
||||||
|
di.preventSideEffects();
|
||||||
|
|
||||||
|
return di;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInjectableFilePaths = memoize(() => [
|
||||||
|
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||||
|
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||||
|
]);
|
||||||
@ -36,11 +36,9 @@ import { mangleProxyEnv } from "./proxy-env";
|
|||||||
import { registerFileProtocol } from "../common/register-protocol";
|
import { registerFileProtocol } from "../common/register-protocol";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
import { ExtensionLoader } from "../extensions/extension-loader";
|
|
||||||
import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery";
|
import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery";
|
||||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||||
import { installDeveloperTools } from "./developer-tools";
|
import { installDeveloperTools } from "./developer-tools";
|
||||||
import { LensProtocolRouterMain } from "./protocol-handler";
|
|
||||||
import { disposer, getAppVersion, getAppVersionFromProxyServer, storedKubeConfigFolder } from "../common/utils";
|
import { disposer, getAppVersion, getAppVersionFromProxyServer, storedKubeConfigFolder } from "../common/utils";
|
||||||
import { bindBroadcastHandlers, ipcMainOn } from "../common/ipc";
|
import { bindBroadcastHandlers, ipcMainOn } from "../common/ipc";
|
||||||
import { startUpdateChecking } from "./app-updater";
|
import { startUpdateChecking } from "./app-updater";
|
||||||
@ -61,11 +59,17 @@ import { FilesystemProvisionerStore } from "./extension-filesystem";
|
|||||||
import { SentryInit } from "../common/sentry";
|
import { SentryInit } from "../common/sentry";
|
||||||
import { ensureDir } from "fs-extra";
|
import { ensureDir } from "fs-extra";
|
||||||
import { Router } from "./router";
|
import { Router } from "./router";
|
||||||
import { initMenu } from "./menu";
|
import { initMenu } from "./menu/menu";
|
||||||
import { initTray } from "./tray";
|
import { initTray } from "./tray";
|
||||||
import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions";
|
import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions";
|
||||||
import { AppPaths } from "../common/app-paths";
|
import { AppPaths } from "../common/app-paths";
|
||||||
import { ShellSession } from "./shell-session/shell-session";
|
import { ShellSession } from "./shell-session/shell-session";
|
||||||
|
import { getDi } from "./getDi";
|
||||||
|
import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable";
|
||||||
|
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
||||||
|
import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||||
|
|
||||||
|
const di = getDi();
|
||||||
|
|
||||||
injectSystemCAs();
|
injectSystemCAs();
|
||||||
|
|
||||||
@ -75,7 +79,6 @@ const onQuitCleanup = disposer();
|
|||||||
SentryInit();
|
SentryInit();
|
||||||
app.setName(appName);
|
app.setName(appName);
|
||||||
|
|
||||||
|
|
||||||
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
|
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
|
||||||
|
|
||||||
if (app.setAsDefaultProtocolClient("lens")) {
|
if (app.setAsDefaultProtocolClient("lens")) {
|
||||||
@ -99,7 +102,10 @@ configurePackages();
|
|||||||
mangleProxyEnv();
|
mangleProxyEnv();
|
||||||
|
|
||||||
logger.debug("[APP-MAIN] initializing ipc main handlers");
|
logger.debug("[APP-MAIN] initializing ipc main handlers");
|
||||||
initializers.initIpcMainHandlers();
|
|
||||||
|
const menuItems = di.inject(electronMenuItemsInjectable);
|
||||||
|
|
||||||
|
initializers.initIpcMainHandlers(menuItems);
|
||||||
|
|
||||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
|
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
|
||||||
@ -107,14 +113,14 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
|||||||
|
|
||||||
logger.debug("[APP-MAIN] Lens protocol routing main");
|
logger.debug("[APP-MAIN] Lens protocol routing main");
|
||||||
|
|
||||||
|
const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable);
|
||||||
|
|
||||||
if (!app.requestSingleInstanceLock()) {
|
if (!app.requestSingleInstanceLock()) {
|
||||||
app.exit();
|
app.exit();
|
||||||
} else {
|
} else {
|
||||||
const lprm = LensProtocolRouterMain.createInstance();
|
|
||||||
|
|
||||||
for (const arg of process.argv) {
|
for (const arg of process.argv) {
|
||||||
if (arg.toLowerCase().startsWith("lens://")) {
|
if (arg.toLowerCase().startsWith("lens://")) {
|
||||||
lprm.route(arg);
|
lensProtocolRouterMain.route(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,11 +128,9 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
app.on("second-instance", (event, argv) => {
|
app.on("second-instance", (event, argv) => {
|
||||||
logger.debug("second-instance message");
|
logger.debug("second-instance message");
|
||||||
|
|
||||||
const lprm = LensProtocolRouterMain.createInstance();
|
|
||||||
|
|
||||||
for (const arg of argv) {
|
for (const arg of argv) {
|
||||||
if (arg.toLowerCase().startsWith("lens://")) {
|
if (arg.toLowerCase().startsWith("lens://")) {
|
||||||
lprm.route(arg);
|
lensProtocolRouterMain.route(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,10 +227,12 @@ app.on("ready", async () => {
|
|||||||
return app.exit();
|
return app.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializers.initRegistries();
|
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
const extensionDiscovery = ExtensionDiscovery.createInstance();
|
|
||||||
|
extensionLoader.init();
|
||||||
|
|
||||||
|
const extensionDiscovery = ExtensionDiscovery.createInstance(extensionLoader);
|
||||||
|
|
||||||
ExtensionLoader.createInstance().init();
|
|
||||||
extensionDiscovery.init();
|
extensionDiscovery.init();
|
||||||
|
|
||||||
// Start the app without showing the main window when auto starting on login
|
// Start the app without showing the main window when auto starting on login
|
||||||
@ -237,7 +243,7 @@ app.on("ready", async () => {
|
|||||||
const windowManager = WindowManager.createInstance();
|
const windowManager = WindowManager.createInstance();
|
||||||
|
|
||||||
onQuitCleanup.push(
|
onQuitCleanup.push(
|
||||||
initMenu(windowManager),
|
initMenu(windowManager, menuItems),
|
||||||
initTray(windowManager),
|
initTray(windowManager),
|
||||||
() => ShellSession.cleanup(),
|
() => ShellSession.cleanup(),
|
||||||
);
|
);
|
||||||
@ -253,7 +259,7 @@ app.on("ready", async () => {
|
|||||||
await ensureDir(storedKubeConfigFolder());
|
await ensureDir(storedKubeConfigFolder());
|
||||||
KubeconfigSyncManager.getInstance().startSync();
|
KubeconfigSyncManager.getInstance().startSync();
|
||||||
startUpdateChecking();
|
startUpdateChecking();
|
||||||
LensProtocolRouterMain.getInstance().rendererLoaded = true;
|
lensProtocolRouterMain.rendererLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info("🧩 Initializing extensions");
|
logger.info("🧩 Initializing extensions");
|
||||||
@ -268,13 +274,13 @@ app.on("ready", async () => {
|
|||||||
// Subscribe to extensions that are copied or deleted to/from the extensions folder
|
// Subscribe to extensions that are copied or deleted to/from the extensions folder
|
||||||
extensionDiscovery.events
|
extensionDiscovery.events
|
||||||
.on("add", (extension: InstalledExtension) => {
|
.on("add", (extension: InstalledExtension) => {
|
||||||
ExtensionLoader.getInstance().addExtension(extension);
|
extensionLoader.addExtension(extension);
|
||||||
})
|
})
|
||||||
.on("remove", (lensExtensionId: LensExtensionId) => {
|
.on("remove", (lensExtensionId: LensExtensionId) => {
|
||||||
ExtensionLoader.getInstance().removeExtension(lensExtensionId);
|
extensionLoader.removeExtension(lensExtensionId);
|
||||||
});
|
});
|
||||||
|
|
||||||
ExtensionLoader.getInstance().initExtensions(extensions);
|
extensionLoader.initExtensions(extensions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
|
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -309,7 +315,6 @@ app.on("will-quit", (event) => {
|
|||||||
|
|
||||||
// This is called when the close button of the main window is clicked
|
// This is called when the close button of the main window is clicked
|
||||||
|
|
||||||
const lprm = LensProtocolRouterMain.getInstance(false);
|
|
||||||
|
|
||||||
logger.info("APP:QUIT");
|
logger.info("APP:QUIT");
|
||||||
appEventBus.emit({ name: "app", action: "close" });
|
appEventBus.emit({ name: "app", action: "close" });
|
||||||
@ -317,11 +322,9 @@ app.on("will-quit", (event) => {
|
|||||||
KubeconfigSyncManager.getInstance(false)?.stopSync();
|
KubeconfigSyncManager.getInstance(false)?.stopSync();
|
||||||
onCloseCleanup();
|
onCloseCleanup();
|
||||||
|
|
||||||
if (lprm) {
|
// This is set to false here so that LPRM can wait to send future lens://
|
||||||
// This is set to false here so that LPRM can wait to send future lens://
|
// requests until after it loads again
|
||||||
// requests until after it loads again
|
lensProtocolRouterMain.rendererLoaded = false;
|
||||||
lprm.rendererLoaded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blockQuit) {
|
if (blockQuit) {
|
||||||
// Quit app on Cmd+Q (MacOS)
|
// Quit app on Cmd+Q (MacOS)
|
||||||
@ -331,7 +334,7 @@ app.on("will-quit", (event) => {
|
|||||||
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
||||||
}
|
}
|
||||||
|
|
||||||
lprm?.cleanup();
|
lensProtocolRouterMain.cleanup();
|
||||||
onQuitCleanup();
|
onQuitCleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -340,7 +343,7 @@ app.on("open-url", (event, rawUrl) => {
|
|||||||
|
|
||||||
// lens:// protocol handler
|
// lens:// protocol handler
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
LensProtocolRouterMain.getInstance().route(rawUrl);
|
lensProtocolRouterMain.route(rawUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -18,8 +18,6 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* 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.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./registries";
|
|
||||||
export * from "./metrics-providers";
|
export * from "./metrics-providers";
|
||||||
export * from "./ipc";
|
export * from "./ipc";
|
||||||
export * from "./cluster-metadata-detectors";
|
export * from "./cluster-metadata-detectors";
|
||||||
|
|||||||
@ -34,9 +34,11 @@ import { IpcMainWindowEvents, WindowManager } from "../window-manager";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { remove } from "fs-extra";
|
import { remove } from "fs-extra";
|
||||||
import { AppPaths } from "../../common/app-paths";
|
import { AppPaths } from "../../common/app-paths";
|
||||||
import { getAppMenu } from "../menu";
|
import { getAppMenu } from "../menu/menu";
|
||||||
|
import type { MenuRegistration } from "../menu/menu-registration";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
|
|
||||||
export function initIpcMainHandlers() {
|
export function initIpcMainHandlers(electronMenuItems: IComputedValue<MenuRegistration[]>) {
|
||||||
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
||||||
return ClusterStore.getInstance()
|
return ClusterStore.getInstance()
|
||||||
.getById(clusterId)
|
.getById(clusterId)
|
||||||
@ -151,7 +153,7 @@ export function initIpcMainHandlers() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMainOn(IpcMainWindowEvents.OPEN_CONTEXT_MENU, async (event) => {
|
ipcMainOn(IpcMainWindowEvents.OPEN_CONTEXT_MENU, async (event) => {
|
||||||
const menu = Menu.buildFromTemplate(getAppMenu(WindowManager.getInstance()));
|
const menu = Menu.buildFromTemplate(getAppMenu(WindowManager.getInstance(), electronMenuItems.get()));
|
||||||
const options = {
|
const options = {
|
||||||
...BrowserWindow.fromWebContents(event.sender),
|
...BrowserWindow.fromWebContents(event.sender),
|
||||||
// Center of the topbar menu icon
|
// Center of the topbar menu icon
|
||||||
|
|||||||
41
src/main/menu/electron-menu-items.injectable.ts
Normal file
41
src/main/menu/electron-menu-items.injectable.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import { Injectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { computed, IComputedValue } from "mobx";
|
||||||
|
import type { LensMainExtension } from "../../extensions/lens-main-extension";
|
||||||
|
import extensionsInjectable from "../../extensions/extensions.injectable";
|
||||||
|
import type { MenuRegistration } from "./menu-registration";
|
||||||
|
|
||||||
|
const electronMenuItemsInjectable: Injectable<
|
||||||
|
IComputedValue<MenuRegistration[]>,
|
||||||
|
{ extensions: IComputedValue<LensMainExtension[]> }
|
||||||
|
> = {
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|
||||||
|
getDependencies: di => ({
|
||||||
|
extensions: di.inject(extensionsInjectable),
|
||||||
|
}),
|
||||||
|
|
||||||
|
instantiate: ({ extensions }) =>
|
||||||
|
computed(() => extensions.get().flatMap(extension => extension.appMenus)),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default electronMenuItemsInjectable;
|
||||||
132
src/main/menu/electron-menu-items.test.ts
Normal file
132
src/main/menu/electron-menu-items.test.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
|
import { LensMainExtension } from "../../extensions/lens-main-extension";
|
||||||
|
import electronMenuItemsInjectable from "./electron-menu-items.injectable";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
|
import { computed, ObservableMap, runInAction } from "mobx";
|
||||||
|
import type { MenuRegistration } from "./menu-registration";
|
||||||
|
import extensionsInjectable from "../../extensions/extensions.injectable";
|
||||||
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
|
||||||
|
describe("electron-menu-items", () => {
|
||||||
|
let di: ConfigurableDependencyInjectionContainer;
|
||||||
|
let electronMenuItems: IComputedValue<MenuRegistration[]>;
|
||||||
|
let extensionsStub: ObservableMap<string, LensMainExtension>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = getDiForUnitTesting();
|
||||||
|
|
||||||
|
extensionsStub = new ObservableMap();
|
||||||
|
|
||||||
|
di.override(
|
||||||
|
extensionsInjectable,
|
||||||
|
computed(() => [...extensionsStub.values()]),
|
||||||
|
);
|
||||||
|
|
||||||
|
electronMenuItems = di.inject(electronMenuItemsInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not have any items yet", () => {
|
||||||
|
expect(electronMenuItems.get()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when extension is enabled", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const someExtension = new SomeTestExtension({
|
||||||
|
id: "some-extension-id",
|
||||||
|
appMenus: [{ parentId: "some-parent-id-from-first-extension" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
extensionsStub.set("some-extension-id", someExtension);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has menu items", () => {
|
||||||
|
expect(electronMenuItems.get()).toEqual([
|
||||||
|
{
|
||||||
|
parentId: "some-parent-id-from-first-extension",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when disabling extension, does not have menu items", () => {
|
||||||
|
extensionsStub.delete("some-extension-id");
|
||||||
|
|
||||||
|
expect(electronMenuItems.get()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when other extension is enabled", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const someOtherExtension = new SomeTestExtension({
|
||||||
|
id: "some-extension-id",
|
||||||
|
appMenus: [{ parentId: "some-parent-id-from-second-extension" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
extensionsStub.set("some-other-extension-id", someOtherExtension);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has menu items for both extensions", () => {
|
||||||
|
expect(electronMenuItems.get()).toEqual([
|
||||||
|
{
|
||||||
|
parentId: "some-parent-id-from-first-extension",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
parentId: "some-parent-id-from-second-extension",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when extension is disabled, still returns menu items for extensions that are enabled", () => {
|
||||||
|
runInAction(() => {
|
||||||
|
extensionsStub.delete("some-other-extension-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(electronMenuItems.get()).toEqual([
|
||||||
|
{
|
||||||
|
parentId: "some-parent-id-from-first-extension",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class SomeTestExtension extends LensMainExtension {
|
||||||
|
constructor({ id, appMenus }: {
|
||||||
|
id: string;
|
||||||
|
appMenus: MenuRegistration[];
|
||||||
|
}) {
|
||||||
|
super({
|
||||||
|
id,
|
||||||
|
absolutePath: "irrelevant",
|
||||||
|
isBundled: false,
|
||||||
|
isCompatible: false,
|
||||||
|
isEnabled: false,
|
||||||
|
manifest: { name: id, version: "some-version" },
|
||||||
|
manifestPath: "irrelevant",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.appMenus = appMenus;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,15 +18,8 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* 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.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Extensions API -> Global menu customizations
|
|
||||||
|
|
||||||
import type { MenuItemConstructorOptions } from "electron";
|
import type { MenuItemConstructorOptions } from "electron";
|
||||||
import { BaseRegistry } from "./base-registry";
|
|
||||||
|
|
||||||
export interface MenuRegistration extends MenuItemConstructorOptions {
|
export interface MenuRegistration extends MenuItemConstructorOptions {
|
||||||
parentId: string;
|
parentId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MenuRegistry extends BaseRegistry<MenuRegistration> {
|
|
||||||
}
|
|
||||||
@ -18,18 +18,17 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* 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.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
|
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
|
||||||
import { autorun } from "mobx";
|
import { autorun, IComputedValue } from "mobx";
|
||||||
import type { WindowManager } from "./window-manager";
|
import type { WindowManager } from "../window-manager";
|
||||||
import { appName, isMac, isWindows, docsUrl, supportUrl, productName } from "../common/vars";
|
import { appName, isMac, isWindows, docsUrl, supportUrl, productName } from "../../common/vars";
|
||||||
import { MenuRegistry } from "../extensions/registries/menu-registry";
|
import logger from "../logger";
|
||||||
import logger from "./logger";
|
import { exitApp } from "../exit-app";
|
||||||
import { exitApp } from "./exit-app";
|
import { broadcastMessage } from "../../common/ipc";
|
||||||
import { broadcastMessage } from "../common/ipc";
|
import * as packageJson from "../../../package.json";
|
||||||
import * as packageJson from "../../package.json";
|
import { preferencesURL, extensionsURL, addClusterURL, catalogURL, welcomeURL } from "../../common/routes";
|
||||||
import { preferencesURL, extensionsURL, addClusterURL, catalogURL, welcomeURL } from "../common/routes";
|
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
|
||||||
import { checkForUpdates, isAutoUpdateEnabled } from "./app-updater";
|
import type { MenuRegistration } from "./menu-registration";
|
||||||
|
|
||||||
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
|
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
|
||||||
|
|
||||||
@ -37,8 +36,11 @@ interface MenuItemsOpts extends MenuItemConstructorOptions {
|
|||||||
submenu?: MenuItemConstructorOptions[];
|
submenu?: MenuItemConstructorOptions[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initMenu(windowManager: WindowManager) {
|
export function initMenu(
|
||||||
return autorun(() => buildMenu(windowManager), {
|
windowManager: WindowManager,
|
||||||
|
electronMenuItems: IComputedValue<MenuRegistration[]>,
|
||||||
|
) {
|
||||||
|
return autorun(() => buildMenu(windowManager, electronMenuItems.get()), {
|
||||||
delay: 100,
|
delay: 100,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -61,7 +63,10 @@ export function showAbout(browserWindow: BrowserWindow) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAppMenu(windowManager: WindowManager) {
|
export function getAppMenu(
|
||||||
|
windowManager: WindowManager,
|
||||||
|
electronMenuItems: MenuRegistration[],
|
||||||
|
) {
|
||||||
function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) {
|
function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) {
|
||||||
return check ? [] : menuItems;
|
return check ? [] : menuItems;
|
||||||
}
|
}
|
||||||
@ -302,14 +307,17 @@ export function getAppMenu(windowManager: WindowManager) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Modify menu from extensions-api
|
// Modify menu from extensions-api
|
||||||
for (const { parentId, ...menuItem } of MenuRegistry.getInstance().getItems()) {
|
for (const menuItem of electronMenuItems) {
|
||||||
if (!appMenu.has(parentId)) {
|
if (!appMenu.has(menuItem.parentId)) {
|
||||||
logger.error(`[MENU]: cannot register menu item for parentId=${parentId}, parent item doesn't exist`, { menuItem });
|
logger.error(
|
||||||
|
`[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`,
|
||||||
|
{ menuItem },
|
||||||
|
);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
appMenu.get(parentId).submenu.push(menuItem);
|
appMenu.get(menuItem.parentId).submenu.push(menuItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMac) {
|
if (!isMac) {
|
||||||
@ -320,6 +328,11 @@ export function getAppMenu(windowManager: WindowManager) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMenu(windowManager: WindowManager) {
|
export function buildMenu(
|
||||||
Menu.setApplicationMenu(Menu.buildFromTemplate(getAppMenu(windowManager)));
|
windowManager: WindowManager,
|
||||||
|
electronMenuItems: MenuRegistration[],
|
||||||
|
) {
|
||||||
|
Menu.setApplicationMenu(
|
||||||
|
Menu.buildFromTemplate(getAppMenu(windowManager, electronMenuItems)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -25,11 +25,15 @@ import { broadcastMessage } from "../../../common/ipc";
|
|||||||
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
|
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
|
||||||
import { delay, noop } from "../../../common/utils";
|
import { delay, noop } from "../../../common/utils";
|
||||||
import { LensExtension } from "../../../extensions/main-api";
|
import { LensExtension } from "../../../extensions/main-api";
|
||||||
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
|
||||||
import { ExtensionsStore } from "../../../extensions/extensions-store";
|
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 mockFs from "mock-fs";
|
||||||
import { AppPaths } from "../../../common/app-paths";
|
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");
|
jest.mock("../../../common/ipc");
|
||||||
|
|
||||||
@ -58,14 +62,22 @@ function throwIfDefined(val: any): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("protocol router tests", () => {
|
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(() => {
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting();
|
||||||
|
|
||||||
|
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
|
||||||
mockFs({
|
mockFs({
|
||||||
"tmp": {},
|
"tmp": {},
|
||||||
});
|
});
|
||||||
ExtensionsStore.createInstance();
|
ExtensionsStore.createInstance();
|
||||||
ExtensionLoader.createInstance();
|
|
||||||
|
|
||||||
const lpr = LensProtocolRouterMain.createInstance();
|
lpr = di.inject(lensProtocolRouterMainInjectable);
|
||||||
|
|
||||||
lpr.rendererLoaded = true;
|
lpr.rendererLoaded = true;
|
||||||
});
|
});
|
||||||
@ -74,15 +86,11 @@ describe("protocol router tests", () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
||||||
ExtensionsStore.resetInstance();
|
ExtensionsStore.resetInstance();
|
||||||
ExtensionLoader.resetInstance();
|
|
||||||
LensProtocolRouterMain.resetInstance();
|
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw on non-lens URLS", async () => {
|
it("should throw on non-lens URLS", async () => {
|
||||||
try {
|
try {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
|
||||||
|
|
||||||
expect(await lpr.route("https://google.ca")).toBeUndefined();
|
expect(await lpr.route("https://google.ca")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(Error);
|
expect(error).toBeInstanceOf(Error);
|
||||||
@ -91,8 +99,6 @@ describe("protocol router tests", () => {
|
|||||||
|
|
||||||
it("should throw when host not internal or extension", async () => {
|
it("should throw when host not internal or extension", async () => {
|
||||||
try {
|
try {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
|
||||||
|
|
||||||
expect(await lpr.route("lens://foobar")).toBeUndefined();
|
expect(await lpr.route("lens://foobar")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(Error);
|
expect(error).toBeInstanceOf(Error);
|
||||||
@ -113,14 +119,13 @@ describe("protocol router tests", () => {
|
|||||||
isCompatible: true,
|
isCompatible: true,
|
||||||
absolutePath: "/foo/bar",
|
absolutePath: "/foo/bar",
|
||||||
});
|
});
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
|
||||||
|
|
||||||
ext.protocolHandlers.push({
|
ext.protocolHandlers.push({
|
||||||
pathSchema: "/",
|
pathSchema: "/",
|
||||||
handler: noop,
|
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" });
|
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" });
|
||||||
|
|
||||||
lpr.addInternalHandler("/", noop);
|
lpr.addInternalHandler("/", noop);
|
||||||
@ -143,7 +148,6 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call handler if matches", async () => {
|
it("should call handler if matches", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
|
||||||
let called = false;
|
let called = false;
|
||||||
|
|
||||||
lpr.addInternalHandler("/page", () => { called = true; });
|
lpr.addInternalHandler("/page", () => { called = true; });
|
||||||
@ -159,7 +163,6 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler", async () => {
|
it("should call most exact handler", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
lpr.addInternalHandler("/page", () => { called = 1; });
|
lpr.addInternalHandler("/page", () => { called = 1; });
|
||||||
@ -178,7 +181,6 @@ describe("protocol router tests", () => {
|
|||||||
it("should call most exact handler for an extension", async () => {
|
it("should call most exact handler for an extension", async () => {
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
|
||||||
const extId = uuid.v4();
|
const extId = uuid.v4();
|
||||||
const ext = new LensExtension({
|
const ext = new LensExtension({
|
||||||
id: extId,
|
id: extId,
|
||||||
@ -202,7 +204,7 @@ describe("protocol router tests", () => {
|
|||||||
handler: params => { called = params.pathname.id; },
|
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" });
|
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -217,7 +219,6 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should work with non-org extensions", async () => {
|
it("should work with non-org extensions", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -241,7 +242,7 @@ describe("protocol router tests", () => {
|
|||||||
handler: params => { called = params.pathname.id; },
|
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" });
|
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,7 +267,7 @@ describe("protocol router tests", () => {
|
|||||||
handler: () => { called = 1; },
|
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" });
|
(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", () => {
|
it("should throw if urlSchema is invalid", () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
|
||||||
|
|
||||||
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler with 3 found handlers", async () => {
|
it("should call most exact handler with 3 found handlers", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
lpr.addInternalHandler("/", () => { called = 2; });
|
lpr.addInternalHandler("/", () => { called = 2; });
|
||||||
@ -311,7 +309,6 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler with 2 found handlers", async () => {
|
it("should call most exact handler with 2 found handlers", async () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
lpr.addInternalHandler("/", () => { called = 2; });
|
lpr.addInternalHandler("/", () => { called = 2; });
|
||||||
|
|||||||
@ -19,4 +19,4 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* 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.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import logger from "../logger";
|
import logger from "../../logger";
|
||||||
import * as proto from "../../common/protocol-handler";
|
import * as proto from "../../../common/protocol-handler";
|
||||||
import URLParse from "url-parse";
|
import URLParse from "url-parse";
|
||||||
import type { LensExtension } from "../../extensions/lens-extension";
|
import type { LensExtension } from "../../../extensions/lens-extension";
|
||||||
import { broadcastMessage } from "../../common/ipc";
|
import { broadcastMessage } from "../../../common/ipc";
|
||||||
import { observable, when, makeObservable } from "mobx";
|
import { observable, when, makeObservable } from "mobx";
|
||||||
import { ProtocolHandlerInvalid, RouteAttempt } from "../../common/protocol-handler";
|
import { ProtocolHandlerInvalid, RouteAttempt } from "../../../common/protocol-handler";
|
||||||
import { disposer, noop } from "../../common/utils";
|
import { disposer, noop } from "../../../common/utils";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../../window-manager";
|
||||||
|
import type { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||||
|
|
||||||
export interface FallbackHandler {
|
export interface FallbackHandler {
|
||||||
(name: string): Promise<boolean>;
|
(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 {
|
export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||||
private missingExtensionHandlers: FallbackHandler[] = [];
|
private missingExtensionHandlers: FallbackHandler[] = [];
|
||||||
|
|
||||||
@ -57,8 +62,8 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
|
|
||||||
protected disposers = disposer();
|
protected disposers = disposer();
|
||||||
|
|
||||||
constructor() {
|
constructor(protected dependencies: Dependencies) {
|
||||||
super();
|
super(dependencies);
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
@ -23,7 +23,7 @@ import path from "path";
|
|||||||
import packageInfo from "../../package.json";
|
import packageInfo from "../../package.json";
|
||||||
import { Menu, Tray } from "electron";
|
import { Menu, Tray } from "electron";
|
||||||
import { autorun } from "mobx";
|
import { autorun } from "mobx";
|
||||||
import { showAbout } from "./menu";
|
import { showAbout } from "./menu/menu";
|
||||||
import { checkForUpdates, isAutoUpdateEnabled } from "./app-updater";
|
import { checkForUpdates, isAutoUpdateEnabled } from "./app-updater";
|
||||||
import type { WindowManager } from "./window-manager";
|
import type { WindowManager } from "./window-manager";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|||||||
@ -34,7 +34,6 @@ import { isMac, isDevelopment } from "../common/vars";
|
|||||||
import { ClusterStore } from "../common/cluster-store";
|
import { ClusterStore } from "../common/cluster-store";
|
||||||
import { UserStore } from "../common/user-store";
|
import { UserStore } from "../common/user-store";
|
||||||
import { ExtensionDiscovery } from "../extensions/extension-discovery";
|
import { ExtensionDiscovery } from "../extensions/extension-discovery";
|
||||||
import { ExtensionLoader } from "../extensions/extension-loader";
|
|
||||||
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
|
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
|
||||||
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
|
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
|
||||||
import { DefaultProps } from "./mui-base-theme";
|
import { DefaultProps } from "./mui-base-theme";
|
||||||
@ -53,6 +52,13 @@ import { registerCustomThemes } from "./components/monaco-editor";
|
|||||||
import { getDi } from "./components/getDi";
|
import { getDi } from "./components/getDi";
|
||||||
import { DiContextProvider } from "@ogre-tools/injectable-react";
|
import { DiContextProvider } from "@ogre-tools/injectable-react";
|
||||||
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
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) {
|
if (process.isMainFrame) {
|
||||||
SentryInit();
|
SentryInit();
|
||||||
@ -73,7 +79,14 @@ async function attachChromeDebugger() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppComponent = React.ComponentType & {
|
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) {
|
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`);
|
logger.info(`${logPrefix} initializing Catalog`);
|
||||||
initializers.initCatalog();
|
initializers.initCatalog();
|
||||||
|
|
||||||
|
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing IpcRendererListeners`);
|
logger.info(`${logPrefix} initializing IpcRendererListeners`);
|
||||||
initializers.initIpcRendererListeners();
|
initializers.initIpcRendererListeners(extensionLoader);
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing StatusBarRegistry`);
|
logger.info(`${logPrefix} initializing StatusBarRegistry`);
|
||||||
initializers.initStatusBarRegistry();
|
initializers.initStatusBarRegistry();
|
||||||
|
|
||||||
ExtensionLoader.createInstance().init();
|
extensionLoader.init();
|
||||||
ExtensionDiscovery.createInstance().init();
|
|
||||||
|
ExtensionDiscovery.createInstance(extensionLoader).init();
|
||||||
|
|
||||||
// ClusterStore depends on: UserStore
|
// ClusterStore depends on: UserStore
|
||||||
const clusterStore = ClusterStore.createInstance();
|
const clusterStore = ClusterStore.createInstance();
|
||||||
@ -151,7 +167,10 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
|||||||
// init app's dependencies if any
|
// init app's dependencies if any
|
||||||
const App = await comp();
|
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(
|
render(
|
||||||
<DiContextProvider value={{ di }}>
|
<DiContextProvider value={{ di }}>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ import { isAllowedResource } from "../common/utils/allowed-resource";
|
|||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { webFrame } from "electron";
|
import { webFrame } from "electron";
|
||||||
import { ClusterPageRegistry, getExtensionPageUrl } from "../extensions/registries/page-registry";
|
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 { appEventBus } from "../common/event-bus";
|
||||||
import { requestMain } from "../common/ipc";
|
import { requestMain } from "../common/ipc";
|
||||||
import { clusterSetFrameIdHandler } from "../common/cluster-ipc";
|
import { clusterSetFrameIdHandler } from "../common/cluster-ipc";
|
||||||
@ -86,7 +86,7 @@ export class ClusterFrame extends React.Component {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async init(rootElem: HTMLElement) {
|
static async init(rootElem: HTMLElement, extensionLoader: ExtensionLoader) {
|
||||||
catalogEntityRegistry.init();
|
catalogEntityRegistry.init();
|
||||||
const frameId = webFrame.routingId;
|
const frameId = webFrame.routingId;
|
||||||
|
|
||||||
@ -101,7 +101,8 @@ export class ClusterFrame extends React.Component {
|
|||||||
|
|
||||||
catalogEntityRegistry.activeEntity = ClusterFrame.clusterId;
|
catalogEntityRegistry.activeEntity = ClusterFrame.clusterId;
|
||||||
|
|
||||||
ExtensionLoader.getInstance().loadOnClusterRenderer();
|
extensionLoader.loadOnClusterRenderer();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
appEventBus.emit({
|
appEventBus.emit({
|
||||||
name: "cluster",
|
name: "cluster",
|
||||||
|
|||||||
@ -20,18 +20,22 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { fireEvent, render, waitFor } from "@testing-library/react";
|
import { fireEvent, waitFor } from "@testing-library/react";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { UserStore } from "../../../../common/user-store";
|
import { UserStore } from "../../../../common/user-store";
|
||||||
import { ExtensionDiscovery } from "../../../../extensions/extension-discovery";
|
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 { ConfirmDialog } from "../../confirm-dialog";
|
||||||
import { ExtensionInstallationStateStore } from "../extension-install.store";
|
import { ExtensionInstallationStateStore } from "../extension-install.store";
|
||||||
import { Extensions } from "../extensions";
|
import { Extensions } from "../extensions";
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
||||||
import { AppPaths } from "../../../../common/app-paths";
|
import { AppPaths } from "../../../../common/app-paths";
|
||||||
|
import extensionLoaderInjectable
|
||||||
|
from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
|
|
||||||
mockWindow();
|
mockWindow();
|
||||||
|
|
||||||
@ -73,14 +77,23 @@ jest.mock("electron", () => ({
|
|||||||
AppPaths.init();
|
AppPaths.init();
|
||||||
|
|
||||||
describe("Extensions", () => {
|
describe("Extensions", () => {
|
||||||
|
let extensionLoader: ExtensionLoader;
|
||||||
|
let render: DiRender;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
const di = getDiForUnitTesting();
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
|
|
||||||
|
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
|
||||||
mockFs({
|
mockFs({
|
||||||
"tmp": {},
|
"tmp": {},
|
||||||
});
|
});
|
||||||
|
|
||||||
ExtensionInstallationStateStore.reset();
|
ExtensionInstallationStateStore.reset();
|
||||||
|
|
||||||
ExtensionLoader.createInstance().addExtension({
|
extensionLoader.addExtension({
|
||||||
id: "extensionId",
|
id: "extensionId",
|
||||||
manifest: {
|
manifest: {
|
||||||
name: "test",
|
name: "test",
|
||||||
@ -92,7 +105,11 @@ describe("Extensions", () => {
|
|||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
isCompatible: true,
|
isCompatible: true,
|
||||||
});
|
});
|
||||||
ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve());
|
|
||||||
|
const extensionDiscovery = ExtensionDiscovery.createInstance(extensionLoader);
|
||||||
|
|
||||||
|
extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve());
|
||||||
|
|
||||||
UserStore.createInstance();
|
UserStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -100,7 +117,6 @@ describe("Extensions", () => {
|
|||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
UserStore.resetInstance();
|
UserStore.resetInstance();
|
||||||
ExtensionDiscovery.resetInstance();
|
ExtensionDiscovery.resetInstance();
|
||||||
ExtensionLoader.resetInstance();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables uninstall and disable buttons while uninstalling", async () => {
|
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),
|
||||||
|
);
|
||||||
@ -18,9 +18,7 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* 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.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
export interface InstallRequest {
|
||||||
import * as registries from "../../extensions/registries";
|
fileName: string;
|
||||||
|
dataP: Promise<Buffer | null>;
|
||||||
export function initRegistries() {
|
|
||||||
registries.MenuRegistry.createInstance();
|
|
||||||
}
|
}
|
||||||
@ -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 "./extensions.scss";
|
||||||
|
import {
|
||||||
import { shell } from "electron";
|
IComputedValue,
|
||||||
import fse from "fs-extra";
|
makeObservable,
|
||||||
import _ from "lodash";
|
observable,
|
||||||
import { makeObservable, observable, reaction, when } from "mobx";
|
reaction,
|
||||||
|
when,
|
||||||
|
} from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import os from "os";
|
|
||||||
import path from "path";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SemVer } from "semver";
|
import type { InstalledExtension } from "../../../extensions/extension-discovery";
|
||||||
import URLParse from "url-parse";
|
import { DropFileInput } from "../input";
|
||||||
import { Disposer, disposer, downloadFile, downloadJson, ExtendableDisposer, extractTar, listTarEntries, noop, readFileFromTar } from "../../../common/utils";
|
import { ExtensionInstallationStateStore } from "./extension-install.store";
|
||||||
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 { Install } from "./install";
|
import { Install } from "./install";
|
||||||
import { InstalledExtensions } from "./installed-extensions";
|
import { InstalledExtensions } from "./installed-extensions";
|
||||||
import { Notice } from "./notice";
|
import { Notice } from "./notice";
|
||||||
import { SettingLayout } from "../layout/setting-layout";
|
import { SettingLayout } from "../layout/setting-layout";
|
||||||
import { docsUrl } from "../../../common/vars";
|
import { docsUrl } from "../../../common/vars";
|
||||||
import { dialog } from "../../remote-helpers";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import { AppPaths } from "../../../common/app-paths";
|
|
||||||
|
import userExtensionsInjectable from "./user-extensions/user-extensions.injectable";
|
||||||
function getMessageFromError(error: any): string {
|
import enableExtensionInjectable from "./enable-extension/enable-extension.injectable";
|
||||||
if (!error || typeof error !== "object") {
|
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
|
||||||
return "an error has occurred";
|
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";
|
||||||
if (error.message) {
|
import type { LensExtensionId } from "../../../extensions/lens-extension";
|
||||||
return String(error.message);
|
import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable";
|
||||||
}
|
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
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
|
@observer
|
||||||
export class Extensions extends React.Component<Props> {
|
class NonInjectedExtensions extends React.Component<Props> {
|
||||||
@observable installPath = "";
|
@observable installPath = "";
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get dependencies() {
|
||||||
|
return this.props.dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => ExtensionLoader.getInstance().userExtensions.size, (curSize, prevSize) => {
|
reaction(() => this.dependencies.userExtensions.get().length, (curSize, prevSize) => {
|
||||||
if (curSize > prevSize) {
|
if (curSize > prevSize) {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
when(() => !ExtensionInstallationStateStore.anyInstalling, () => this.installPath = ""),
|
when(() => !ExtensionInstallationStateStore.anyInstalling, () => this.installPath = ""),
|
||||||
@ -508,10 +87,10 @@ export class Extensions extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const extensions = Array.from(ExtensionLoader.getInstance().userExtensions.values());
|
const userExtensions = this.dependencies.userExtensions.get();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropFileInput onDropFiles={installOnDrop}>
|
<DropFileInput onDropFiles={this.dependencies.installOnDrop}>
|
||||||
<SettingLayout className="Extensions" contentGaps={false}>
|
<SettingLayout className="Extensions" contentGaps={false}>
|
||||||
<section>
|
<section>
|
||||||
<h1>Extensions</h1>
|
<h1>Extensions</h1>
|
||||||
@ -525,20 +104,20 @@ export class Extensions extends React.Component<Props> {
|
|||||||
</Notice>
|
</Notice>
|
||||||
|
|
||||||
<Install
|
<Install
|
||||||
supportedFormats={supportedFormats}
|
supportedFormats={supportedExtensionFormats}
|
||||||
onChange={(value) => this.installPath = value}
|
onChange={value => (this.installPath = value)}
|
||||||
installFromInput={() => installFromInput(this.installPath)}
|
installFromInput={() => this.dependencies.installFromInput(this.installPath)}
|
||||||
installFromSelectFileDialog={installFromSelectFileDialog}
|
installFromSelectFileDialog={this.dependencies.installFromSelectFileDialog}
|
||||||
installPath={this.installPath}
|
installPath={this.installPath}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{extensions.length > 0 && <hr/>}
|
{userExtensions.length > 0 && <hr />}
|
||||||
|
|
||||||
<InstalledExtensions
|
<InstalledExtensions
|
||||||
extensions={extensions}
|
extensions={userExtensions}
|
||||||
enable={enableExtension}
|
enable={this.dependencies.enableExtension}
|
||||||
disable={disableExtension}
|
disable={this.dependencies.disableExtension}
|
||||||
uninstall={confirmUninstallExtension}
|
uninstall={this.dependencies.confirmUninstallExtension}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</SettingLayout>
|
</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;
|
||||||
@ -20,12 +20,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createContainer } from "@ogre-tools/injectable";
|
import { createContainer } from "@ogre-tools/injectable";
|
||||||
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
|
||||||
|
|
||||||
export const getDi = () => {
|
export const getDi = () =>
|
||||||
const di: ConfigurableDependencyInjectionContainer = createContainer(
|
createContainer(
|
||||||
() => require.context("./", true, /\.injectable\.(ts|tsx)$/),
|
getRequireContextForRendererCode,
|
||||||
|
getRequireContextForCommonExtensionCode,
|
||||||
);
|
);
|
||||||
|
|
||||||
return di;
|
const getRequireContextForRendererCode = () =>
|
||||||
};
|
require.context("../", true, /\.injectable\.(ts|tsx)$/);
|
||||||
|
|
||||||
|
const getRequireContextForCommonExtensionCode = () =>
|
||||||
|
require.context("../../extensions", true, /\.injectable\.(ts|tsx)$/);
|
||||||
|
|||||||
@ -48,6 +48,7 @@ export const getDiForUnitTesting = () => {
|
|||||||
return di;
|
return di;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInjectableFilePaths = memoize(() =>
|
const getInjectableFilePaths = memoize(() => [
|
||||||
glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||||
);
|
...glob.sync("../../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||||
|
]);
|
||||||
|
|||||||
@ -20,4 +20,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type { KubeObjectMenuProps } from "./kube-object-menu";
|
export type { KubeObjectMenuProps } from "./kube-object-menu";
|
||||||
export { KubeObjectMenu } from "./kube-object-menu-container";
|
export { KubeObjectMenu } from "./kube-object-menu";
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -25,34 +25,45 @@ import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
|||||||
import { MenuActions, MenuActionsProps } from "../menu";
|
import { MenuActions, MenuActionsProps } from "../menu";
|
||||||
import identity from "lodash/identity";
|
import identity from "lodash/identity";
|
||||||
import type { ApiManager } from "../../../common/k8s-api/api-manager";
|
import type { ApiManager } from "../../../common/k8s-api/api-manager";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import clusterNameInjectable from "./dependencies/cluster-name.injectable";
|
||||||
|
import editResourceTabInjectable from "./dependencies/edit-resource-tab.injectable";
|
||||||
|
import hideDetailsInjectable from "./dependencies/hide-details.injectable";
|
||||||
|
import kubeObjectMenuItemsInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-items.injectable";
|
||||||
|
import apiManagerInjectable from "./dependencies/api-manager.injectable";
|
||||||
|
|
||||||
export interface KubeObjectMenuDependencies<TKubeObject> {
|
// TODO: Replace with KubeObjectMenuProps2
|
||||||
apiManager: ApiManager;
|
|
||||||
kubeObjectMenuItems: React.ElementType[];
|
|
||||||
clusterName: string;
|
|
||||||
hideDetails: () => void;
|
|
||||||
editResourceTab: (kubeObject: TKubeObject) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KubeObjectMenuProps<TKubeObject> extends MenuActionsProps {
|
export interface KubeObjectMenuProps<TKubeObject> extends MenuActionsProps {
|
||||||
object: TKubeObject | null | undefined;
|
object: TKubeObject | null | undefined;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
removable?: boolean;
|
removable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KubeObjectMenuPropsAndDependencies<TKubeObject>
|
interface KubeObjectMenuProps2 extends MenuActionsProps {
|
||||||
extends KubeObjectMenuProps<TKubeObject>,
|
object: KubeObject | null | undefined;
|
||||||
KubeObjectMenuDependencies<TKubeObject> {}
|
editable?: boolean;
|
||||||
|
removable?: boolean;
|
||||||
|
|
||||||
export class KubeObjectMenu<
|
dependencies: {
|
||||||
TKubeObject extends KubeObject,
|
apiManager: ApiManager;
|
||||||
> extends React.Component<KubeObjectMenuPropsAndDependencies<TKubeObject>> {
|
kubeObjectMenuItems: React.ElementType[];
|
||||||
|
clusterName: string;
|
||||||
|
hideDetails: () => void;
|
||||||
|
editResourceTab: (kubeObject: KubeObject) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class NonInjectedKubeObjectMenu extends React.Component<KubeObjectMenuProps2> {
|
||||||
|
get dependencies() {
|
||||||
|
return this.props.dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
get store() {
|
get store() {
|
||||||
const { object } = this.props;
|
const { object } = this.props;
|
||||||
|
|
||||||
if (!object) return null;
|
if (!object) return null;
|
||||||
|
|
||||||
return this.props.apiManager.getStore(object.selfLink);
|
return this.props.dependencies.apiManager.getStore(object.selfLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isEditable() {
|
get isEditable() {
|
||||||
@ -65,13 +76,13 @@ export class KubeObjectMenu<
|
|||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
async update() {
|
async update() {
|
||||||
this.props.hideDetails();
|
this.props.dependencies.hideDetails();
|
||||||
this.props.editResourceTab(this.props.object);
|
this.props.dependencies.editResourceTab(this.props.object);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
async remove() {
|
async remove() {
|
||||||
this.props.hideDetails();
|
this.props.dependencies.hideDetails();
|
||||||
const { object, removeAction } = this.props;
|
const { object, removeAction } = this.props;
|
||||||
|
|
||||||
if (removeAction) await removeAction();
|
if (removeAction) await removeAction();
|
||||||
@ -92,7 +103,7 @@ export class KubeObjectMenu<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
Remove {object.kind} <b>{breadcrumb}</b> from <b>{this.props.clusterName}</b>?
|
Remove {object.kind} <b>{breadcrumb}</b> from <b>{this.dependencies.clusterName}</b>?
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -100,12 +111,8 @@ export class KubeObjectMenu<
|
|||||||
getMenuItems(): React.ReactChild[] {
|
getMenuItems(): React.ReactChild[] {
|
||||||
const { object, toolbar } = this.props;
|
const { object, toolbar } = this.props;
|
||||||
|
|
||||||
return this.props.kubeObjectMenuItems.map((MenuItem, index) => (
|
return this.props.dependencies.kubeObjectMenuItems.map((MenuItem, index) => (
|
||||||
<MenuItem
|
<MenuItem object={object} toolbar={toolbar} key={`menu-item-${index}`} />
|
||||||
object={object}
|
|
||||||
toolbar={toolbar}
|
|
||||||
key={`menu-item-${index}`}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,3 +133,20 @@ export class KubeObjectMenu<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const KubeObjectMenu = withInjectables(NonInjectedKubeObjectMenu, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
dependencies: {
|
||||||
|
clusterName: di.inject(clusterNameInjectable),
|
||||||
|
apiManager: di.inject(apiManagerInjectable),
|
||||||
|
editResourceTab: di.inject(editResourceTabInjectable),
|
||||||
|
hideDetails: di.inject(hideDetailsInjectable),
|
||||||
|
|
||||||
|
kubeObjectMenuItems: di.inject(kubeObjectMenuItemsInjectable, {
|
||||||
|
kubeObject: props.object,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@ -20,11 +20,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ipcRendererOn } from "../../common/ipc";
|
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";
|
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>) => {
|
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.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./router";
|
export { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer/lens-protocol-router-renderer";
|
||||||
export * from "./app-handlers";
|
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 React from "react";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import * as proto from "../../common/protocol-handler";
|
import * as proto from "../../../common/protocol-handler";
|
||||||
import Url from "url-parse";
|
import Url from "url-parse";
|
||||||
import { onCorrect } from "../../common/ipc";
|
import { onCorrect } from "../../../common/ipc";
|
||||||
import { foldAttemptResults, ProtocolHandlerInvalid, RouteAttempt } from "../../common/protocol-handler";
|
import { foldAttemptResults, ProtocolHandlerInvalid, RouteAttempt } from "../../../common/protocol-handler";
|
||||||
import { Notifications } from "../components/notifications";
|
import { Notifications } from "../../components/notifications";
|
||||||
|
import type { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||||
|
|
||||||
function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] {
|
function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] {
|
||||||
if (args.length !== 2) {
|
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 {
|
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.
|
* 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 { ErrorBoundary } from "./components/error-boundary";
|
||||||
import { Notifications } from "./components/notifications";
|
import { Notifications } from "./components/notifications";
|
||||||
import { ConfirmDialog } from "./components/confirm-dialog";
|
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 { broadcastMessage } from "../common/ipc";
|
||||||
import { CommandContainer } from "./components/command-palette/command-container";
|
import { CommandContainer } from "./components/command-palette/command-container";
|
||||||
import { bindProtocolAddRouteHandlers, LensProtocolRouterRenderer } from "./protocol-handler";
|
|
||||||
import { registerIpcListeners } from "./ipc";
|
import { registerIpcListeners } from "./ipc";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { IpcRendererNavigationEvents } from "./navigation/events";
|
import { IpcRendererNavigationEvents } from "./navigation/events";
|
||||||
@ -39,6 +38,7 @@ import { catalogEntityRegistry } from "./api/catalog-entity-registry";
|
|||||||
import logger from "../common/logger";
|
import logger from "../common/logger";
|
||||||
import { unmountComponentAtNode } from "react-dom";
|
import { unmountComponentAtNode } from "react-dom";
|
||||||
import { ClusterFrameHandler } from "./components/cluster-manager/lens-views";
|
import { ClusterFrameHandler } from "./components/cluster-manager/lens-views";
|
||||||
|
import type { LensProtocolRouterRenderer } from "./protocol-handler";
|
||||||
|
|
||||||
injectSystemCAs();
|
injectSystemCAs();
|
||||||
|
|
||||||
@ -47,10 +47,15 @@ export class RootFrame extends React.Component {
|
|||||||
static readonly logPrefix = "[ROOT-FRAME]:";
|
static readonly logPrefix = "[ROOT-FRAME]:";
|
||||||
static displayName = "RootFrame";
|
static displayName = "RootFrame";
|
||||||
|
|
||||||
static async init(rootElem: HTMLElement) {
|
static async init(
|
||||||
|
rootElem: HTMLElement,
|
||||||
|
extensionLoader: ExtensionLoader,
|
||||||
|
bindProtocolAddRouteHandlers: () => void,
|
||||||
|
lensProtocolRouterRendererInjectable: LensProtocolRouterRenderer,
|
||||||
|
) {
|
||||||
catalogEntityRegistry.init();
|
catalogEntityRegistry.init();
|
||||||
ExtensionLoader.getInstance().loadOnClusterManagerRenderer();
|
extensionLoader.loadOnClusterManagerRenderer();
|
||||||
LensProtocolRouterRenderer.createInstance().init();
|
lensProtocolRouterRendererInjectable.init();
|
||||||
bindProtocolAddRouteHandlers();
|
bindProtocolAddRouteHandlers();
|
||||||
|
|
||||||
window.addEventListener("offline", () => broadcastMessage("network:offline"));
|
window.addEventListener("offline", () => broadcastMessage("network:offline"));
|
||||||
|
|||||||
30
yarn.lock
30
yarn.lock
@ -972,28 +972,28 @@
|
|||||||
"@nodelib/fs.scandir" "2.1.3"
|
"@nodelib/fs.scandir" "2.1.3"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
"@ogre-tools/fp@^1.0.2":
|
"@ogre-tools/fp@^1.4.0":
|
||||||
version "1.0.2"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-1.0.2.tgz#26c2c5cf60aa01cc94763cc68beba7052fdadfd9"
|
resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-1.4.0.tgz#94c50378c5bc51ea1571f775e4428256f22c61b5"
|
||||||
integrity sha512-ftvi/aoi5PaojWnuhHzp0YiecUd22HzW5gErsSiKyO2bps90WI4WjgY6d9hWdlzM9eukVmwM+dC6rGNlltNHNw==
|
integrity sha512-Eh/pK67CoYU/tJPWHeuNFEp+YdE8RPAAxZlSDAoXUDAd8sta3e+1vG7OEJlkYIJW4L8sCGKLWZu2DZ8uI6URhA==
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
|
|
||||||
"@ogre-tools/injectable-react@^1.3.1":
|
"@ogre-tools/injectable-react@^1.4.1":
|
||||||
version "1.3.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-1.3.1.tgz#dec3829ac8cf295c32cfe636ca2cd39a495d56ce"
|
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-1.4.1.tgz#48d8633462189939292596a66631d6717e39e47f"
|
||||||
integrity sha512-5jHL9Zcb3QkrttdzqJpN6iCXaV2+fEuDNigwH6NJ3uyV1iQWuRIctnlXxfa9qtZESwaAz7o0hAwkyqEl7YSA4g==
|
integrity sha512-SRk3QXvFCEQk4MeVG8TAomGcOt0Pf06hZ5kBh+iNIug3FLYeyWagH6OSVylZRu4u2Izd89J0taS1GmSfYDoHaA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ogre-tools/fp" "^1.0.2"
|
"@ogre-tools/fp" "^1.4.0"
|
||||||
"@ogre-tools/injectable" "^1.3.0"
|
"@ogre-tools/injectable" "^1.4.1"
|
||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
|
|
||||||
"@ogre-tools/injectable@^1.3.0":
|
"@ogre-tools/injectable@^1.4.1":
|
||||||
version "1.3.0"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-1.3.0.tgz#87d329a81575c9345b3af5c1afb0b45537f8f70e"
|
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-1.4.1.tgz#45414c6e13c870d7d84f4fa8e0dd67b33f6cc23e"
|
||||||
integrity sha512-rBy8HSExUy1r53ATvk823GXevwultKuSn3mmyRlIj7opJDVRp7Usx0bvOPs+X169jmAZNzsT6HBXbDLXt4Jl4A==
|
integrity sha512-vX4QXS/2d3g7oUenOKcv3mZRnJ5XewUMPsSsELjCyhL2caJlD0eB9J7y3y0eeFu/I18L8GC3DRs9o3QNshwN5Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ogre-tools/fp" "^1.0.2"
|
"@ogre-tools/fp" "^1.4.0"
|
||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
|
|
||||||
"@panva/asn1.js@^1.0.0":
|
"@panva/asn1.js@^1.0.0":
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user