mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Stop using global shared state for ExtensionDiscovery and it's relatives
Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
41aacb3db2
commit
a9189f82e6
@ -26,8 +26,8 @@ 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 type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store";
|
||||||
import type { ExtensionLoader as ExtensionLoaderType } from "../../extensions/extension-loader/extension-loader";
|
import type { ExtensionLoader } from "../../extensions/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";
|
||||||
@ -79,7 +79,8 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
extensionLoader: ExtensionLoaderType
|
extensionLoader: ExtensionLoader
|
||||||
|
extensionsStore: ExtensionsStore
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class LensProtocolRouter {
|
export abstract class LensProtocolRouter {
|
||||||
@ -212,7 +213,7 @@ export abstract class LensProtocolRouter {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ExtensionsStore.getInstance().isEnabled(extension)) {
|
if (!this.dependencies.extensionsStore.isEnabled(extension)) {
|
||||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
|
|||||||
@ -21,11 +21,15 @@
|
|||||||
|
|
||||||
import type { ExtensionLoader } from "../extension-loader";
|
import type { ExtensionLoader } from "../extension-loader";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { ExtensionsStore } from "../extensions-store";
|
import type {
|
||||||
|
ExtensionsStore,
|
||||||
|
} from "../extensions-store/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 { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
||||||
|
import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable";
|
||||||
|
import { AppPaths } from "../../common/app-paths";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
@ -33,15 +37,6 @@ const manifestPath = "manifest/path";
|
|||||||
const manifestPath2 = "manifest/path2";
|
const manifestPath2 = "manifest/path2";
|
||||||
const manifestPath3 = "manifest/path3";
|
const manifestPath3 = "manifest/path3";
|
||||||
|
|
||||||
jest.mock("../extensions-store", () => ({
|
|
||||||
ExtensionsStore: {
|
|
||||||
getInstance: () => ({
|
|
||||||
whenLoaded: Promise.resolve(true),
|
|
||||||
mergeState: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
"electron",
|
"electron",
|
||||||
() => ({
|
() => ({
|
||||||
@ -129,13 +124,25 @@ jest.mock(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
AppPaths.init();
|
||||||
|
|
||||||
describe("ExtensionLoader", () => {
|
describe("ExtensionLoader", () => {
|
||||||
let extensionLoader: ExtensionLoader;
|
let extensionLoader: ExtensionLoader;
|
||||||
|
let extensionsStoreStub: ExtensionsStore;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const di = getDiForUnitTesting();
|
const di = getDiForUnitTesting();
|
||||||
|
|
||||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
|
||||||
|
// TODO: Find out how to either easily create mocks of interfaces with a lot of members or
|
||||||
|
// introduce design for more minimal interfaces
|
||||||
|
// @ts-ignore
|
||||||
|
extensionsStoreStub = {
|
||||||
|
mergeState: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
di.override(extensionsStoreInjectable, () => extensionsStoreStub);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only("renderer updates extension after ipc broadcast", async done => {
|
it.only("renderer updates extension after ipc broadcast", async done => {
|
||||||
@ -177,18 +184,18 @@ describe("ExtensionLoader", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updates ExtensionsStore after isEnabled is changed", async () => {
|
it("updates ExtensionsStore after isEnabled is changed", async () => {
|
||||||
(ExtensionsStore.getInstance().mergeState as any).mockClear();
|
(extensionsStoreStub.mergeState as any).mockClear();
|
||||||
|
|
||||||
// Disable sending events in this test
|
// Disable sending events in this test
|
||||||
(ipcRenderer.on as any).mockImplementation();
|
(ipcRenderer.on as any).mockImplementation();
|
||||||
|
|
||||||
await extensionLoader.init();
|
await extensionLoader.init();
|
||||||
|
|
||||||
expect(ExtensionsStore.getInstance().mergeState).not.toHaveBeenCalled();
|
expect(extensionsStoreStub.mergeState).not.toHaveBeenCalled();
|
||||||
|
|
||||||
Array.from(extensionLoader.userExtensions.values())[0].isEnabled = false;
|
Array.from(extensionLoader.userExtensions.values())[0].isEnabled = false;
|
||||||
|
|
||||||
expect(ExtensionsStore.getInstance().mergeState).toHaveBeenCalledWith({
|
expect(extensionsStoreStub.mergeState).toHaveBeenCalledWith({
|
||||||
"manifest/path": {
|
"manifest/path": {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
name: "TestExtension",
|
name: "TestExtension",
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 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 { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
|
||||||
|
|
||||||
|
type TentativeTuple<T> = T extends object ? [T] : [undefined?];
|
||||||
|
|
||||||
|
type FactoryType = <
|
||||||
|
TInjectable extends Injectable<unknown, TInstance, TInstantiationParameter>,
|
||||||
|
TInstantiationParameter,
|
||||||
|
TInstance extends (...args: unknown[]) => any,
|
||||||
|
TFunction extends (...args: unknown[]) => any = Awaited<
|
||||||
|
ReturnType<TInjectable["instantiate"]>
|
||||||
|
>,
|
||||||
|
>(
|
||||||
|
injectableKey: TInjectable,
|
||||||
|
...instantiationParameter: TentativeTuple<TInstantiationParameter>
|
||||||
|
) => (...args: Parameters<TFunction>) => ReturnType<TFunction>;
|
||||||
|
|
||||||
|
export const asLegacyGlobalFunctionForExtensionApi: FactoryType =
|
||||||
|
(injectableKey, ...instantiationParameter) =>
|
||||||
|
(...args) => {
|
||||||
|
const injected = getLegacyGlobalDiForExtensionApi().inject(
|
||||||
|
injectableKey,
|
||||||
|
...instantiationParameter,
|
||||||
|
);
|
||||||
|
|
||||||
|
return injected(...args);
|
||||||
|
};
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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 { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
let legacyGlobalDi: DependencyInjectionContainer;
|
||||||
|
|
||||||
|
export const setLegacyGlobalDiForExtensionApi = (di: DependencyInjectionContainer) => {
|
||||||
|
legacyGlobalDi = di;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLegacyGlobalDiForExtensionApi = () => legacyGlobalDi;
|
||||||
@ -20,14 +20,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getAppVersion } from "../../common/utils";
|
import { getAppVersion } from "../../common/utils";
|
||||||
import { ExtensionsStore } from "../extensions-store";
|
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-global-function-for-extension-api/as-legacy-global-function-for-extension-api";
|
||||||
|
import getEnabledExtensionsInjectable from "./get-enabled-extensions/get-enabled-extensions.injectable";
|
||||||
import * as Preferences from "./user-preferences";
|
import * as Preferences from "./user-preferences";
|
||||||
|
|
||||||
export const version = getAppVersion();
|
export const version = getAppVersion();
|
||||||
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
||||||
|
|
||||||
export function getEnabledExtensions(): string[] {
|
export const getEnabledExtensions = asLegacyGlobalFunctionForExtensionApi(getEnabledExtensionsInjectable);
|
||||||
return ExtensionsStore.getInstance().enabledExtensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Preferences };
|
export { Preferences };
|
||||||
|
|||||||
@ -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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import extensionsStoreInjectable from "../../extensions-store/extensions-store.injectable";
|
||||||
|
|
||||||
|
const getEnabledExtensionsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => () =>
|
||||||
|
di.inject(extensionsStoreInjectable).enabledExtensions,
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getEnabledExtensionsInjectable;
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 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 { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { ExtensionDiscovery } from "./extension-discovery";
|
||||||
|
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
||||||
|
import extensionInstallerInjectable from "../extension-installer/extension-installer.injectable";
|
||||||
|
import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable";
|
||||||
|
import isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable";
|
||||||
|
import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable";
|
||||||
|
import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
|
|
||||||
|
const extensionDiscoveryInjectable = getInjectable({
|
||||||
|
instantiate: (di) =>
|
||||||
|
new ExtensionDiscovery({
|
||||||
|
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||||
|
extensionInstaller: di.inject(extensionInstallerInjectable),
|
||||||
|
extensionsStore: di.inject(extensionsStoreInjectable),
|
||||||
|
|
||||||
|
extensionInstallationStateStore: di.inject(
|
||||||
|
extensionInstallationStateStoreInjectable,
|
||||||
|
),
|
||||||
|
|
||||||
|
isCompatibleBundledExtension: di.inject(
|
||||||
|
isCompatibleBundledExtensionInjectable,
|
||||||
|
),
|
||||||
|
|
||||||
|
isCompatibleExtension: di.inject(isCompatibleExtensionInjectable),
|
||||||
|
}),
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default extensionDiscoveryInjectable;
|
||||||
@ -21,15 +21,14 @@
|
|||||||
|
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import { watch } from "chokidar";
|
import { watch } from "chokidar";
|
||||||
import { ExtensionsStore } from "../extensions-store";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { ExtensionDiscovery } from "../extension-discovery";
|
import type { 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";
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
import extensionDiscoveryInjectable from "./extension-discovery.injectable";
|
||||||
|
import extensionInstallerInjectable from "../extension-installer/extension-installer.injectable";
|
||||||
|
|
||||||
jest.setTimeout(60_000);
|
jest.setTimeout(60_000);
|
||||||
|
|
||||||
@ -37,12 +36,6 @@ jest.mock("../../common/ipc");
|
|||||||
jest.mock("chokidar", () => ({
|
jest.mock("chokidar", () => ({
|
||||||
watch: jest.fn(),
|
watch: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock("../extension-installer", () => ({
|
|
||||||
extensionInstaller: {
|
|
||||||
extensionPackagesRoot: "",
|
|
||||||
installPackage: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
app: {
|
app: {
|
||||||
getVersion: () => "99.99.99",
|
getVersion: () => "99.99.99",
|
||||||
@ -65,16 +58,23 @@ 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;
|
let extensionDiscovery: ExtensionDiscovery;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ExtensionDiscovery.resetInstance();
|
|
||||||
ExtensionsStore.resetInstance();
|
|
||||||
ExtensionsStore.createInstance();
|
|
||||||
|
|
||||||
const di = getDiForUnitTesting();
|
const di = getDiForUnitTesting();
|
||||||
|
|
||||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
const extensionInstallerStub = {
|
||||||
|
installPackages: () => Promise.resolve(),
|
||||||
|
npm: () => Promise.resolve(),
|
||||||
|
extensionPackagesRoot: "some-extension-packages-root",
|
||||||
|
npmPath: "some-npm-path",
|
||||||
|
installPackage: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
di.override(extensionInstallerInjectable, () => extensionInstallerStub);
|
||||||
|
|
||||||
|
extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with mockFs", () => {
|
describe("with mockFs", () => {
|
||||||
@ -103,13 +103,7 @@ describe("ExtensionDiscovery", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedWatch.mockImplementationOnce(() =>
|
mockedWatch.mockImplementationOnce(() => mockWatchInstance as any);
|
||||||
(mockWatchInstance) as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
||||||
@ -119,15 +113,18 @@ describe("ExtensionDiscovery", () => {
|
|||||||
extensionDiscovery.events.on("add", extension => {
|
extensionDiscovery.events.on("add", extension => {
|
||||||
expect(extension).toEqual({
|
expect(extension).toEqual({
|
||||||
absolutePath: expect.any(String),
|
absolutePath: expect.any(String),
|
||||||
id: path.normalize("node_modules/my-extension/package.json"),
|
id: path.normalize("some-extension-packages-root/node_modules/my-extension/package.json"),
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
isCompatible: false,
|
isCompatible: false,
|
||||||
manifest: {
|
manifest: {
|
||||||
name: "my-extension",
|
name: "my-extension",
|
||||||
},
|
},
|
||||||
manifestPath: path.normalize("node_modules/my-extension/package.json"),
|
manifestPath: path.normalize(
|
||||||
|
"some-extension-packages-root/node_modules/my-extension/package.json",
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -148,12 +145,7 @@ describe("ExtensionDiscovery", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedWatch.mockImplementationOnce(() =>
|
mockedWatch.mockImplementationOnce(() => mockWatchInstance as any);
|
||||||
(mockWatchInstance) as any,
|
|
||||||
);
|
|
||||||
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;
|
||||||
@ -23,19 +23,35 @@ import { watch } from "chokidar";
|
|||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { observable, reaction, when, makeObservable } from "mobx";
|
import { makeObservable, observable, reaction, when } from "mobx";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { broadcastMessage, ipcMainHandle, ipcRendererOn, requestMain } from "../common/ipc";
|
import {
|
||||||
import { Singleton, toJS } from "../common/utils";
|
broadcastMessage,
|
||||||
import logger from "../main/logger";
|
ipcMainHandle,
|
||||||
import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store";
|
ipcRendererOn,
|
||||||
import { extensionInstaller } from "./extension-installer";
|
requestMain,
|
||||||
import { ExtensionsStore } from "./extensions-store";
|
} from "../../common/ipc";
|
||||||
import type { ExtensionLoader } from "./extension-loader";
|
import { toJS } from "../../common/utils";
|
||||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
import logger from "../../main/logger";
|
||||||
import { isProduction } from "../common/vars";
|
import type { ExtensionInstaller } from "../extension-installer/extension-installer";
|
||||||
import { isCompatibleBundledExtension, isCompatibleExtension } from "./extension-compatibility";
|
import type { ExtensionsStore } from "../extensions-store/extensions-store";
|
||||||
|
import type { ExtensionLoader } from "../extension-loader";
|
||||||
|
import type { LensExtensionId, LensExtensionManifest } from "../lens-extension";
|
||||||
|
import { isProduction } from "../../common/vars";
|
||||||
|
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
extensionLoader: ExtensionLoader;
|
||||||
|
|
||||||
|
extensionInstaller: ExtensionInstaller;
|
||||||
|
extensionsStore: ExtensionsStore;
|
||||||
|
|
||||||
|
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||||
|
|
||||||
|
isCompatibleBundledExtension: (manifest: LensExtensionManifest) => boolean;
|
||||||
|
isCompatibleExtension: (manifest: LensExtensionManifest) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InstalledExtension {
|
export interface InstalledExtension {
|
||||||
id: LensExtensionId;
|
id: LensExtensionId;
|
||||||
@ -81,7 +97,7 @@ interface LoadFromFolderOptions {
|
|||||||
* - "add": When extension is added. The event is of type InstalledExtension
|
* - "add": When extension is added. The event is of type InstalledExtension
|
||||||
* - "remove": When extension is removed. The event is of type LensExtensionId
|
* - "remove": When extension is removed. The event is of type LensExtensionId
|
||||||
*/
|
*/
|
||||||
export class ExtensionDiscovery extends Singleton {
|
export class ExtensionDiscovery {
|
||||||
protected bundledFolderPath: string;
|
protected bundledFolderPath: string;
|
||||||
|
|
||||||
private loadStarted = false;
|
private loadStarted = false;
|
||||||
@ -99,9 +115,7 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
|
|
||||||
public events = new EventEmitter();
|
public events = new EventEmitter();
|
||||||
|
|
||||||
constructor(protected extensionLoader: ExtensionLoader) {
|
constructor(protected dependencies : Dependencies) {
|
||||||
super();
|
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,11 +124,11 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get packageJsonPath(): string {
|
get packageJsonPath(): string {
|
||||||
return path.join(extensionInstaller.extensionPackagesRoot, manifestFilename);
|
return path.join(this.dependencies.extensionInstaller.extensionPackagesRoot, manifestFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
get inTreeTargetPath(): string {
|
get inTreeTargetPath(): string {
|
||||||
return path.join(extensionInstaller.extensionPackagesRoot, "extensions");
|
return path.join(this.dependencies.extensionInstaller.extensionPackagesRoot, "extensions");
|
||||||
}
|
}
|
||||||
|
|
||||||
get inTreeFolderPath(): string {
|
get inTreeFolderPath(): string {
|
||||||
@ -122,7 +136,7 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get nodeModulesPath(): string {
|
get nodeModulesPath(): string {
|
||||||
return path.join(extensionInstaller.extensionPackagesRoot, "node_modules");
|
return path.join(this.dependencies.extensionInstaller.extensionPackagesRoot, "node_modules");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -197,7 +211,7 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
|
|
||||||
if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) {
|
if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) {
|
||||||
try {
|
try {
|
||||||
ExtensionInstallationStateStore.setInstallingFromMain(manifestPath);
|
this.dependencies.extensionInstallationStateStore.setInstallingFromMain(manifestPath);
|
||||||
const absPath = path.dirname(manifestPath);
|
const absPath = path.dirname(manifestPath);
|
||||||
|
|
||||||
// this.loadExtensionFromPath updates this.packagesJson
|
// this.loadExtensionFromPath updates this.packagesJson
|
||||||
@ -217,7 +231,7 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${logModule}: failed to add extension: ${error}`, { error });
|
logger.error(`${logModule}: failed to add extension: ${error}`, { error });
|
||||||
} finally {
|
} finally {
|
||||||
ExtensionInstallationStateStore.clearInstallingFromMain(manifestPath);
|
this.dependencies.extensionInstallationStateStore.clearInstallingFromMain(manifestPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -277,7 +291,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) ?? this.extensionLoader.getExtension(extensionId);
|
const { manifest, absolutePath } = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtension(extensionId);
|
||||||
|
|
||||||
logger.info(`${logModule} Uninstalling ${manifest.name}`);
|
logger.info(`${logModule} Uninstalling ${manifest.name}`);
|
||||||
|
|
||||||
@ -295,10 +309,15 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
|
|
||||||
this.loadStarted = true;
|
this.loadStarted = true;
|
||||||
|
|
||||||
logger.info(`${logModule} loading extensions from ${extensionInstaller.extensionPackagesRoot}`);
|
const extensionPackagesRoot =
|
||||||
|
this.dependencies.extensionInstaller.extensionPackagesRoot;
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`${logModule} loading extensions from ${extensionPackagesRoot}`,
|
||||||
|
);
|
||||||
|
|
||||||
// fs.remove won't throw if path is missing
|
// fs.remove won't throw if path is missing
|
||||||
await fse.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json"));
|
await fse.remove(path.join(extensionPackagesRoot, "package-lock.json"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify write access to static/extensions, which is needed for symlinking
|
// Verify write access to static/extensions, which is needed for symlinking
|
||||||
@ -357,11 +376,11 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
try {
|
try {
|
||||||
const manifest = await fse.readJson(manifestPath) as LensExtensionManifest;
|
const manifest = await fse.readJson(manifestPath) as LensExtensionManifest;
|
||||||
const id = this.getInstalledManifestPath(manifest.name);
|
const id = this.getInstalledManifestPath(manifest.name);
|
||||||
const isEnabled = ExtensionsStore.getInstance().isEnabled({ id, isBundled });
|
const isEnabled = this.dependencies.extensionsStore.isEnabled({ id, isBundled });
|
||||||
const extensionDir = path.dirname(manifestPath);
|
const extensionDir = path.dirname(manifestPath);
|
||||||
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||||
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
||||||
const isCompatible = (isBundled && isCompatibleBundledExtension(manifest)) || isCompatibleExtension(manifest);
|
const isCompatible = (isBundled && this.dependencies.isCompatibleBundledExtension(manifest)) || this.dependencies.isCompatibleExtension(manifest);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@ -417,11 +436,11 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
extensions.map(extension => [extension.manifest.name, extension.absolutePath]),
|
extensions.map(extension => [extension.manifest.name, extension.absolutePath]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return extensionInstaller.installPackages(packageJsonPath, { dependencies });
|
return this.dependencies.extensionInstaller.installPackages(packageJsonPath, { dependencies });
|
||||||
}
|
}
|
||||||
|
|
||||||
async installPackage(name: string): Promise<void> {
|
async installPackage(name: string): Promise<void> {
|
||||||
return extensionInstaller.installPackage(name);
|
return this.dependencies.extensionInstaller.installPackage(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadBundledExtensions(): Promise<InstalledExtension[]> {
|
async loadBundledExtensions(): Promise<InstalledExtension[]> {
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { appSemVer } from "../../../common/vars";
|
||||||
|
import { isCompatibleBundledExtension } from "./is-compatible-bundled-extension";
|
||||||
|
|
||||||
|
const isCompatibleBundledExtensionInjectable = getInjectable({
|
||||||
|
instantiate: () => isCompatibleBundledExtension({ appSemVer }),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default isCompatibleBundledExtensionInjectable;
|
||||||
@ -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 type { LensExtensionManifest } from "../../lens-extension";
|
||||||
|
import { isProduction } from "../../../common/vars";
|
||||||
|
import type { SemVer } from "semver";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
appSemVer: SemVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isCompatibleBundledExtension =
|
||||||
|
({ appSemVer }: Dependencies) =>
|
||||||
|
(manifest: LensExtensionManifest): boolean =>
|
||||||
|
!isProduction || manifest.version === appSemVer.raw;
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { appSemVer } from "../../../common/vars";
|
||||||
|
import { isCompatibleExtension } from "./is-compatible-extension";
|
||||||
|
|
||||||
|
const isCompatibleExtensionInjectable = getInjectable({
|
||||||
|
instantiate: () => isCompatibleExtension({ appSemVer }),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default isCompatibleExtensionInjectable;
|
||||||
@ -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 semver, { SemVer } from "semver";
|
||||||
|
import type { LensExtensionManifest } from "../../lens-extension";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
appSemVer: SemVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isCompatibleExtension = ({
|
||||||
|
appSemVer,
|
||||||
|
}: Dependencies): ((manifest: LensExtensionManifest) => boolean) => {
|
||||||
|
const { major, minor, patch, prerelease: oldPrelease } = appSemVer;
|
||||||
|
let prerelease = "";
|
||||||
|
|
||||||
|
if (oldPrelease.length > 0) {
|
||||||
|
const [first] = oldPrelease;
|
||||||
|
|
||||||
|
if (first === "alpha" || first === "beta" || first === "rc") {
|
||||||
|
/**
|
||||||
|
* Strip the build IDs and "latest" prerelease tag as that is not really
|
||||||
|
* a part of API version
|
||||||
|
*/
|
||||||
|
prerelease = `-${oldPrelease.slice(0, 2).join(".")}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We unfortunately have to format as string because the constructor only
|
||||||
|
* takes an instance or a string.
|
||||||
|
*/
|
||||||
|
const strippedVersion = new SemVer(
|
||||||
|
`${major}.${minor}.${patch}${prerelease}`,
|
||||||
|
{ includePrerelease: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (manifest: LensExtensionManifest): boolean => {
|
||||||
|
if (manifest.engines?.lens) {
|
||||||
|
/**
|
||||||
|
* include Lens's prerelease tag in the matching so the extension's
|
||||||
|
* compatibility is not limited by it
|
||||||
|
*/
|
||||||
|
return semver.satisfies(strippedVersion, manifest.engines.lens, {
|
||||||
|
includePrerelease: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { ExtensionInstallationStateStore } from "./extension-installation-state-store";
|
||||||
|
|
||||||
|
const extensionInstallationStateStoreInjectable = getInjectable({
|
||||||
|
instantiate: () => new ExtensionInstallationStateStore(),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default extensionInstallationStateStoreInjectable;
|
||||||
@ -0,0 +1,260 @@
|
|||||||
|
/**
|
||||||
|
* 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 { action, computed, observable } from "mobx";
|
||||||
|
import logger from "../../main/logger";
|
||||||
|
import { disposer } from "../../renderer/utils";
|
||||||
|
import type { ExtendableDisposer } from "../../renderer/utils";
|
||||||
|
import * as uuid from "uuid";
|
||||||
|
import { broadcastMessage } from "../../common/ipc";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
|
export enum ExtensionInstallationState {
|
||||||
|
INSTALLING = "installing",
|
||||||
|
UNINSTALLING = "uninstalling",
|
||||||
|
IDLE = "idle",
|
||||||
|
}
|
||||||
|
|
||||||
|
const Prefix = "[ExtensionInstallationStore]";
|
||||||
|
|
||||||
|
export class ExtensionInstallationStateStore {
|
||||||
|
private InstallingFromMainChannel =
|
||||||
|
"extension-installation-state-store:install";
|
||||||
|
|
||||||
|
private ClearInstallingFromMainChannel =
|
||||||
|
"extension-installation-state-store:clear-install";
|
||||||
|
|
||||||
|
private PreInstallIds = observable.set<string>();
|
||||||
|
private UninstallingExtensions = observable.set<string>();
|
||||||
|
private InstallingExtensions = observable.set<string>();
|
||||||
|
|
||||||
|
bindIpcListeners = () => {
|
||||||
|
ipcRenderer
|
||||||
|
.on(this.InstallingFromMainChannel, (event, extId) => {
|
||||||
|
this.setInstalling(extId);
|
||||||
|
})
|
||||||
|
|
||||||
|
.on(this.ClearInstallingFromMainChannel, (event, extId) => {
|
||||||
|
this.clearInstalling(extId);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strictly transitions an extension from not installing to installing
|
||||||
|
* @param extId the ID of the extension
|
||||||
|
* @throws if state is not IDLE
|
||||||
|
*/
|
||||||
|
@action setInstalling = (extId: string): void => {
|
||||||
|
logger.debug(`${Prefix}: trying to set ${extId} as installing`);
|
||||||
|
|
||||||
|
const curState = this.getInstallationState(extId);
|
||||||
|
|
||||||
|
if (curState !== ExtensionInstallationState.IDLE) {
|
||||||
|
throw new Error(
|
||||||
|
`${Prefix}: cannot set ${extId} as installing. Is currently ${curState}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.InstallingExtensions.add(extId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts that an extension is being installed by the main process
|
||||||
|
* @param extId the ID of the extension
|
||||||
|
*/
|
||||||
|
setInstallingFromMain = (extId: string): void => {
|
||||||
|
broadcastMessage(this.InstallingFromMainChannel, extId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts that an extension is no longer being installed by the main process
|
||||||
|
* @param extId the ID of the extension
|
||||||
|
*/
|
||||||
|
clearInstallingFromMain = (extId: string): void => {
|
||||||
|
broadcastMessage(this.ClearInstallingFromMainChannel, extId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the start of a pre-install phase of an extension installation. The
|
||||||
|
* part of the installation before the tarball has been unpacked and the ID
|
||||||
|
* determined.
|
||||||
|
* @returns a disposer which should be called to mark the end of the install phase
|
||||||
|
*/
|
||||||
|
@action startPreInstall = (): ExtendableDisposer => {
|
||||||
|
const preInstallStepId = uuid.v4();
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`${Prefix}: starting a new preinstall phase: ${preInstallStepId}`,
|
||||||
|
);
|
||||||
|
this.PreInstallIds.add(preInstallStepId);
|
||||||
|
|
||||||
|
return disposer(() => {
|
||||||
|
this.PreInstallIds.delete(preInstallStepId);
|
||||||
|
logger.debug(`${Prefix}: ending a preinstall phase: ${preInstallStepId}`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strictly transitions an extension from not uninstalling to uninstalling
|
||||||
|
* @param extId the ID of the extension
|
||||||
|
* @throws if state is not IDLE
|
||||||
|
*/
|
||||||
|
@action setUninstalling = (extId: string): void => {
|
||||||
|
logger.debug(`${Prefix}: trying to set ${extId} as uninstalling`);
|
||||||
|
|
||||||
|
const curState = this.getInstallationState(extId);
|
||||||
|
|
||||||
|
if (curState !== ExtensionInstallationState.IDLE) {
|
||||||
|
throw new Error(
|
||||||
|
`${Prefix}: cannot set ${extId} as uninstalling. Is currently ${curState}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.UninstallingExtensions.add(extId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strictly clears the INSTALLING state of an extension
|
||||||
|
* @param extId The ID of the extension
|
||||||
|
* @throws if state is not INSTALLING
|
||||||
|
*/
|
||||||
|
@action clearInstalling = (extId: string): void => {
|
||||||
|
logger.debug(`${Prefix}: trying to clear ${extId} as installing`);
|
||||||
|
|
||||||
|
const curState = this.getInstallationState(extId);
|
||||||
|
|
||||||
|
switch (curState) {
|
||||||
|
case ExtensionInstallationState.INSTALLING:
|
||||||
|
return void this.InstallingExtensions.delete(extId);
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`${Prefix}: cannot clear INSTALLING state for ${extId}, it is currently ${curState}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strictly clears the UNINSTALLING state of an extension
|
||||||
|
* @param extId The ID of the extension
|
||||||
|
* @throws if state is not UNINSTALLING
|
||||||
|
*/
|
||||||
|
@action clearUninstalling = (extId: string): void => {
|
||||||
|
logger.debug(`${Prefix}: trying to clear ${extId} as uninstalling`);
|
||||||
|
|
||||||
|
const curState = this.getInstallationState(extId);
|
||||||
|
|
||||||
|
switch (curState) {
|
||||||
|
case ExtensionInstallationState.UNINSTALLING:
|
||||||
|
return void this.UninstallingExtensions.delete(extId);
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`${Prefix}: cannot clear UNINSTALLING state for ${extId}, it is currently ${curState}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current state of the extension. IDLE is default value.
|
||||||
|
* @param extId The ID of the extension
|
||||||
|
*/
|
||||||
|
getInstallationState = (extId: string): ExtensionInstallationState => {
|
||||||
|
if (this.InstallingExtensions.has(extId)) {
|
||||||
|
return ExtensionInstallationState.INSTALLING;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.UninstallingExtensions.has(extId)) {
|
||||||
|
return ExtensionInstallationState.UNINSTALLING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExtensionInstallationState.IDLE;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the extension is currently INSTALLING
|
||||||
|
* @param extId The ID of the extension
|
||||||
|
*/
|
||||||
|
isExtensionInstalling = (extId: string): boolean =>
|
||||||
|
this.getInstallationState(extId) === ExtensionInstallationState.INSTALLING;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the extension is currently UNINSTALLING
|
||||||
|
* @param extId The ID of the extension
|
||||||
|
*/
|
||||||
|
isExtensionUninstalling = (extId: string): boolean =>
|
||||||
|
this.getInstallationState(extId) ===
|
||||||
|
ExtensionInstallationState.UNINSTALLING;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the extension is currently IDLE
|
||||||
|
* @param extId The ID of the extension
|
||||||
|
*/
|
||||||
|
isExtensionIdle = (extId: string): boolean =>
|
||||||
|
this.getInstallationState(extId) === ExtensionInstallationState.IDLE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current number of extensions installing
|
||||||
|
*/
|
||||||
|
@computed get installing(): number {
|
||||||
|
return this.InstallingExtensions.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current number of extensions uninstalling
|
||||||
|
*/
|
||||||
|
get uninstalling(): number {
|
||||||
|
return this.UninstallingExtensions.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is at least one extension currently installing
|
||||||
|
*/
|
||||||
|
get anyInstalling(): boolean {
|
||||||
|
return this.installing > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is at least one extension currently uninstalling
|
||||||
|
*/
|
||||||
|
get anyUninstalling(): boolean {
|
||||||
|
return this.uninstalling > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current number of extensions preinstalling
|
||||||
|
*/
|
||||||
|
get preinstalling(): number {
|
||||||
|
return this.PreInstallIds.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is at least one extension currently downloading
|
||||||
|
*/
|
||||||
|
get anyPreinstalling(): boolean {
|
||||||
|
return this.preinstalling > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is at least one installing or preinstalling step taking place
|
||||||
|
*/
|
||||||
|
get anyPreInstallingOrInstalling(): boolean {
|
||||||
|
return this.anyInstalling || this.anyPreinstalling;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { ExtensionInstaller } from "./extension-installer";
|
||||||
|
|
||||||
|
const extensionInstallerInjectable = getInjectable({
|
||||||
|
instantiate: () => new ExtensionInstaller(),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default extensionInstallerInjectable;
|
||||||
@ -23,13 +23,12 @@ import AwaitLock from "await-lock";
|
|||||||
import child_process from "child_process";
|
import child_process from "child_process";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import logger from "../main/logger";
|
import logger from "../../main/logger";
|
||||||
import { extensionPackagesRoot } from "./extension-loader";
|
import { extensionPackagesRoot } from "../extension-loader";
|
||||||
import type { PackageJson } from "type-fest";
|
import type { PackageJson } from "type-fest";
|
||||||
|
|
||||||
const logModule = "[EXTENSION-INSTALLER]";
|
const logModule = "[EXTENSION-INSTALLER]";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs dependencies for extensions
|
* Installs dependencies for extensions
|
||||||
*/
|
*/
|
||||||
@ -108,5 +107,3 @@ export class ExtensionInstaller {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extensionInstaller = new ExtensionInstaller();
|
|
||||||
@ -21,9 +21,13 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { lifecycleEnum } from "@ogre-tools/injectable";
|
import { lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import { ExtensionLoader } from "./extension-loader";
|
import { ExtensionLoader } from "./extension-loader";
|
||||||
|
import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable";
|
||||||
|
|
||||||
const extensionLoaderInjectable = getInjectable({
|
const extensionLoaderInjectable = getInjectable({
|
||||||
instantiate: () => new ExtensionLoader(),
|
instantiate: (di) => new ExtensionLoader({
|
||||||
|
extensionsStore: di.inject(extensionsStoreInjectable),
|
||||||
|
}),
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
lifecycle: lifecycleEnum.singleton,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -29,8 +29,8 @@ import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle
|
|||||||
import { Disposer, toJS } from "../../common/utils";
|
import { Disposer, toJS } from "../../common/utils";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import type { KubernetesCluster } from "../common-api/catalog";
|
import type { KubernetesCluster } from "../common-api/catalog";
|
||||||
import type { InstalledExtension } from "../extension-discovery";
|
import type { InstalledExtension } from "../extension-discovery/extension-discovery";
|
||||||
import { ExtensionsStore } from "../extensions-store";
|
import type { ExtensionsStore } from "../extensions-store/extensions-store";
|
||||||
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
|
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-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";
|
||||||
@ -41,6 +41,10 @@ export function extensionPackagesRoot() {
|
|||||||
|
|
||||||
const logModule = "[EXTENSIONS-LOADER]";
|
const logModule = "[EXTENSIONS-LOADER]";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
extensionsStore: ExtensionsStore
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads installed extensions to the Lens application
|
* Loads installed extensions to the Lens application
|
||||||
*/
|
*/
|
||||||
@ -75,7 +79,7 @@ export class ExtensionLoader {
|
|||||||
return when(() => this.isLoaded);
|
return when(() => this.isLoaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor(protected dependencies : Dependencies) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
observe(this.instances, change => {
|
observe(this.instances, change => {
|
||||||
switch (change.type) {
|
switch (change.type) {
|
||||||
@ -156,7 +160,7 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
// save state on change `extension.isEnabled`
|
// save state on change `extension.isEnabled`
|
||||||
reaction(() => this.storeState, extensionsState => {
|
reaction(() => this.storeState, extensionsState => {
|
||||||
ExtensionsStore.getInstance().mergeState(extensionsState);
|
this.dependencies.extensionsStore.mergeState(extensionsState);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { ExtensionsStore } from "./extensions-store";
|
||||||
|
|
||||||
|
const extensionsStoreInjectable = getInjectable({
|
||||||
|
instantiate: () => ExtensionsStore.createInstance(),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default extensionsStoreInjectable;
|
||||||
@ -19,10 +19,10 @@
|
|||||||
* 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 type { LensExtensionId } from "./lens-extension";
|
import type { LensExtensionId } from "../lens-extension";
|
||||||
import { BaseStore } from "../common/base-store";
|
import { action, computed, makeObservable, observable } from "mobx";
|
||||||
import { action, computed, observable, makeObservable } from "mobx";
|
import { toJS } from "../../common/utils";
|
||||||
import { toJS } from "../common/utils";
|
import { BaseStore } from "../../common/base-store";
|
||||||
|
|
||||||
export interface LensExtensionsStoreModel {
|
export interface LensExtensionsStoreModel {
|
||||||
extensions: Record<LensExtensionId, LensExtensionState>;
|
extensions: Record<LensExtensionId, LensExtensionState>;
|
||||||
@ -26,10 +26,13 @@ import {
|
|||||||
createContainer,
|
createContainer,
|
||||||
ConfigurableDependencyInjectionContainer,
|
ConfigurableDependencyInjectionContainer,
|
||||||
} from "@ogre-tools/injectable";
|
} from "@ogre-tools/injectable";
|
||||||
|
import { setLegacyGlobalDiForExtensionApi } from "./as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api";
|
||||||
|
|
||||||
export const getDiForUnitTesting = () => {
|
export const getDiForUnitTesting = () => {
|
||||||
const di: ConfigurableDependencyInjectionContainer = createContainer();
|
const di: ConfigurableDependencyInjectionContainer = createContainer();
|
||||||
|
|
||||||
|
setLegacyGlobalDiForExtensionApi(di);
|
||||||
|
|
||||||
getInjectableFilePaths()
|
getInjectableFilePaths()
|
||||||
.map(key => {
|
.map(key => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
* 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 type { InstalledExtension } from "./extension-discovery";
|
import type { InstalledExtension } from "./extension-discovery/extension-discovery";
|
||||||
import { action, observable, makeObservable, computed } 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";
|
||||||
|
|||||||
@ -20,13 +20,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createContainer } from "@ogre-tools/injectable";
|
import { createContainer } from "@ogre-tools/injectable";
|
||||||
|
import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api";
|
||||||
|
|
||||||
export const getDi = () =>
|
export const getDi = () => {
|
||||||
createContainer(
|
const di = createContainer(
|
||||||
getRequireContextForMainCode,
|
getRequireContextForMainCode,
|
||||||
getRequireContextForCommonExtensionCode,
|
getRequireContextForCommonExtensionCode,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setLegacyGlobalDiForExtensionApi(di);
|
||||||
|
|
||||||
|
return di;
|
||||||
|
};
|
||||||
|
|
||||||
const getRequireContextForMainCode = () =>
|
const getRequireContextForMainCode = () =>
|
||||||
require.context("./", true, /\.injectable\.(ts|tsx)$/);
|
require.context("./", true, /\.injectable\.(ts|tsx)$/);
|
||||||
|
|
||||||
|
|||||||
@ -26,10 +26,13 @@ import {
|
|||||||
createContainer,
|
createContainer,
|
||||||
ConfigurableDependencyInjectionContainer,
|
ConfigurableDependencyInjectionContainer,
|
||||||
} from "@ogre-tools/injectable";
|
} from "@ogre-tools/injectable";
|
||||||
|
import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api";
|
||||||
|
|
||||||
export const getDiForUnitTesting = () => {
|
export const getDiForUnitTesting = () => {
|
||||||
const di: ConfigurableDependencyInjectionContainer = createContainer();
|
const di: ConfigurableDependencyInjectionContainer = createContainer();
|
||||||
|
|
||||||
|
setLegacyGlobalDiForExtensionApi(di);
|
||||||
|
|
||||||
getInjectableFilePaths()
|
getInjectableFilePaths()
|
||||||
.map(key => {
|
.map(key => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
|||||||
@ -36,7 +36,7 @@ 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 { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery";
|
import type { InstalledExtension } from "../extensions/extension-discovery/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 { disposer, getAppVersion, getAppVersionFromProxyServer, storedKubeConfigFolder } from "../common/utils";
|
import { disposer, getAppVersion, getAppVersionFromProxyServer, storedKubeConfigFolder } from "../common/utils";
|
||||||
@ -54,7 +54,6 @@ import { ClusterStore } from "../common/cluster-store";
|
|||||||
import { HotbarStore } from "../common/hotbar-store";
|
import { HotbarStore } from "../common/hotbar-store";
|
||||||
import { UserStore } from "../common/user-store";
|
import { UserStore } from "../common/user-store";
|
||||||
import { WeblinkStore } from "../common/weblink-store";
|
import { WeblinkStore } from "../common/weblink-store";
|
||||||
import { ExtensionsStore } from "../extensions/extensions-store";
|
|
||||||
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
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";
|
||||||
@ -68,6 +67,8 @@ import { getDi } from "./getDi";
|
|||||||
import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable";
|
import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable";
|
||||||
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
||||||
import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
|
import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||||
|
import extensionDiscoveryInjectable
|
||||||
|
from "../extensions/extension-discovery/extension-discovery.injectable";
|
||||||
|
|
||||||
const di = getDi();
|
const di = getDi();
|
||||||
|
|
||||||
@ -169,7 +170,6 @@ app.on("ready", async () => {
|
|||||||
// HotbarStore depends on: ClusterStore
|
// HotbarStore depends on: ClusterStore
|
||||||
HotbarStore.createInstance();
|
HotbarStore.createInstance();
|
||||||
|
|
||||||
ExtensionsStore.createInstance();
|
|
||||||
FilesystemProvisionerStore.createInstance();
|
FilesystemProvisionerStore.createInstance();
|
||||||
WeblinkStore.createInstance();
|
WeblinkStore.createInstance();
|
||||||
|
|
||||||
@ -231,7 +231,7 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
extensionLoader.init();
|
extensionLoader.init();
|
||||||
|
|
||||||
const extensionDiscovery = ExtensionDiscovery.createInstance(extensionLoader);
|
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||||
|
|
||||||
extensionDiscovery.init();
|
extensionDiscovery.init();
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ 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 { ExtensionsStore } from "../../../extensions/extensions-store";
|
import { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store";
|
||||||
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
|
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";
|
||||||
@ -34,6 +34,8 @@ import extensionLoaderInjectable
|
|||||||
from "../../../extensions/extension-loader/extension-loader.injectable";
|
from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
import lensProtocolRouterMainInjectable
|
import lensProtocolRouterMainInjectable
|
||||||
from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
|
from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||||
|
import extensionsStoreInjectable
|
||||||
|
from "../../../extensions/extensions-store/extensions-store.injectable";
|
||||||
|
|
||||||
jest.mock("../../../common/ipc");
|
jest.mock("../../../common/ipc");
|
||||||
|
|
||||||
@ -66,16 +68,17 @@ describe("protocol router tests", () => {
|
|||||||
// Unit tests are allowed to only public interfaces.
|
// Unit tests are allowed to only public interfaces.
|
||||||
let extensionLoader: any;
|
let extensionLoader: any;
|
||||||
let lpr: LensProtocolRouterMain;
|
let lpr: LensProtocolRouterMain;
|
||||||
|
let extensionsStore: ExtensionsStore;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const di = getDiForUnitTesting();
|
const di = getDiForUnitTesting();
|
||||||
|
|
||||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
extensionsStore = di.inject(extensionsStoreInjectable);
|
||||||
|
|
||||||
mockFs({
|
mockFs({
|
||||||
"tmp": {},
|
"tmp": {},
|
||||||
});
|
});
|
||||||
ExtensionsStore.createInstance();
|
|
||||||
|
|
||||||
lpr = di.inject(lensProtocolRouterMainInjectable);
|
lpr = di.inject(lensProtocolRouterMainInjectable);
|
||||||
|
|
||||||
@ -85,7 +88,9 @@ describe("protocol router tests", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// TODO: Remove Singleton from BaseStore to achieve independent unit testing
|
||||||
ExtensionsStore.resetInstance();
|
ExtensionsStore.resetInstance();
|
||||||
|
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -126,7 +131,7 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
extensionLoader.instances.set(extId, ext);
|
extensionLoader.instances.set(extId, ext);
|
||||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" });
|
(extensionsStore as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" });
|
||||||
|
|
||||||
lpr.addInternalHandler("/", noop);
|
lpr.addInternalHandler("/", noop);
|
||||||
|
|
||||||
@ -205,7 +210,7 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
extensionLoader.instances.set(extId, ext);
|
extensionLoader.instances.set(extId, ext);
|
||||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
||||||
@ -243,7 +248,7 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
extensionLoader.instances.set(extId, ext);
|
extensionLoader.instances.set(extId, ext);
|
||||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -268,11 +273,11 @@ describe("protocol router tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
extensionLoader.instances.set(extId, ext);
|
extensionLoader.instances.set(extId, ext);
|
||||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "icecream" });
|
(extensionsStore as any).state.set(extId, { enabled: true, name: "icecream" });
|
||||||
}
|
}
|
||||||
|
|
||||||
(ExtensionsStore.getInstance() as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" });
|
(extensionsStore as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" });
|
||||||
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
|
(extensionsStore as any).state.set("icecream", { enabled: true, name: "icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
||||||
|
|||||||
@ -21,11 +21,13 @@
|
|||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
import { LensProtocolRouterMain } from "./lens-protocol-router-main";
|
import { LensProtocolRouterMain } from "./lens-protocol-router-main";
|
||||||
|
import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable";
|
||||||
|
|
||||||
const lensProtocolRouterMainInjectable = getInjectable({
|
const lensProtocolRouterMainInjectable = getInjectable({
|
||||||
instantiate: (di) =>
|
instantiate: (di) =>
|
||||||
new LensProtocolRouterMain({
|
new LensProtocolRouterMain({
|
||||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||||
|
extensionsStore: di.inject(extensionsStoreInjectable),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import { ProtocolHandlerInvalid, RouteAttempt } from "../../../common/protocol-h
|
|||||||
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";
|
import type { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||||
|
import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store";
|
||||||
|
|
||||||
export interface FallbackHandler {
|
export interface FallbackHandler {
|
||||||
(name: string): Promise<boolean>;
|
(name: string): Promise<boolean>;
|
||||||
@ -53,6 +54,7 @@ function checkHost<Query>(url: URLParse<Query>): boolean {
|
|||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
extensionLoader: ExtensionLoader
|
extensionLoader: ExtensionLoader
|
||||||
|
extensionsStore: ExtensionsStore
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||||
|
|||||||
@ -33,16 +33,13 @@ import { delay } from "../common/utils";
|
|||||||
import { isMac, isDevelopment } from "../common/vars";
|
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 { HelmRepoManager } from "../main/helm/helm-repo-manager";
|
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
|
||||||
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
|
|
||||||
import { DefaultProps } from "./mui-base-theme";
|
import { DefaultProps } from "./mui-base-theme";
|
||||||
import configurePackages from "../common/configure-packages";
|
import configurePackages from "../common/configure-packages";
|
||||||
import * as initializers from "./initializers";
|
import * as initializers from "./initializers";
|
||||||
import logger from "../common/logger";
|
import logger from "../common/logger";
|
||||||
import { HotbarStore } from "../common/hotbar-store";
|
import { HotbarStore } from "../common/hotbar-store";
|
||||||
import { WeblinkStore } from "../common/weblink-store";
|
import { WeblinkStore } from "../common/weblink-store";
|
||||||
import { ExtensionsStore } from "../extensions/extensions-store";
|
|
||||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||||
import { ThemeStore } from "./theme.store";
|
import { ThemeStore } from "./theme.store";
|
||||||
import { SentryInit } from "../common/sentry";
|
import { SentryInit } from "../common/sentry";
|
||||||
@ -59,6 +56,10 @@ import bindProtocolAddRouteHandlersInjectable
|
|||||||
import type { LensProtocolRouterRenderer } from "./protocol-handler";
|
import type { LensProtocolRouterRenderer } from "./protocol-handler";
|
||||||
import lensProtocolRouterRendererInjectable
|
import lensProtocolRouterRendererInjectable
|
||||||
from "./protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable";
|
from "./protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable";
|
||||||
|
import extensionDiscoveryInjectable
|
||||||
|
from "../extensions/extension-discovery/extension-discovery.injectable";
|
||||||
|
import extensionInstallationStateStoreInjectable
|
||||||
|
from "../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
|
|
||||||
if (process.isMainFrame) {
|
if (process.isMainFrame) {
|
||||||
SentryInit();
|
SentryInit();
|
||||||
@ -139,7 +140,9 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
|||||||
|
|
||||||
extensionLoader.init();
|
extensionLoader.init();
|
||||||
|
|
||||||
ExtensionDiscovery.createInstance(extensionLoader).init();
|
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||||
|
|
||||||
|
extensionDiscovery.init();
|
||||||
|
|
||||||
// ClusterStore depends on: UserStore
|
// ClusterStore depends on: UserStore
|
||||||
const clusterStore = ClusterStore.createInstance();
|
const clusterStore = ClusterStore.createInstance();
|
||||||
@ -148,7 +151,6 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
|||||||
|
|
||||||
// HotbarStore depends on: ClusterStore
|
// HotbarStore depends on: ClusterStore
|
||||||
HotbarStore.createInstance();
|
HotbarStore.createInstance();
|
||||||
ExtensionsStore.createInstance();
|
|
||||||
FilesystemProvisionerStore.createInstance();
|
FilesystemProvisionerStore.createInstance();
|
||||||
|
|
||||||
// ThemeStore depends on: UserStore
|
// ThemeStore depends on: UserStore
|
||||||
@ -158,7 +160,10 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
|||||||
TerminalStore.createInstance();
|
TerminalStore.createInstance();
|
||||||
WeblinkStore.createInstance();
|
WeblinkStore.createInstance();
|
||||||
|
|
||||||
ExtensionInstallationStateStore.bindIpcListeners();
|
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||||
|
|
||||||
|
extensionInstallationStateStore.bindIpcListeners();
|
||||||
|
|
||||||
HelmRepoManager.createInstance(); // initialize the manager
|
HelmRepoManager.createInstance(); // initialize the manager
|
||||||
|
|
||||||
// Register additional store listeners
|
// Register additional store listeners
|
||||||
|
|||||||
@ -24,10 +24,9 @@ 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 type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery";
|
||||||
import type { 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 { 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";
|
||||||
@ -36,6 +35,8 @@ import extensionLoaderInjectable
|
|||||||
from "../../../../extensions/extension-loader/extension-loader.injectable";
|
from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
|
import extensionDiscoveryInjectable
|
||||||
|
from "../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||||
|
|
||||||
mockWindow();
|
mockWindow();
|
||||||
|
|
||||||
@ -78,6 +79,7 @@ AppPaths.init();
|
|||||||
|
|
||||||
describe("Extensions", () => {
|
describe("Extensions", () => {
|
||||||
let extensionLoader: ExtensionLoader;
|
let extensionLoader: ExtensionLoader;
|
||||||
|
let extensionDiscovery: ExtensionDiscovery;
|
||||||
let render: DiRender;
|
let render: DiRender;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -87,12 +89,12 @@ describe("Extensions", () => {
|
|||||||
|
|
||||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
|
||||||
|
extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||||
|
|
||||||
mockFs({
|
mockFs({
|
||||||
"tmp": {},
|
"tmp": {},
|
||||||
});
|
});
|
||||||
|
|
||||||
ExtensionInstallationStateStore.reset();
|
|
||||||
|
|
||||||
extensionLoader.addExtension({
|
extensionLoader.addExtension({
|
||||||
id: "extensionId",
|
id: "extensionId",
|
||||||
manifest: {
|
manifest: {
|
||||||
@ -106,8 +108,6 @@ describe("Extensions", () => {
|
|||||||
isCompatible: true,
|
isCompatible: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const extensionDiscovery = ExtensionDiscovery.createInstance(extensionLoader);
|
|
||||||
|
|
||||||
extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve());
|
extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve());
|
||||||
|
|
||||||
UserStore.createInstance();
|
UserStore.createInstance();
|
||||||
@ -116,11 +116,10 @@ describe("Extensions", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
UserStore.resetInstance();
|
UserStore.resetInstance();
|
||||||
ExtensionDiscovery.resetInstance();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables uninstall and disable buttons while uninstalling", async () => {
|
it("disables uninstall and disable buttons while uninstalling", async () => {
|
||||||
ExtensionDiscovery.getInstance().isLoaded = true;
|
extensionDiscovery.isLoaded = true;
|
||||||
|
|
||||||
const res = render(<><Extensions /><ConfirmDialog /></>);
|
const res = render(<><Extensions /><ConfirmDialog /></>);
|
||||||
const table = res.getByTestId("extensions-table");
|
const table = res.getByTestId("extensions-table");
|
||||||
@ -137,7 +136,7 @@ describe("Extensions", () => {
|
|||||||
fireEvent.click(res.getByText("Yes"));
|
fireEvent.click(res.getByText("Yes"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled();
|
expect(extensionDiscovery.uninstallExtension).toHaveBeenCalled();
|
||||||
fireEvent.click(menuTrigger);
|
fireEvent.click(menuTrigger);
|
||||||
expect(res.getByText("Disable")).toHaveAttribute("aria-disabled", "true");
|
expect(res.getByText("Disable")).toHaveAttribute("aria-disabled", "true");
|
||||||
expect(res.getByText("Uninstall")).toHaveAttribute("aria-disabled", "true");
|
expect(res.getByText("Uninstall")).toHaveAttribute("aria-disabled", "true");
|
||||||
@ -164,14 +163,14 @@ describe("Extensions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("displays spinner while extensions are loading", () => {
|
it("displays spinner while extensions are loading", () => {
|
||||||
ExtensionDiscovery.getInstance().isLoaded = false;
|
extensionDiscovery.isLoaded = false;
|
||||||
const { container } = render(<Extensions />);
|
const { container } = render(<Extensions />);
|
||||||
|
|
||||||
expect(container.querySelector(".Spinner")).toBeInTheDocument();
|
expect(container.querySelector(".Spinner")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not display the spinner while extensions are not loading", async () => {
|
it("does not display the spinner while extensions are not loading", async () => {
|
||||||
ExtensionDiscovery.getInstance().isLoaded = true;
|
extensionDiscovery.isLoaded = true;
|
||||||
const { container } = render(<Extensions />);
|
const { container } = render(<Extensions />);
|
||||||
|
|
||||||
expect(container.querySelector(".Spinner")).not.toBeInTheDocument();
|
expect(container.querySelector(".Spinner")).not.toBeInTheDocument();
|
||||||
|
|||||||
@ -22,12 +22,15 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
|||||||
import { attemptInstallByInfo } from "./attempt-install-by-info";
|
import { attemptInstallByInfo } from "./attempt-install-by-info";
|
||||||
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
|
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
|
||||||
import getBaseRegistryUrlInjectable from "../get-base-registry-url/get-base-registry-url.injectable";
|
import getBaseRegistryUrlInjectable from "../get-base-registry-url/get-base-registry-url.injectable";
|
||||||
|
import extensionInstallationStateStoreInjectable
|
||||||
|
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
|
|
||||||
const attemptInstallByInfoInjectable = getInjectable({
|
const attemptInstallByInfoInjectable = getInjectable({
|
||||||
instantiate: (di) =>
|
instantiate: (di) =>
|
||||||
attemptInstallByInfo({
|
attemptInstallByInfo({
|
||||||
attemptInstall: di.inject(attemptInstallInjectable),
|
attemptInstall: di.inject(attemptInstallInjectable),
|
||||||
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
|
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
|
||||||
|
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|||||||
@ -18,7 +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.
|
||||||
*/
|
*/
|
||||||
import { ExtensionInstallationStateStore } from "../extension-install.store";
|
|
||||||
import { downloadFile, downloadJson, ExtendableDisposer } from "../../../../common/utils";
|
import { downloadFile, downloadJson, ExtendableDisposer } from "../../../../common/utils";
|
||||||
import { Notifications } from "../../notifications";
|
import { Notifications } from "../../notifications";
|
||||||
import { ConfirmDialog } from "../../confirm-dialog";
|
import { ConfirmDialog } from "../../confirm-dialog";
|
||||||
@ -28,6 +27,7 @@ import { SemVer } from "semver";
|
|||||||
import URLParse from "url-parse";
|
import URLParse from "url-parse";
|
||||||
import type { InstallRequest } from "../attempt-install/install-request";
|
import type { InstallRequest } from "../attempt-install/install-request";
|
||||||
import lodash from "lodash";
|
import lodash from "lodash";
|
||||||
|
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||||
|
|
||||||
export interface ExtensionInfo {
|
export interface ExtensionInfo {
|
||||||
name: string;
|
name: string;
|
||||||
@ -38,14 +38,15 @@ export interface ExtensionInfo {
|
|||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>;
|
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>;
|
||||||
getBaseRegistryUrl: () => Promise<string>;
|
getBaseRegistryUrl: () => Promise<string>;
|
||||||
|
extensionInstallationStateStore: ExtensionInstallationStateStore
|
||||||
}
|
}
|
||||||
|
|
||||||
export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl }: Dependencies) => async ({
|
export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl, extensionInstallationStateStore }: Dependencies) => async ({
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
requireConfirmation = false,
|
requireConfirmation = false,
|
||||||
}: ExtensionInfo) => {
|
}: ExtensionInfo) => {
|
||||||
const disposer = ExtensionInstallationStateStore.startPreInstall();
|
const disposer = extensionInstallationStateStore.startPreInstall();
|
||||||
const baseUrl = await getBaseRegistryUrl();
|
const baseUrl = await getBaseRegistryUrl();
|
||||||
const registryUrl = new URLParse(baseUrl).set("pathname", name).toString();
|
const registryUrl = new URLParse(baseUrl).set("pathname", name).toString();
|
||||||
let json: any;
|
let json: any;
|
||||||
|
|||||||
@ -23,6 +23,11 @@ import extensionLoaderInjectable from "../../../../extensions/extension-loader/e
|
|||||||
import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable";
|
import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable";
|
||||||
import { attemptInstall } from "./attempt-install";
|
import { attemptInstall } from "./attempt-install";
|
||||||
import unpackExtensionInjectable from "./unpack-extension/unpack-extension.injectable";
|
import unpackExtensionInjectable from "./unpack-extension/unpack-extension.injectable";
|
||||||
|
import getExtensionDestFolderInjectable
|
||||||
|
from "./get-extension-dest-folder/get-extension-dest-folder.injectable";
|
||||||
|
import createTempFilesAndValidateInjectable from "./create-temp-files-and-validate/create-temp-files-and-validate.injectable";
|
||||||
|
import extensionInstallationStateStoreInjectable
|
||||||
|
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
|
|
||||||
const attemptInstallInjectable = getInjectable({
|
const attemptInstallInjectable = getInjectable({
|
||||||
instantiate: (di) =>
|
instantiate: (di) =>
|
||||||
@ -30,6 +35,9 @@ const attemptInstallInjectable = getInjectable({
|
|||||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||||
uninstallExtension: di.inject(uninstallExtensionInjectable),
|
uninstallExtension: di.inject(uninstallExtensionInjectable),
|
||||||
unpackExtension: di.inject(unpackExtensionInjectable),
|
unpackExtension: di.inject(unpackExtensionInjectable),
|
||||||
|
createTempFilesAndValidate: di.inject(createTempFilesAndValidateInjectable),
|
||||||
|
getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable),
|
||||||
|
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|||||||
@ -23,10 +23,6 @@ import {
|
|||||||
disposer,
|
disposer,
|
||||||
ExtendableDisposer,
|
ExtendableDisposer,
|
||||||
} from "../../../../common/utils";
|
} from "../../../../common/utils";
|
||||||
import {
|
|
||||||
ExtensionInstallationState,
|
|
||||||
ExtensionInstallationStateStore,
|
|
||||||
} from "../extension-install.store";
|
|
||||||
import { Notifications } from "../../notifications";
|
import { Notifications } from "../../notifications";
|
||||||
import { Button } from "../../button";
|
import { Button } from "../../button";
|
||||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||||
@ -34,27 +30,43 @@ import type { LensExtensionId } from "../../../../extensions/lens-extension";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import {
|
import type { InstallRequestValidated } from "./create-temp-files-and-validate/create-temp-files-and-validate";
|
||||||
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";
|
import type { InstallRequest } from "./install-request";
|
||||||
|
import {
|
||||||
|
ExtensionInstallationState,
|
||||||
|
ExtensionInstallationStateStore,
|
||||||
|
} from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
extensionLoader: ExtensionLoader;
|
extensionLoader: ExtensionLoader;
|
||||||
uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
|
uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
|
||||||
|
|
||||||
unpackExtension: (
|
unpackExtension: (
|
||||||
request: InstallRequestValidated,
|
request: InstallRequestValidated,
|
||||||
disposeDownloading: Disposer,
|
disposeDownloading: Disposer,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
|
createTempFilesAndValidate: (
|
||||||
|
installRequest: InstallRequest,
|
||||||
|
) => Promise<InstallRequestValidated | null>;
|
||||||
|
|
||||||
|
getExtensionDestFolder: (name: string) => string
|
||||||
|
|
||||||
|
extensionInstallationStateStore: ExtensionInstallationStateStore
|
||||||
}
|
}
|
||||||
|
|
||||||
export const attemptInstall =
|
export const attemptInstall =
|
||||||
({ extensionLoader, uninstallExtension, unpackExtension }: Dependencies) =>
|
({
|
||||||
|
extensionLoader,
|
||||||
|
uninstallExtension,
|
||||||
|
unpackExtension,
|
||||||
|
createTempFilesAndValidate,
|
||||||
|
getExtensionDestFolder,
|
||||||
|
extensionInstallationStateStore,
|
||||||
|
}: Dependencies) =>
|
||||||
async (request: InstallRequest, d?: ExtendableDisposer): Promise<void> => {
|
async (request: InstallRequest, d?: ExtendableDisposer): Promise<void> => {
|
||||||
const dispose = disposer(
|
const dispose = disposer(
|
||||||
ExtensionInstallationStateStore.startPreInstall(),
|
extensionInstallationStateStore.startPreInstall(),
|
||||||
d,
|
d,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -65,7 +77,7 @@ export const attemptInstall =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { name, version, description } = validatedRequest.manifest;
|
const { name, version, description } = validatedRequest.manifest;
|
||||||
const curState = ExtensionInstallationStateStore.getInstallationState(
|
const curState = extensionInstallationStateStore.getInstallationState(
|
||||||
validatedRequest.id,
|
validatedRequest.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { createTempFilesAndValidate } from "./create-temp-files-and-validate";
|
||||||
|
import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||||
|
|
||||||
|
const createTempFilesAndValidateInjectable = getInjectable({
|
||||||
|
instantiate: (di) =>
|
||||||
|
createTempFilesAndValidate({
|
||||||
|
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||||
|
}),
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createTempFilesAndValidateInjectable;
|
||||||
@ -19,7 +19,7 @@
|
|||||||
* 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 { validatePackage } from "../validate-package/validate-package";
|
import { validatePackage } from "../validate-package/validate-package";
|
||||||
import { ExtensionDiscovery } from "../../../../../extensions/extension-discovery";
|
import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery";
|
||||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
||||||
import logger from "../../../../../main/logger";
|
import logger from "../../../../../main/logger";
|
||||||
import { Notifications } from "../../../notifications";
|
import { Notifications } from "../../../notifications";
|
||||||
@ -41,60 +41,66 @@ export interface InstallRequestValidated {
|
|||||||
tempFile: string; // temp system path to packed extension for unpacking
|
tempFile: string; // temp system path to packed extension for unpacking
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTempFilesAndValidate({
|
interface Dependencies {
|
||||||
fileName,
|
extensionDiscovery: ExtensionDiscovery
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createTempFilesAndValidate =
|
||||||
|
({ extensionDiscovery }: Dependencies) =>
|
||||||
|
async ({
|
||||||
|
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.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 = "") {
|
function getExtensionPackageTemp(fileName = "") {
|
||||||
return path.join(os.tmpdir(), "lens-extensions", fileName);
|
return path.join(os.tmpdir(), "lens-extensions", fileName);
|
||||||
|
|||||||
@ -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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||||
|
|
||||||
|
import { getExtensionDestFolder } from "./get-extension-dest-folder";
|
||||||
|
|
||||||
|
const getExtensionDestFolderInjectable = getInjectable({
|
||||||
|
instantiate: (di) =>
|
||||||
|
getExtensionDestFolder({
|
||||||
|
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||||
|
}),
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getExtensionDestFolderInjectable;
|
||||||
@ -18,11 +18,15 @@
|
|||||||
* 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 { ExtensionDiscovery } from "../../../../../extensions/extension-discovery";
|
import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery";
|
||||||
import { sanitizeExtensionName } from "../../../../../extensions/lens-extension";
|
import { sanitizeExtensionName } from "../../../../../extensions/lens-extension";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
export const getExtensionDestFolder = (name: string) => path.join(
|
interface Dependencies {
|
||||||
ExtensionDiscovery.getInstance().localFolderPath,
|
extensionDiscovery: ExtensionDiscovery;
|
||||||
sanitizeExtensionName(name),
|
}
|
||||||
);
|
|
||||||
|
export const getExtensionDestFolder =
|
||||||
|
({ extensionDiscovery }: Dependencies) =>
|
||||||
|
(name: string) =>
|
||||||
|
path.join(extensionDiscovery.localFolderPath, sanitizeExtensionName(name));
|
||||||
@ -22,11 +22,17 @@ import { getInjectable } from "@ogre-tools/injectable";
|
|||||||
import { lifecycleEnum } from "@ogre-tools/injectable";
|
import { lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import { unpackExtension } from "./unpack-extension";
|
import { unpackExtension } from "./unpack-extension";
|
||||||
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
|
import getExtensionDestFolderInjectable
|
||||||
|
from "../get-extension-dest-folder/get-extension-dest-folder.injectable";
|
||||||
|
import extensionInstallationStateStoreInjectable
|
||||||
|
from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
|
|
||||||
const unpackExtensionInjectable = getInjectable({
|
const unpackExtensionInjectable = getInjectable({
|
||||||
instantiate: (di) =>
|
instantiate: (di) =>
|
||||||
unpackExtension({
|
unpackExtension({
|
||||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||||
|
getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable),
|
||||||
|
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|||||||
@ -20,93 +20,98 @@
|
|||||||
*/
|
*/
|
||||||
import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate";
|
import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate";
|
||||||
import { Disposer, extractTar, noop } from "../../../../../common/utils";
|
import { Disposer, extractTar, noop } from "../../../../../common/utils";
|
||||||
import { ExtensionInstallationStateStore } from "../../extension-install.store";
|
|
||||||
import { extensionDisplayName } from "../../../../../extensions/lens-extension";
|
import { extensionDisplayName } from "../../../../../extensions/lens-extension";
|
||||||
import logger from "../../../../../main/logger";
|
import logger from "../../../../../main/logger";
|
||||||
import type { ExtensionLoader } from "../../../../../extensions/extension-loader";
|
import type { ExtensionLoader } from "../../../../../extensions/extension-loader";
|
||||||
import { Notifications } from "../../../notifications";
|
import { Notifications } from "../../../notifications";
|
||||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
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 path from "path";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { when } from "mobx";
|
import { when } from "mobx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import type { ExtensionInstallationStateStore } from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
extensionLoader: ExtensionLoader
|
extensionLoader: ExtensionLoader
|
||||||
|
getExtensionDestFolder: (name: string) => string
|
||||||
|
extensionInstallationStateStore: ExtensionInstallationStateStore
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unpackExtension = ({ extensionLoader }: Dependencies) => async (
|
export const unpackExtension =
|
||||||
request: InstallRequestValidated,
|
({
|
||||||
disposeDownloading?: Disposer,
|
extensionLoader,
|
||||||
) => {
|
getExtensionDestFolder,
|
||||||
const {
|
extensionInstallationStateStore,
|
||||||
id,
|
}: Dependencies) =>
|
||||||
fileName,
|
async (request: InstallRequestValidated, disposeDownloading?: Disposer) => {
|
||||||
tempFile,
|
const {
|
||||||
manifest: { name, version },
|
id,
|
||||||
} = request;
|
fileName,
|
||||||
|
tempFile,
|
||||||
|
manifest: { name, version },
|
||||||
|
} = request;
|
||||||
|
|
||||||
ExtensionInstallationStateStore.setInstalling(id);
|
extensionInstallationStateStore.setInstalling(id);
|
||||||
disposeDownloading?.();
|
disposeDownloading?.();
|
||||||
|
|
||||||
const displayName = extensionDisplayName(name, version);
|
const displayName = extensionDisplayName(name, version);
|
||||||
const extensionFolder = getExtensionDestFolder(name);
|
const extensionFolder = getExtensionDestFolder(name);
|
||||||
const unpackingTempFolder = path.join(
|
const unpackingTempFolder = path.join(
|
||||||
path.dirname(tempFile),
|
path.dirname(tempFile),
|
||||||
`${path.basename(tempFile)}-unpacked`,
|
`${path.basename(tempFile)}-unpacked`,
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// extract to temp folder first
|
// extract to temp folder first
|
||||||
await fse.remove(unpackingTempFolder).catch(noop);
|
await fse.remove(unpackingTempFolder).catch(noop);
|
||||||
await fse.ensureDir(unpackingTempFolder);
|
await fse.ensureDir(unpackingTempFolder);
|
||||||
await extractTar(tempFile, { cwd: unpackingTempFolder });
|
await extractTar(tempFile, { cwd: unpackingTempFolder });
|
||||||
|
|
||||||
// move contents to extensions folder
|
// move contents to extensions folder
|
||||||
const unpackedFiles = await fse.readdir(unpackingTempFolder);
|
const unpackedFiles = await fse.readdir(unpackingTempFolder);
|
||||||
let unpackedRootFolder = unpackingTempFolder;
|
let unpackedRootFolder = unpackingTempFolder;
|
||||||
|
|
||||||
if (unpackedFiles.length === 1) {
|
if (unpackedFiles.length === 1) {
|
||||||
// check if %extension.tgz was packed with single top folder,
|
// check if %extension.tgz was packed with single top folder,
|
||||||
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
|
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
|
||||||
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
|
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fse.ensureDir(extensionFolder);
|
await fse.ensureDir(extensionFolder);
|
||||||
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||||
|
|
||||||
// wait for the loader has actually install it
|
// wait for the loader has actually install it
|
||||||
await when(() => extensionLoader.userExtensions.has(id));
|
await when(() => extensionLoader.userExtensions.has(id));
|
||||||
|
|
||||||
// Enable installed extensions by default.
|
// Enable installed extensions by default.
|
||||||
extensionLoader.setIsEnabled(id, true);
|
extensionLoader.setIsEnabled(id, true);
|
||||||
|
|
||||||
Notifications.ok(
|
Notifications.ok(
|
||||||
<p>
|
<p>
|
||||||
Extension <b>{displayName}</b> successfully installed!
|
Extension <b>{displayName}</b> successfully installed!
|
||||||
</p>,
|
</p>,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getMessageFromError(error);
|
const message = getMessageFromError(error);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`,
|
`[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`,
|
||||||
{ error },
|
{ error },
|
||||||
);
|
);
|
||||||
Notifications.error(
|
Notifications.error(
|
||||||
<p>
|
<p>
|
||||||
Installing extension <b>{displayName}</b> has failed: <em>{message}</em>
|
Installing extension <b>{displayName}</b> has failed:{" "}
|
||||||
</p>,
|
<em>{message}</em>
|
||||||
);
|
</p>,
|
||||||
} finally {
|
);
|
||||||
// Remove install state once finished
|
} finally {
|
||||||
ExtensionInstallationStateStore.clearInstalling(id);
|
// Remove install state once finished
|
||||||
|
extensionInstallationStateStore.clearInstalling(id);
|
||||||
|
|
||||||
// clean up
|
// clean up
|
||||||
fse.remove(unpackingTempFolder).catch(noop);
|
fse.remove(unpackingTempFolder).catch(noop);
|
||||||
fse.unlink(tempFile).catch(noop);
|
fse.unlink(tempFile).catch(noop);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
import type { LensExtensionManifest } from "../../../../../extensions/lens-extension";
|
import type { LensExtensionManifest } from "../../../../../extensions/lens-extension";
|
||||||
import { listTarEntries, readFileFromTar } from "../../../../../common/utils";
|
import { listTarEntries, readFileFromTar } from "../../../../../common/utils";
|
||||||
import { manifestFilename } from "../../../../../extensions/extension-discovery";
|
import { manifestFilename } from "../../../../../extensions/extension-discovery/extension-discovery";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
export const validatePackage = async (
|
export const validatePackage = async (
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
* 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 React from "react";
|
import React from "react";
|
||||||
import type { InstalledExtension } from "../../../../extensions/extension-discovery";
|
import type { InstalledExtension } from "../../../../extensions/extension-discovery/extension-discovery";
|
||||||
import type { LensExtensionId } from "../../../../extensions/lens-extension";
|
import type { LensExtensionId } from "../../../../extensions/lens-extension";
|
||||||
import { extensionDisplayName } from "../../../../extensions/lens-extension";
|
import { extensionDisplayName } from "../../../../extensions/lens-extension";
|
||||||
import { ConfirmDialog } from "../../confirm-dialog";
|
import { ConfirmDialog } from "../../confirm-dialog";
|
||||||
|
|||||||
@ -1,254 +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 { action, computed, observable } from "mobx";
|
|
||||||
import logger from "../../../main/logger";
|
|
||||||
import { disposer } from "../../utils";
|
|
||||||
import type { ExtendableDisposer } from "../../utils";
|
|
||||||
import * as uuid from "uuid";
|
|
||||||
import { broadcastMessage } from "../../../common/ipc";
|
|
||||||
import { ipcRenderer } from "electron";
|
|
||||||
|
|
||||||
export enum ExtensionInstallationState {
|
|
||||||
INSTALLING = "installing",
|
|
||||||
UNINSTALLING = "uninstalling",
|
|
||||||
IDLE = "idle",
|
|
||||||
}
|
|
||||||
|
|
||||||
const Prefix = "[ExtensionInstallationStore]";
|
|
||||||
|
|
||||||
export class ExtensionInstallationStateStore {
|
|
||||||
private static InstallingFromMainChannel = "extension-installation-state-store:install";
|
|
||||||
private static ClearInstallingFromMainChannel = "extension-installation-state-store:clear-install";
|
|
||||||
private static PreInstallIds = observable.set<string>();
|
|
||||||
private static UninstallingExtensions = observable.set<string>();
|
|
||||||
private static InstallingExtensions = observable.set<string>();
|
|
||||||
|
|
||||||
static bindIpcListeners() {
|
|
||||||
ipcRenderer
|
|
||||||
.on(ExtensionInstallationStateStore.InstallingFromMainChannel, (event, extId) => {
|
|
||||||
ExtensionInstallationStateStore.setInstalling(extId);
|
|
||||||
})
|
|
||||||
.on(ExtensionInstallationStateStore.ClearInstallingFromMainChannel, (event, extId) => {
|
|
||||||
ExtensionInstallationStateStore.clearInstalling(extId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action static reset() {
|
|
||||||
logger.warn(`${Prefix}: resetting, may throw errors`);
|
|
||||||
ExtensionInstallationStateStore.InstallingExtensions.clear();
|
|
||||||
ExtensionInstallationStateStore.UninstallingExtensions.clear();
|
|
||||||
ExtensionInstallationStateStore.PreInstallIds.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strictly transitions an extension from not installing to installing
|
|
||||||
* @param extId the ID of the extension
|
|
||||||
* @throws if state is not IDLE
|
|
||||||
*/
|
|
||||||
@action static setInstalling(extId: string): void {
|
|
||||||
logger.debug(`${Prefix}: trying to set ${extId} as installing`);
|
|
||||||
|
|
||||||
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
|
|
||||||
|
|
||||||
if (curState !== ExtensionInstallationState.IDLE) {
|
|
||||||
throw new Error(`${Prefix}: cannot set ${extId} as installing. Is currently ${curState}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtensionInstallationStateStore.InstallingExtensions.add(extId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcasts that an extension is being installed by the main process
|
|
||||||
* @param extId the ID of the extension
|
|
||||||
*/
|
|
||||||
static setInstallingFromMain(extId: string): void {
|
|
||||||
broadcastMessage(ExtensionInstallationStateStore.InstallingFromMainChannel, extId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcasts that an extension is no longer being installed by the main process
|
|
||||||
* @param extId the ID of the extension
|
|
||||||
*/
|
|
||||||
static clearInstallingFromMain(extId: string): void {
|
|
||||||
broadcastMessage(ExtensionInstallationStateStore.ClearInstallingFromMainChannel, extId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks the start of a pre-install phase of an extension installation. The
|
|
||||||
* part of the installation before the tarball has been unpacked and the ID
|
|
||||||
* determined.
|
|
||||||
* @returns a disposer which should be called to mark the end of the install phase
|
|
||||||
*/
|
|
||||||
@action static startPreInstall(): ExtendableDisposer {
|
|
||||||
const preInstallStepId = uuid.v4();
|
|
||||||
|
|
||||||
logger.debug(`${Prefix}: starting a new preinstall phase: ${preInstallStepId}`);
|
|
||||||
ExtensionInstallationStateStore.PreInstallIds.add(preInstallStepId);
|
|
||||||
|
|
||||||
return disposer(() => {
|
|
||||||
ExtensionInstallationStateStore.PreInstallIds.delete(preInstallStepId);
|
|
||||||
logger.debug(`${Prefix}: ending a preinstall phase: ${preInstallStepId}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strictly transitions an extension from not uninstalling to uninstalling
|
|
||||||
* @param extId the ID of the extension
|
|
||||||
* @throws if state is not IDLE
|
|
||||||
*/
|
|
||||||
@action static setUninstalling(extId: string): void {
|
|
||||||
logger.debug(`${Prefix}: trying to set ${extId} as uninstalling`);
|
|
||||||
|
|
||||||
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
|
|
||||||
|
|
||||||
if (curState !== ExtensionInstallationState.IDLE) {
|
|
||||||
throw new Error(`${Prefix}: cannot set ${extId} as uninstalling. Is currently ${curState}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtensionInstallationStateStore.UninstallingExtensions.add(extId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strictly clears the INSTALLING state of an extension
|
|
||||||
* @param extId The ID of the extension
|
|
||||||
* @throws if state is not INSTALLING
|
|
||||||
*/
|
|
||||||
@action static clearInstalling(extId: string): void {
|
|
||||||
logger.debug(`${Prefix}: trying to clear ${extId} as installing`);
|
|
||||||
|
|
||||||
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
|
|
||||||
|
|
||||||
switch (curState) {
|
|
||||||
case ExtensionInstallationState.INSTALLING:
|
|
||||||
return void ExtensionInstallationStateStore.InstallingExtensions.delete(extId);
|
|
||||||
default:
|
|
||||||
throw new Error(`${Prefix}: cannot clear INSTALLING state for ${extId}, it is currently ${curState}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strictly clears the UNINSTALLING state of an extension
|
|
||||||
* @param extId The ID of the extension
|
|
||||||
* @throws if state is not UNINSTALLING
|
|
||||||
*/
|
|
||||||
@action static clearUninstalling(extId: string): void {
|
|
||||||
logger.debug(`${Prefix}: trying to clear ${extId} as uninstalling`);
|
|
||||||
|
|
||||||
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
|
|
||||||
|
|
||||||
switch (curState) {
|
|
||||||
case ExtensionInstallationState.UNINSTALLING:
|
|
||||||
return void ExtensionInstallationStateStore.UninstallingExtensions.delete(extId);
|
|
||||||
default:
|
|
||||||
throw new Error(`${Prefix}: cannot clear UNINSTALLING state for ${extId}, it is currently ${curState}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current state of the extension. IDLE is default value.
|
|
||||||
* @param extId The ID of the extension
|
|
||||||
*/
|
|
||||||
static getInstallationState(extId: string): ExtensionInstallationState {
|
|
||||||
if (ExtensionInstallationStateStore.InstallingExtensions.has(extId)) {
|
|
||||||
return ExtensionInstallationState.INSTALLING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ExtensionInstallationStateStore.UninstallingExtensions.has(extId)) {
|
|
||||||
return ExtensionInstallationState.UNINSTALLING;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExtensionInstallationState.IDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the extension is currently INSTALLING
|
|
||||||
* @param extId The ID of the extension
|
|
||||||
*/
|
|
||||||
static isExtensionInstalling(extId: string): boolean {
|
|
||||||
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.INSTALLING;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the extension is currently UNINSTALLING
|
|
||||||
* @param extId The ID of the extension
|
|
||||||
*/
|
|
||||||
static isExtensionUninstalling(extId: string): boolean {
|
|
||||||
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.UNINSTALLING;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the extension is currently IDLE
|
|
||||||
* @param extId The ID of the extension
|
|
||||||
*/
|
|
||||||
static isExtensionIdle(extId: string): boolean {
|
|
||||||
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.IDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current number of extensions installing
|
|
||||||
*/
|
|
||||||
@computed static get installing(): number {
|
|
||||||
return ExtensionInstallationStateStore.InstallingExtensions.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current number of extensions uninstalling
|
|
||||||
*/
|
|
||||||
static get uninstalling(): number {
|
|
||||||
return ExtensionInstallationStateStore.UninstallingExtensions.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If there is at least one extension currently installing
|
|
||||||
*/
|
|
||||||
static get anyInstalling(): boolean {
|
|
||||||
return ExtensionInstallationStateStore.installing > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If there is at least one extension currently uninstalling
|
|
||||||
*/
|
|
||||||
static get anyUninstalling(): boolean {
|
|
||||||
return ExtensionInstallationStateStore.uninstalling > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current number of extensions preinstalling
|
|
||||||
*/
|
|
||||||
static get preinstalling(): number {
|
|
||||||
return ExtensionInstallationStateStore.PreInstallIds.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If there is at least one extension currently downloading
|
|
||||||
*/
|
|
||||||
static get anyPreinstalling(): boolean {
|
|
||||||
return ExtensionInstallationStateStore.preinstalling > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If there is at least one installing or preinstalling step taking place
|
|
||||||
*/
|
|
||||||
static get anyPreInstallingOrInstalling(): boolean {
|
|
||||||
return ExtensionInstallationStateStore.anyInstalling || ExtensionInstallationStateStore.anyPreinstalling;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -29,9 +29,8 @@ import {
|
|||||||
} from "mobx";
|
} from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { InstalledExtension } from "../../../extensions/extension-discovery";
|
import type { InstalledExtension } from "../../../extensions/extension-discovery/extension-discovery";
|
||||||
import { DropFileInput } from "../input";
|
import { DropFileInput } from "../input";
|
||||||
import { 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";
|
||||||
@ -48,6 +47,9 @@ import installFromSelectFileDialogInjectable from "./install-from-select-file-di
|
|||||||
import type { LensExtensionId } from "../../../extensions/lens-extension";
|
import type { LensExtensionId } from "../../../extensions/lens-extension";
|
||||||
import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable";
|
import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable";
|
||||||
import { supportedExtensionFormats } from "./supported-extension-formats";
|
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||||
|
import extensionInstallationStateStoreInjectable
|
||||||
|
from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
|
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
userExtensions: IComputedValue<InstalledExtension[]>;
|
userExtensions: IComputedValue<InstalledExtension[]>;
|
||||||
@ -57,6 +59,7 @@ interface Dependencies {
|
|||||||
installFromInput: (input: string) => Promise<void>;
|
installFromInput: (input: string) => Promise<void>;
|
||||||
installFromSelectFileDialog: () => Promise<void>;
|
installFromSelectFileDialog: () => Promise<void>;
|
||||||
installOnDrop: (files: File[]) => Promise<void>;
|
installOnDrop: (files: File[]) => Promise<void>;
|
||||||
|
extensionInstallationStateStore: ExtensionInstallationStateStore
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -73,7 +76,7 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
|
|||||||
reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => {
|
reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => {
|
||||||
if (curSize > prevSize) {
|
if (curSize > prevSize) {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
when(() => !ExtensionInstallationStateStore.anyInstalling, () => this.installPath = ""),
|
when(() => !this.props.extensionInstallationStateStore.anyInstalling, () => this.installPath = ""),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -134,6 +137,8 @@ export const Extensions = withInjectables<Dependencies>(
|
|||||||
installFromSelectFileDialog: di.inject(
|
installFromSelectFileDialog: di.inject(
|
||||||
installFromSelectFileDialogInjectable,
|
installFromSelectFileDialogInjectable,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -22,12 +22,15 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
|||||||
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
|
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
|
||||||
import { installFromInput } from "./install-from-input";
|
import { installFromInput } from "./install-from-input";
|
||||||
import attemptInstallByInfoInjectable from "../attempt-install-by-info/attempt-install-by-info.injectable";
|
import attemptInstallByInfoInjectable from "../attempt-install-by-info/attempt-install-by-info.injectable";
|
||||||
|
import extensionInstallationStateStoreInjectable
|
||||||
|
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
|
|
||||||
const installFromInputInjectable = getInjectable({
|
const installFromInputInjectable = getInjectable({
|
||||||
instantiate: (di) =>
|
instantiate: (di) =>
|
||||||
installFromInput({
|
installFromInput({
|
||||||
attemptInstall: di.inject(attemptInstallInjectable),
|
attemptInstall: di.inject(attemptInstallInjectable),
|
||||||
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
|
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
|
||||||
|
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
import { downloadFile, ExtendableDisposer } from "../../../../common/utils";
|
import { downloadFile, ExtendableDisposer } from "../../../../common/utils";
|
||||||
import { InputValidators } from "../../input";
|
import { InputValidators } from "../../input";
|
||||||
import { ExtensionInstallationStateStore } from "../extension-install.store";
|
|
||||||
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
||||||
import logger from "../../../../main/logger";
|
import logger from "../../../../main/logger";
|
||||||
import { Notifications } from "../../notifications";
|
import { Notifications } from "../../notifications";
|
||||||
@ -29,20 +28,22 @@ import React from "react";
|
|||||||
import { readFileNotify } from "../read-file-notify/read-file-notify";
|
import { readFileNotify } from "../read-file-notify/read-file-notify";
|
||||||
import type { InstallRequest } from "../attempt-install/install-request";
|
import type { InstallRequest } from "../attempt-install/install-request";
|
||||||
import type { ExtensionInfo } from "../attempt-install-by-info/attempt-install-by-info";
|
import type { ExtensionInfo } from "../attempt-install-by-info/attempt-install-by-info";
|
||||||
|
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise<void>,
|
attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise<void>,
|
||||||
attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise<void>
|
attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise<void>,
|
||||||
|
extensionInstallationStateStore: ExtensionInstallationStateStore
|
||||||
}
|
}
|
||||||
|
|
||||||
export const installFromInput = ({ attemptInstall, attemptInstallByInfo }: Dependencies) => async (input: string) => {
|
export const installFromInput = ({ attemptInstall, attemptInstallByInfo, extensionInstallationStateStore }: Dependencies) => async (input: string) => {
|
||||||
let disposer: ExtendableDisposer | undefined = undefined;
|
let disposer: ExtendableDisposer | undefined = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// fixme: improve error messages for non-tar-file URLs
|
// fixme: improve error messages for non-tar-file URLs
|
||||||
if (InputValidators.isUrl.validate(input)) {
|
if (InputValidators.isUrl.validate(input)) {
|
||||||
// install via url
|
// install via url
|
||||||
disposer = ExtensionInstallationStateStore.startPreInstall();
|
disposer = extensionInstallationStateStore.startPreInstall();
|
||||||
const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 });
|
const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 });
|
||||||
const fileName = path.basename(input);
|
const fileName = path.basename(input);
|
||||||
|
|
||||||
|
|||||||
@ -24,11 +24,14 @@ import React from "react";
|
|||||||
import { prevDefault } from "../../utils";
|
import { prevDefault } from "../../utils";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
import { Input, InputValidator, InputValidators } from "../input";
|
import { Input, InputValidator, InputValidators } from "../input";
|
||||||
import { SubTitle } from "../layout/sub-title";
|
import { SubTitle } from "../layout/sub-title";
|
||||||
import { TooltipPosition } from "../tooltip";
|
import { TooltipPosition } from "../tooltip";
|
||||||
import { ExtensionInstallationStateStore } from "./extension-install.store";
|
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||||
import { observer } from "mobx-react";
|
import extensionInstallationStateStoreInjectable
|
||||||
|
from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
installPath: string;
|
installPath: string;
|
||||||
@ -38,6 +41,10 @@ interface Props {
|
|||||||
installFromSelectFileDialog: () => void;
|
installFromSelectFileDialog: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||||
|
}
|
||||||
|
|
||||||
const installInputValidators = [
|
const installInputValidators = [
|
||||||
InputValidators.isUrl,
|
InputValidators.isUrl,
|
||||||
InputValidators.isPath,
|
InputValidators.isPath,
|
||||||
@ -51,49 +58,72 @@ const installInputValidator: InputValidator = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Install = observer((props: Props) => {
|
const NonInjectedInstall: React.FC<Dependencies & Props> = ({
|
||||||
const { installPath, supportedFormats, onChange, installFromInput, installFromSelectFileDialog } = props;
|
installPath,
|
||||||
|
supportedFormats,
|
||||||
return (
|
onChange,
|
||||||
<section className="mt-2">
|
installFromInput,
|
||||||
<SubTitle title={`Name or file path or URL to an extension package (${supportedFormats.join(", ")})`}/>
|
installFromSelectFileDialog,
|
||||||
<div className="flex">
|
extensionInstallationStateStore,
|
||||||
<div className="flex-1">
|
}) => (
|
||||||
<Input
|
<section className="mt-2">
|
||||||
className="box grow mr-6"
|
<SubTitle
|
||||||
theme="round-black"
|
title={`Name or file path or URL to an extension package (${supportedFormats.join(
|
||||||
disabled={ExtensionInstallationStateStore.anyPreInstallingOrInstalling}
|
", ",
|
||||||
placeholder={"Name or file path or URL"}
|
)})`}
|
||||||
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
/>
|
||||||
validators={installPath ? installInputValidator : undefined}
|
<div className="flex">
|
||||||
value={installPath}
|
<div className="flex-1">
|
||||||
onChange={onChange}
|
<Input
|
||||||
onSubmit={installFromInput}
|
className="box grow mr-6"
|
||||||
iconRight={
|
theme="round-black"
|
||||||
<Icon
|
disabled={
|
||||||
className={styles.icon}
|
extensionInstallationStateStore.anyPreInstallingOrInstalling
|
||||||
material="folder_open"
|
}
|
||||||
onClick={prevDefault(installFromSelectFileDialog)}
|
placeholder={"Name or file path or URL"}
|
||||||
tooltip="Browse"
|
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
||||||
/>
|
validators={installPath ? installInputValidator : undefined}
|
||||||
}
|
value={installPath}
|
||||||
/>
|
onChange={onChange}
|
||||||
</div>
|
onSubmit={installFromInput}
|
||||||
<div className="flex-initial">
|
iconRight={
|
||||||
<Button
|
<Icon
|
||||||
primary
|
className={styles.icon}
|
||||||
label="Install"
|
material="folder_open"
|
||||||
className="w-80 h-full"
|
onClick={prevDefault(installFromSelectFileDialog)}
|
||||||
disabled={ExtensionInstallationStateStore.anyPreInstallingOrInstalling}
|
tooltip="Browse"
|
||||||
waiting={ExtensionInstallationStateStore.anyPreInstallingOrInstalling}
|
/>
|
||||||
onClick={installFromInput}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<small className="mt-3">
|
<div className="flex-initial">
|
||||||
<b>Pro-Tip</b>: you can drag-n-drop tarball-file to this area
|
<Button
|
||||||
</small>
|
primary
|
||||||
</section>
|
label="Install"
|
||||||
);
|
className="w-80 h-full"
|
||||||
});
|
disabled={
|
||||||
|
extensionInstallationStateStore.anyPreInstallingOrInstalling
|
||||||
|
}
|
||||||
|
waiting={extensionInstallationStateStore.anyPreInstallingOrInstalling}
|
||||||
|
onClick={installFromInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small className="mt-3">
|
||||||
|
<b>Pro-Tip</b>: you can drag-n-drop tarball-file to this area
|
||||||
|
</small>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Install = withInjectables<Dependencies, Props>(
|
||||||
|
observer(NonInjectedInstall),
|
||||||
|
{
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
extensionInstallationStateStore: di.inject(
|
||||||
|
extensionInstallationStateStoreInjectable,
|
||||||
|
),
|
||||||
|
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -21,16 +21,25 @@
|
|||||||
|
|
||||||
import styles from "./installed-extensions.module.scss";
|
import styles from "./installed-extensions.module.scss";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery";
|
import type {
|
||||||
|
ExtensionDiscovery,
|
||||||
|
InstalledExtension,
|
||||||
|
} from "../../../extensions/extension-discovery/extension-discovery";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { List } from "../list/list";
|
import { List } from "../list/list";
|
||||||
import { MenuActions, MenuItem } from "../menu";
|
import { MenuActions, MenuItem } from "../menu";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { ExtensionInstallationStateStore } from "./extension-install.store";
|
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import type { Row } from "react-table";
|
import type { Row } from "react-table";
|
||||||
import type { LensExtensionId } from "../../../extensions/lens-extension";
|
import type { LensExtensionId } from "../../../extensions/lens-extension";
|
||||||
|
import extensionDiscoveryInjectable
|
||||||
|
from "../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||||
|
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import extensionInstallationStateStoreInjectable
|
||||||
|
from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
|
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
extensions: InstalledExtension[];
|
extensions: InstalledExtension[];
|
||||||
@ -39,6 +48,11 @@ interface Props {
|
|||||||
uninstall: (extension: InstalledExtension) => void;
|
uninstall: (extension: InstalledExtension) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
extensionDiscovery: ExtensionDiscovery;
|
||||||
|
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||||
|
}
|
||||||
|
|
||||||
function getStatus(extension: InstalledExtension) {
|
function getStatus(extension: InstalledExtension) {
|
||||||
if (!extension.isCompatible) {
|
if (!extension.isCompatible) {
|
||||||
return "Incompatible";
|
return "Incompatible";
|
||||||
@ -47,7 +61,7 @@ function getStatus(extension: InstalledExtension) {
|
|||||||
return extension.isEnabled ? "Enabled" : "Disabled";
|
return extension.isEnabled ? "Enabled" : "Disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable }: Props) => {
|
const NonInjectedInstalledExtensions : React.FC<Dependencies & Props> = (({ extensionDiscovery, extensionInstallationStateStore, extensions, uninstall, enable, disable }) => {
|
||||||
const filters = [
|
const filters = [
|
||||||
(extension: InstalledExtension) => extension.manifest.name,
|
(extension: InstalledExtension) => extension.manifest.name,
|
||||||
(extension: InstalledExtension) => getStatus(extension),
|
(extension: InstalledExtension) => getStatus(extension),
|
||||||
@ -93,7 +107,7 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
|
|||||||
return extensions.map(extension => {
|
return extensions.map(extension => {
|
||||||
const { id, isEnabled, isCompatible, manifest } = extension;
|
const { id, isEnabled, isCompatible, manifest } = extension;
|
||||||
const { name, description, version } = manifest;
|
const { name, description, version } = manifest;
|
||||||
const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id);
|
const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
extension: (
|
extension: (
|
||||||
@ -145,10 +159,10 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [extensions, ExtensionInstallationStateStore.anyUninstalling],
|
}, [extensions, extensionInstallationStateStore.anyUninstalling],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!ExtensionDiscovery.getInstance().isLoaded) {
|
if (!extensionDiscovery.isLoaded) {
|
||||||
return <div><Spinner center /></div>;
|
return <div><Spinner center /></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,3 +190,16 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const InstalledExtensions = withInjectables<Dependencies, Props>(
|
||||||
|
observer(NonInjectedInstalledExtensions),
|
||||||
|
|
||||||
|
{
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||||
|
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||||
|
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -21,11 +21,17 @@
|
|||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
import { uninstallExtension } from "./uninstall-extension";
|
import { uninstallExtension } from "./uninstall-extension";
|
||||||
|
import extensionInstallationStateStoreInjectable
|
||||||
|
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
|
import extensionDiscoveryInjectable
|
||||||
|
from "../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||||
|
|
||||||
const uninstallExtensionInjectable = getInjectable({
|
const uninstallExtensionInjectable = getInjectable({
|
||||||
instantiate: (di) =>
|
instantiate: (di) =>
|
||||||
uninstallExtension({
|
uninstallExtension({
|
||||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||||
|
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||||
|
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|||||||
@ -21,28 +21,30 @@
|
|||||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||||
import { extensionDisplayName, LensExtensionId } from "../../../../extensions/lens-extension";
|
import { extensionDisplayName, LensExtensionId } from "../../../../extensions/lens-extension";
|
||||||
import logger from "../../../../main/logger";
|
import logger from "../../../../main/logger";
|
||||||
import { ExtensionInstallationStateStore } from "../extension-install.store";
|
import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery";
|
||||||
import { ExtensionDiscovery } from "../../../../extensions/extension-discovery";
|
|
||||||
import { Notifications } from "../../notifications";
|
import { Notifications } from "../../notifications";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { when } from "mobx";
|
import { when } from "mobx";
|
||||||
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
||||||
|
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
extensionLoader: ExtensionLoader
|
extensionLoader: ExtensionLoader
|
||||||
|
extensionDiscovery: ExtensionDiscovery
|
||||||
|
extensionInstallationStateStore: ExtensionInstallationStateStore
|
||||||
}
|
}
|
||||||
|
|
||||||
export const uninstallExtension =
|
export const uninstallExtension =
|
||||||
({ extensionLoader }: Dependencies) =>
|
({ extensionLoader, extensionDiscovery, extensionInstallationStateStore }: Dependencies) =>
|
||||||
async (extensionId: LensExtensionId): Promise<boolean> => {
|
async (extensionId: LensExtensionId): Promise<boolean> => {
|
||||||
const { manifest } = extensionLoader.getExtension(extensionId);
|
const { manifest } = extensionLoader.getExtension(extensionId);
|
||||||
const displayName = extensionDisplayName(manifest.name, manifest.version);
|
const displayName = extensionDisplayName(manifest.name, manifest.version);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug(`[EXTENSIONS]: trying to uninstall ${extensionId}`);
|
logger.debug(`[EXTENSIONS]: trying to uninstall ${extensionId}`);
|
||||||
ExtensionInstallationStateStore.setUninstalling(extensionId);
|
extensionInstallationStateStore.setUninstalling(extensionId);
|
||||||
|
|
||||||
await ExtensionDiscovery.getInstance().uninstallExtension(extensionId);
|
await extensionDiscovery.uninstallExtension(extensionId);
|
||||||
|
|
||||||
// wait for the ExtensionLoader to actually uninstall the extension
|
// wait for the ExtensionLoader to actually uninstall the extension
|
||||||
await when(() => !extensionLoader.userExtensions.has(extensionId));
|
await when(() => !extensionLoader.userExtensions.has(extensionId));
|
||||||
@ -71,6 +73,6 @@ export const uninstallExtension =
|
|||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
// Remove uninstall state on uninstall failure
|
// Remove uninstall state on uninstall failure
|
||||||
ExtensionInstallationStateStore.clearUninstalling(extensionId);
|
extensionInstallationStateStore.clearUninstalling(extensionId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,13 +20,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createContainer } from "@ogre-tools/injectable";
|
import { createContainer } from "@ogre-tools/injectable";
|
||||||
|
import { setLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api";
|
||||||
|
|
||||||
export const getDi = () =>
|
export const getDi = () => {
|
||||||
createContainer(
|
const di = createContainer(
|
||||||
getRequireContextForRendererCode,
|
getRequireContextForRendererCode,
|
||||||
getRequireContextForCommonExtensionCode,
|
getRequireContextForCommonExtensionCode,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setLegacyGlobalDiForExtensionApi(di);
|
||||||
|
|
||||||
|
return di;
|
||||||
|
};
|
||||||
|
|
||||||
const getRequireContextForRendererCode = () =>
|
const getRequireContextForRendererCode = () =>
|
||||||
require.context("../", true, /\.injectable\.(ts|tsx)$/);
|
require.context("../", true, /\.injectable\.(ts|tsx)$/);
|
||||||
|
|
||||||
|
|||||||
@ -26,10 +26,13 @@ import {
|
|||||||
createContainer,
|
createContainer,
|
||||||
ConfigurableDependencyInjectionContainer,
|
ConfigurableDependencyInjectionContainer,
|
||||||
} from "@ogre-tools/injectable";
|
} from "@ogre-tools/injectable";
|
||||||
|
import { setLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api";
|
||||||
|
|
||||||
export const getDiForUnitTesting = () => {
|
export const getDiForUnitTesting = () => {
|
||||||
const di: ConfigurableDependencyInjectionContainer = createContainer();
|
const di: ConfigurableDependencyInjectionContainer = createContainer();
|
||||||
|
|
||||||
|
setLegacyGlobalDiForExtensionApi(di);
|
||||||
|
|
||||||
getInjectableFilePaths()
|
getInjectableFilePaths()
|
||||||
.map(key => {
|
.map(key => {
|
||||||
const injectable = require(key).default;
|
const injectable = require(key).default;
|
||||||
|
|||||||
@ -21,11 +21,14 @@
|
|||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
import { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer";
|
import { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer";
|
||||||
|
import extensionsStoreInjectable
|
||||||
|
from "../../../extensions/extensions-store/extensions-store.injectable";
|
||||||
|
|
||||||
const lensProtocolRouterRendererInjectable = getInjectable({
|
const lensProtocolRouterRendererInjectable = getInjectable({
|
||||||
instantiate: (di) =>
|
instantiate: (di) =>
|
||||||
new LensProtocolRouterRenderer({
|
new LensProtocolRouterRenderer({
|
||||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||||
|
extensionsStore: di.inject(extensionsStoreInjectable),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|||||||
@ -27,6 +27,7 @@ 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";
|
import type { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||||
|
import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store";
|
||||||
|
|
||||||
function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] {
|
function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] {
|
||||||
if (args.length !== 2) {
|
if (args.length !== 2) {
|
||||||
@ -49,6 +50,7 @@ function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] {
|
|||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
extensionLoader: ExtensionLoader
|
extensionLoader: ExtensionLoader
|
||||||
|
extensionsStore: ExtensionsStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user