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

Fix extensions not being able to be installed in some cases

- Specifically, when an empty folder exists with the name that would be
  used to install it

- Make extensions and IPC more injected, so that
  ExtensionInstallationStateStore can be removed

- Add test to cover bug

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-02-01 10:43:06 -05:00
parent c46d0036cc
commit c2a359295b
128 changed files with 1728 additions and 1161 deletions

View File

@ -78,7 +78,7 @@ describe("BaseStore", () => {
let store: TestStore; let store: TestStore;
beforeEach(async () => { beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true }); const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
dis.mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory"); dis.mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory");

View File

@ -80,7 +80,7 @@ describe("cluster-store", () => {
let createCluster: (model: ClusterModel) => Cluster; let createCluster: (model: ClusterModel) => Cluster;
beforeEach(async () => { beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true }); const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
mockFs(); mockFs();

View File

@ -113,7 +113,7 @@ const awsCluster = getMockCatalogEntity({
describe("HotbarStore", () => { describe("HotbarStore", () => {
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");

View File

@ -40,7 +40,7 @@ describe("user store tests", () => {
let mainDi: DependencyInjectionContainer; let mainDi: DependencyInjectionContainer;
beforeEach(async () => { beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true }); const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
mockFs(); mockFs();

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { AppPaths } from "./app-paths";
import type { Channel } from "../communication/channel";
export type AppPathsChannel = Channel<[], AppPaths>;
export const appPathsInjectionChannelToken = getInjectionToken<AppPathsChannel>();
export const appPathsIpcChannel = "app-paths";

View File

@ -3,13 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectionToken } from "@ogre-tools/injectable"; import { getInjectionToken } from "@ogre-tools/injectable";
import type { PathName } from "./app-path-names"; import type { AppPaths } from "./app-paths";
import { createChannel } from "../ipc-channel/create-channel/create-channel";
export type AppPaths = Record<PathName, string>;
export const appPathsInjectionToken = getInjectionToken<AppPaths>(); export const appPathsInjectionToken = getInjectionToken<AppPaths>();
export const appPathsIpcChannel = createChannel<AppPaths>("app-paths");

View File

@ -3,22 +3,22 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
import { AppPaths, appPathsInjectionToken } from "./app-path-injection-token";
import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable"; import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable";
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
import type { PathName } from "./app-path-names"; import type { AppPaths, PathName } from "./app-paths";
import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable"; import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable";
import appNameInjectable from "../../main/app-paths/app-name/app-name.injectable"; import appNameInjectable from "../../main/app-paths/app-name/app-name.injectable";
import directoryForIntegrationTestingInjectable from "../../main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable"; import directoryForIntegrationTestingInjectable from "../../main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable";
import path from "path"; import path from "path";
import { appPathsInjectionToken } from "./app-path-injection-token";
describe("app-paths", () => { describe("app-paths", () => {
let mainDi: DependencyInjectionContainer; let mainDi: DependencyInjectionContainer;
let rendererDi: DependencyInjectionContainer; let rendererDi: DependencyInjectionContainer;
let runSetups: () => Promise<void[]>; let runSetups: () => Promise<void>;
beforeEach(() => { beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true }); const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
mainDi = dis.mainDi; mainDi = dis.mainDi;
rendererDi = dis.rendererDi; rendererDi = dis.rendererDi;
@ -45,15 +45,12 @@ describe("app-paths", () => {
mainDi.override( mainDi.override(
getElectronAppPathInjectable, getElectronAppPathInjectable,
() => () => (key: PathName): string | null => defaultAppPathsStub[key],
(key: PathName): string | null =>
defaultAppPathsStub[key],
); );
mainDi.override( mainDi.override(
setElectronAppPathInjectable, setElectronAppPathInjectable,
() => () => (key: PathName, path: string): void => {
(key: PathName, path: string): void => {
defaultAppPathsStub[key] = path; defaultAppPathsStub[key] = path;
}, },
); );
@ -123,7 +120,7 @@ describe("app-paths", () => {
await runSetups(); await runSetups();
}); });
it("given in renderer, when injecting path for app data, has integration specific app data path", () => { it("when in renderer, when injecting path for app data, has integration specific app data path", () => {
const { appData, userData } = rendererDi.inject(appPathsInjectionToken); const { appData, userData } = rendererDi.inject(appPathsInjectionToken);
expect({ appData, userData }).toEqual({ expect({ appData, userData }).toEqual({
@ -132,8 +129,8 @@ describe("app-paths", () => {
}); });
}); });
it("given in main, when injecting path for app data, has integration specific app data path", () => { it("when in main, when injecting path for app data, has integration specific app data path", () => {
const { appData, userData } = rendererDi.inject(appPathsInjectionToken); const { appData, userData } = mainDi.inject(appPathsInjectionToken);
expect({ appData, userData }).toEqual({ expect({ appData, userData }).toEqual({
appData: "some-integration-testing-app-data", appData: "some-integration-testing-app-data",

View File

@ -4,6 +4,7 @@
*/ */
import type { app as electronApp } from "electron"; import type { app as electronApp } from "electron";
export type AppPaths = Record<PathName, string>;
export type PathName = Parameters<typeof electronApp["getPath"]>[0]; export type PathName = Parameters<typeof electronApp["getPath"]>[0];
export const pathNames: PathName[] = [ export const pathNames: PathName[] = [

View File

@ -7,8 +7,7 @@ import path from "path";
import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable";
const directoryForBinariesInjectable = getInjectable({ const directoryForBinariesInjectable = getInjectable({
instantiate: (di) => instantiate: (di) => path.join(di.inject(directoryForUserDataInjectable), "binaries"),
path.join(di.inject(directoryForUserDataInjectable), "binaries"),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -7,8 +7,7 @@ import directoryForUserDataInjectable from "../directory-for-user-data/directory
import path from "path"; import path from "path";
const directoryForKubeConfigsInjectable = getInjectable({ const directoryForKubeConfigsInjectable = getInjectable({
instantiate: (di) => instantiate: (di) => path.resolve(di.inject(directoryForUserDataInjectable), "kubeconfigs"),
path.resolve(di.inject(directoryForUserDataInjectable), "kubeconfigs"),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { broadcastMessage } from "../ipc";
const broadcastInjectable = getInjectable({
instantiate: () => broadcastMessage as (channel: string, ...args: any[]) => void,
lifecycle: lifecycleEnum.singleton,
});
export default broadcastInjectable;

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* A Channel represent an link that renderer can request on, given some
* parameters, and get a value back
*/
export type Channel<Parameters extends any[], Value> = (...args: Parameters) => Promise<Value>;
export type ChannelValue<T> = T extends Channel<any, infer Value>
? Value
: never;
export type ChannelParameters<T> = T extends Channel<infer Parameters, any>
? Parameters
: never;

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* An EmitterChannel represents a broadcast point where any side can emit data
* on
*/
export type EmitterChannel<Parameters extends any[]> = (...args: Parameters) => void;

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
export type IpcOnEvent = (channel: string, ...args: any[]) => void;
export const ipcOnEventInjectionToken = getInjectionToken<IpcOnEvent>();

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { LensLogger } from "../logger";
import broadcastInjectable from "./broadcast.injectable";
import type { EmitterChannel } from "./emitter";
interface Dependencies {
broadcast: (name: string, ...args: any[]) => void;
}
function registerEmitterChannel({ broadcast }: Dependencies) {
return function <Parameters extends any[]>(name: string, logger?: LensLogger): EmitterChannel<Parameters> {
return (...args) => {
logger?.info(`Broadcasting on ${name}`, { args });
broadcast(name, ...args);
};
};
}
/**
* This dependency is for registering the source of events
*/
const registerEmitterChannelInjectable = getInjectable({
instantiate: (di) => registerEmitterChannel({
broadcast: di.inject(broadcastInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default registerEmitterChannelInjectable;

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { EmitterChannel } from "../../common/communication/emitter";
import type { LensLogger } from "../../common/logger";
import { ipcOnEventInjectionToken } from "./ipc-on-event-injection-token";
interface Depencencies {
onEvent: (channel: string, ...args: any[]) => void;
}
const registerEventSink = ({ onEvent }: Depencencies) => (
function <Parameters extends any[]>(name: string, listener: (...args: Parameters) => void, logger?: LensLogger): EmitterChannel<Parameters> {
onEvent(name, (...args: Parameters) => {
logger?.info(`Received event on ${name}`, { args });
listener(...args);
});
return listener;
}
);
const registerEventSinkInjectable = getInjectable({
instantiate: (di) => registerEventSink({
onEvent: di.inject(ipcOnEventInjectionToken),
}),
lifecycle: lifecycleEnum.singleton,
});
export default registerEventSinkInjectable;

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable";
export type RemoveDir = (dir: string) => Promise<void>;
const removeDirInjectable = getInjectable({
instantiate: (di) => di.inject(fsInjectable).remove as RemoveDir,
lifecycle: lifecycleEnum.singleton,
});
export default removeDirInjectable;

View File

@ -1,8 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export interface Channel<TInstance> {
name: string;
_template: TInstance;
}

View File

@ -1,10 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Channel } from "../channel";
export const createChannel = <TInstance>(name: string): Channel<TInstance> => ({
name,
_template: null,
});

View File

@ -10,6 +10,15 @@ import { consoleFormat } from "winston-console-format";
import { isDebugging, isTestEnv } from "./vars"; import { isDebugging, isTestEnv } from "./vars";
import BrowserConsole from "winston-transport-browserconsole"; import BrowserConsole from "winston-transport-browserconsole";
export interface LensLogger {
error: (...args: any[]) => void;
warn: (...args: any[]) => void;
info: (...args: any[]) => void;
debug: (...args: any[]) => void;
verbose: (...args: any[]) => void;
silly: (...args: any[]) => void;
}
const logLevel = process.env.LOG_LEVEL const logLevel = process.env.LOG_LEVEL
? process.env.LOG_LEVEL ? process.env.LOG_LEVEL
: isDebugging : isDebugging
@ -67,4 +76,4 @@ if (ipcMain) {
export default winston.createLogger({ export default winston.createLogger({
format: format.simple(), format: format.simple(),
transports, transports,
}); }) as LensLogger;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import logger from "../logger";
const baseLoggerInjectable = getInjectable({
instantiate: () => logger,
lifecycle: lifecycleEnum.singleton,
});
export default baseLoggerInjectable;

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { LensLogger } from "../logger";
import baseLoggerInjectable from "./base-logger.injectable";
interface Dependencies {
baseLogger: LensLogger;
}
const createChildLogger = ({ baseLogger }: Dependencies) => (
(prefix: string): LensLogger => {
return {
debug: (message, info) => baseLogger.debug(`${prefix}: ${message}`, info),
warn: (message, info) => baseLogger.warn(`${prefix}: ${message}`, info),
error: (message, info) => baseLogger.error(`${prefix}: ${message}`, info),
verbose: (message, info) => baseLogger.verbose(`${prefix}: ${message}`, info),
info: (message, info) => baseLogger.info(`${prefix}: ${message}`, info),
silly: (message, info) => baseLogger.silly(`${prefix}: ${message}`, info),
};
}
);
const createChildLoggerInjectable = getInjectable({
instantiate: (di) => createChildLogger({
baseLogger: di.inject(baseLoggerInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createChildLoggerInjectable;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { v4 } from "uuid";
const uniqueIdInjectable = getInjectable({
instantiate: () => v4,
lifecycle: lifecycleEnum.singleton,
});
export default uniqueIdInjectable;

View File

@ -110,7 +110,7 @@ describe("ExtensionLoader", () => {
let updateExtensionStateMock: jest.Mock; let updateExtensionStateMock: jest.Mock;
beforeEach(async () => { beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true }); const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
mockFs(); mockFs();

View File

@ -8,33 +8,22 @@ import extensionLoaderInjectable from "../extension-loader/extension-loader.inje
import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable"; import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable";
import isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable"; import isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable";
import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable"; import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable";
import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable";
import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable"; import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable";
import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable"; import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable";
import installExtensionsInjectable from "../extension-installer/install-extensions/install-extensions.injectable"; import installExtensionsInjectable from "../extension-installer/install-extensions/install-extensions.injectable";
import { clearInstallingChannelInjectionToken, setInstallingChannelInjectionToken } from "../installation-state/state-channels";
const extensionDiscoveryInjectable = getInjectable({ const extensionDiscoveryInjectable = getInjectable({
instantiate: (di) => instantiate: (di) => new ExtensionDiscovery({
new ExtensionDiscovery({
extensionLoader: di.inject(extensionLoaderInjectable), extensionLoader: di.inject(extensionLoaderInjectable),
extensionsStore: di.inject(extensionsStoreInjectable), extensionsStore: di.inject(extensionsStoreInjectable),
setInstalling: di.inject(setInstallingChannelInjectionToken),
extensionInstallationStateStore: di.inject( clearInstalling: di.inject(clearInstallingChannelInjectionToken),
extensionInstallationStateStoreInjectable, isCompatibleBundledExtension: di.inject(isCompatibleBundledExtensionInjectable),
),
isCompatibleBundledExtension: di.inject(
isCompatibleBundledExtensionInjectable,
),
isCompatibleExtension: di.inject(isCompatibleExtensionInjectable), isCompatibleExtension: di.inject(isCompatibleExtensionInjectable),
installExtension: di.inject(installExtensionInjectable), installExtension: di.inject(installExtensionInjectable),
installExtensions: di.inject(installExtensionsInjectable), installExtensions: di.inject(installExtensionsInjectable),
extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable),
extensionPackageRootDirectory: di.inject(
extensionPackageRootDirectoryInjectable,
),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,

View File

@ -11,10 +11,8 @@ import * as fse from "fs-extra";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import extensionDiscoveryInjectable from "../extension-discovery/extension-discovery.injectable"; import extensionDiscoveryInjectable from "../extension-discovery/extension-discovery.injectable";
import type { ExtensionDiscovery } from "../extension-discovery/extension-discovery"; import type { ExtensionDiscovery } from "../extension-discovery/extension-discovery";
import installExtensionInjectable import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable";
from "../extension-installer/install-extension/install-extension.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForUserDataInjectable
from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
jest.setTimeout(60_000); jest.setTimeout(60_000);
@ -49,16 +47,15 @@ describe("ExtensionDiscovery", () => {
let extensionDiscovery: ExtensionDiscovery; let extensionDiscovery: ExtensionDiscovery;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(installExtensionInjectable, () => () => Promise.resolve()); di.override(installExtensionInjectable, () => () => Promise.resolve());
mockFs();
await di.runSetups(); await di.runSetups();
extensionDiscovery = di.inject(extensionDiscoveryInjectable); extensionDiscovery = di.inject(extensionDiscoveryInjectable);
mockFs();
}); });
afterEach(() => { afterEach(() => {

View File

@ -17,7 +17,6 @@ import type { ExtensionsStore } from "../extensions-store/extensions-store";
import type { ExtensionLoader } from "../extension-loader"; import type { ExtensionLoader } from "../extension-loader";
import type { LensExtensionId, LensExtensionManifest } from "../lens-extension"; import type { LensExtensionId, LensExtensionManifest } from "../lens-extension";
import { isProduction } from "../../common/vars"; import { isProduction } from "../../common/vars";
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
import type { PackageJson } from "type-fest"; import type { PackageJson } from "type-fest";
import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling"; import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling";
import { requestInitialExtensionDiscovery } from "../../renderer/ipc"; import { requestInitialExtensionDiscovery } from "../../renderer/ipc";
@ -25,12 +24,10 @@ import { requestInitialExtensionDiscovery } from "../../renderer/ipc";
interface Dependencies { interface Dependencies {
extensionLoader: ExtensionLoader; extensionLoader: ExtensionLoader;
extensionsStore: ExtensionsStore; extensionsStore: ExtensionsStore;
setInstalling: (extId: string) => void;
extensionInstallationStateStore: ExtensionInstallationStateStore; clearInstalling: (extId: string) => void;
isCompatibleBundledExtension: (manifest: LensExtensionManifest) => boolean; isCompatibleBundledExtension: (manifest: LensExtensionManifest) => boolean;
isCompatibleExtension: (manifest: LensExtensionManifest) => boolean; isCompatibleExtension: (manifest: LensExtensionManifest) => boolean;
installExtension: (name: string) => Promise<void>; installExtension: (name: string) => Promise<void>;
installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise<void> installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise<void>
extensionPackageRootDirectory: string; extensionPackageRootDirectory: string;
@ -191,7 +188,7 @@ export class ExtensionDiscovery {
if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) { if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) {
try { try {
this.dependencies.extensionInstallationStateStore.setInstallingFromMain(manifestPath); this.dependencies.setInstalling(manifestPath);
const absPath = path.dirname(manifestPath); const absPath = path.dirname(manifestPath);
// this.loadExtensionFromPath updates this.packagesJson // this.loadExtensionFromPath updates this.packagesJson
@ -211,7 +208,7 @@ export class ExtensionDiscovery {
} catch (error) { } catch (error) {
logger.error(`${logModule}: failed to add extension: ${error}`, { error }); logger.error(`${logModule}: failed to add extension: ${error}`, { error });
} finally { } finally {
this.dependencies.extensionInstallationStateStore.clearInstallingFromMain(manifestPath); this.dependencies.clearInstalling(manifestPath);
} }
} }
}; };

View File

@ -1,13 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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;

View File

@ -1,244 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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;
}
}

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "./extension-loader.injectable";
const getInstalledExtensionInjectable = getInjectable({
instantiate: (di) => {
const store = di.inject(extensionLoaderInjectable);
return (extId: string) => store.getExtension(extId);
},
lifecycle: lifecycleEnum.singleton,
});
export default getInstalledExtensionInjectable;

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import createChildLoggerInjectable from "../../common/logger/create-child-logger.injectable";
const installationStateLoggerInjectable = getInjectable({
instantiate: (di) => {
const createChildLogger = di.inject(createChildLoggerInjectable);
return createChildLogger("[ExtensionInstallationStore]");
},
lifecycle: lifecycleEnum.singleton,
});
export default installationStateLoggerInjectable;

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { EmitterChannel } from "../../common/communication/emitter";
export type SetInstalling = EmitterChannel<[extId: string]>;
export const setInstallingChannel = "extension-installation-state-store:install";
export const setInstallingChannelInjectionToken = getInjectionToken<SetInstalling>();
export type ClearInstalling = EmitterChannel<[extId: string]>;
export const clearInstallingChannel = "extension-installation-state-store:clear-install";
export const clearInstallingChannelInjectionToken = getInjectionToken<ClearInstalling>();

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* The possible installation states
*/
export enum InstallationState {
INSTALLING = "installing",
UNINSTALLING = "uninstalling",
IDLE = "idle",
}

View File

@ -37,7 +37,7 @@ let ext: LensExtension = null;
describe("page registry tests", () => { describe("page registry tests", () => {
beforeEach(async () => { beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true }); const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
mockFs(); mockFs();

View File

@ -51,7 +51,7 @@ describe("create clusters", () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks(); jest.clearAllMocks();
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
mockFs({ mockFs({
"minikube-config.yml": JSON.stringify({ "minikube-config.yml": JSON.stringify({

View File

@ -74,7 +74,7 @@ describe("ContextHandler", () => {
let createContextHandler: (cluster: Cluster) => ContextHandler; let createContextHandler: (cluster: Cluster) => ContextHandler;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
mockFs({ mockFs({
"tmp": {}, "tmp": {},

View File

@ -89,7 +89,7 @@ describe("kube auth proxy tests", () => {
"tmp": {}, "tmp": {},
}; };
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
mockFs(mockMinikubeConfig); mockFs(mockMinikubeConfig);

View File

@ -48,7 +48,7 @@ describe("kubeconfig manager tests", () => {
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForTempInjectable, () => "some-directory-for-temp"); di.override(directoryForTempInjectable, () => "some-directory-for-temp");

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { appPathsInjectionChannelToken, appPathsIpcChannel } from "../../common/app-paths/app-path-channel-injection-token";
import registerChannelInjectable from "../communication/register-channel.injectable";
import type { Channel } from "../../common/communication/channel";
import type { AppPaths } from "../../common/app-paths/app-paths";
import { appPathsInjectionToken } from "../../common/app-paths/app-path-injection-token";
let channel: Channel<[], AppPaths>;
const appPathsInjectable = getInjectable({
setup: (di) => {
const appPaths = di.inject(appPathsInjectionToken);
const registerChannel = di.inject(registerChannelInjectable);
channel = registerChannel(appPathsIpcChannel, () => appPaths);
},
instantiate: () => channel,
injectionToken: appPathsInjectionChannelToken,
lifecycle: lifecycleEnum.singleton,
});
export default appPathsInjectable;

View File

@ -2,67 +2,38 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
DependencyInjectionContainer,
getInjectable,
lifecycleEnum,
} from "@ogre-tools/injectable";
import {
appPathsInjectionToken,
appPathsIpcChannel,
} from "../../common/app-paths/app-path-injection-token";
import registerChannelInjectable from "./register-channel/register-channel.injectable";
import { getAppPaths } from "./get-app-paths"; import { getAppPaths } from "./get-app-paths";
import getElectronAppPathInjectable from "./get-electron-app-path/get-electron-app-path.injectable"; import getElectronAppPathInjectable from "./get-electron-app-path/get-electron-app-path.injectable";
import setElectronAppPathInjectable from "./set-electron-app-path/set-electron-app-path.injectable"; import setElectronAppPathInjectable from "./set-electron-app-path/set-electron-app-path.injectable";
import path from "path"; import path from "path";
import appNameInjectable from "./app-name/app-name.injectable"; import appNameInjectable from "./app-name/app-name.injectable";
import directoryForIntegrationTestingInjectable from "./directory-for-integration-testing/directory-for-integration-testing.injectable"; import directoryForIntegrationTestingInjectable from "./directory-for-integration-testing/directory-for-integration-testing.injectable";
import { appPathsInjectionToken } from "../../common/app-paths/app-path-injection-token";
const appPathsInjectable = getInjectable({ const appPathsInjectable = getInjectable({
setup: (di) => { instantiate: (di) => {
const directoryForIntegrationTesting = di.inject( const directoryForIntegrationTesting = di.inject(directoryForIntegrationTestingInjectable);
directoryForIntegrationTestingInjectable, const setElectronAppPath = di.inject(setElectronAppPathInjectable);
);
if (directoryForIntegrationTesting) { if (directoryForIntegrationTesting) {
setupPathForAppDataInIntegrationTesting(di, directoryForIntegrationTesting); // TODO: this kludge is here only until we have a proper place to setup integration testing.
setElectronAppPath("appData", directoryForIntegrationTesting);
} }
setupPathForUserData(di); // Set path for user data
registerAppPathsChannel(di); const appName = di.inject(appNameInjectable);
const getAppPath = di.inject(getElectronAppPathInjectable);
const appDataPath = getAppPath("appData");
setElectronAppPath("userData", path.join(appDataPath, appName));
return getAppPaths({
getAppPath: di.inject(getElectronAppPathInjectable),
});
}, },
instantiate: (di) =>
getAppPaths({ getAppPath: di.inject(getElectronAppPathInjectable) }),
injectionToken: appPathsInjectionToken, injectionToken: appPathsInjectionToken,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });
export default appPathsInjectable; export default appPathsInjectable;
const registerAppPathsChannel = (di: DependencyInjectionContainer) => {
const registerChannel = di.inject(registerChannelInjectable);
registerChannel(appPathsIpcChannel, () => di.inject(appPathsInjectable));
};
const setupPathForUserData = (di: DependencyInjectionContainer) => {
const setElectronAppPath = di.inject(setElectronAppPathInjectable);
const appName = di.inject(appNameInjectable);
const getAppPath = di.inject(getElectronAppPathInjectable);
const appDataPath = getAppPath("appData");
setElectronAppPath("userData", path.join(appDataPath, appName));
};
// Todo: this kludge is here only until we have a proper place to setup integration testing.
const setupPathForAppDataInIntegrationTesting = (di: DependencyInjectionContainer, appDataPath: string) => {
const setElectronAppPath = di.inject(setElectronAppPathInjectable);
setElectronAppPath("appData", appDataPath);
};

View File

@ -2,13 +2,13 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { fromPairs } from "lodash/fp"; import { pathNames, PathName, AppPaths } from "../../common/app-paths/app-paths";
import { pathNames, PathName } from "../../common/app-paths/app-path-names"; import { fromEntries } from "../../renderer/utils";
import type { AppPaths } from "../../common/app-paths/app-path-injection-token";
interface Dependencies { interface Dependencies {
getAppPath: (name: PathName) => string getAppPath: (name: PathName) => string
} }
export const getAppPaths = ({ getAppPath }: Dependencies) => export function getAppPaths({ getAppPath }: Dependencies): AppPaths {
fromPairs(pathNames.map((name) => [name, getAppPath(name)])) as AppPaths; return fromEntries(pathNames.map((name) => [name, getAppPath(name)]));
}

View File

@ -6,13 +6,13 @@ import electronAppInjectable from "./electron-app/electron-app.injectable";
import getElectronAppPathInjectable from "./get-electron-app-path.injectable"; import getElectronAppPathInjectable from "./get-electron-app-path.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { App } from "electron"; import type { App } from "electron";
import registerChannelInjectable from "../register-channel/register-channel.injectable"; import registerChannelInjectable from "../../communication/register-channel.injectable";
describe("get-electron-app-path", () => { describe("get-electron-app-path", () => {
let getElectronAppPath: (name: string) => string | null; let getElectronAppPath: (name: string) => string | null;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: false }); const di = await getDiForUnitTesting({ doGeneralOverrides: false });
const appStub = { const appStub = {
name: "some-app-name", name: "some-app-name",

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { App } from "electron"; import type { App } from "electron";
import type { PathName } from "../../../common/app-paths/app-path-names"; import type { PathName } from "../../../common/app-paths/app-paths";
interface Dependencies { interface Dependencies {
app: App; app: App;

View File

@ -1,17 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import ipcMainInjectable from "./ipc-main/ipc-main.injectable";
import { registerChannel } from "./register-channel";
const registerChannelInjectable = getInjectable({
instantiate: (di) => registerChannel({
ipcMain: di.inject(ipcMainInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default registerChannelInjectable;

View File

@ -1,18 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IpcMain } from "electron";
import type { Channel } from "../../../common/ipc-channel/channel";
interface Dependencies {
ipcMain: IpcMain;
}
export const registerChannel =
({ ipcMain }: Dependencies) =>
<TChannel extends Channel<TInstance>, TInstance>(
channel: TChannel,
getValue: () => TInstance,
) =>
ipcMain.handle(channel.name, getValue);

View File

@ -3,12 +3,17 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { PathName } from "../../../common/app-paths/app-path-names"; import type { PathName } from "../../../common/app-paths/app-paths";
import electronAppInjectable from "../get-electron-app-path/electron-app/electron-app.injectable"; import electronAppInjectable from "../get-electron-app-path/electron-app/electron-app.injectable";
const setElectronAppPathInjectable = getInjectable({ const setElectronAppPathInjectable = getInjectable({
instantiate: (di) => (name: PathName, path: string) : void => instantiate: (di) => {
di.inject(electronAppInjectable).setPath(name, path), const app = di.inject(electronAppInjectable);
return (name: PathName, path: string): void => {
app.setPath(name, path);
};
},
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -38,7 +38,7 @@ describe("kubeconfig-sync.source tests", () => {
let computeDiff: ReturnType<typeof computeDiffFor>; let computeDiff: ReturnType<typeof computeDiffFor>;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
mockFs(); mockFs();

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IpcMainInvokeEvent } from "electron";
import ipcMainInjectable from "./ipc-main.injectable";
const ipcHandleInjectable = getInjectable({
instantiate: (di) => {
const ipcMain = di.inject(ipcMainInjectable);
return (channel: string, listener: (event: IpcMainInvokeEvent, ...args: any[]) => any): void => {
ipcMain.handle(channel, listener);
};
},
lifecycle: lifecycleEnum.singleton,
});
export default ipcHandleInjectable;

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IpcMainEvent } from "electron";
import { ipcOnEventInjectionToken } from "../../common/communication/ipc-on-event-injection-token";
import ipcMainInjectable from "./ipc-main.injectable";
const ipcOnInjectable = getInjectable({
instantiate: (di) => {
const ipcMain = di.inject(ipcMainInjectable);
return (channel: string, listener: (event: IpcMainEvent, ...args: any[]) => void): void => {
ipcMain.on(channel, listener);
};
},
injectionToken: ipcOnEventInjectionToken,
lifecycle: lifecycleEnum.singleton,
});
export default ipcOnInjectable;

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IpcMainInvokeEvent } from "electron";
import type { Channel } from "../../common/communication/channel";
import ipcHandleInjectable from "./ipc-handle.injectable";
interface Dependencies {
handle: (channel: string, listener: (event: IpcMainInvokeEvent, ...args: any[]) => void) => void;
}
function registerChannel({ handle }: Dependencies) {
return function <Parameters extends any[], Value>(name: string, getValue: (...args: Parameters) => Value): Channel<Parameters, Value> {
handle(name, async (event, ...args: Parameters) => await getValue(...args));
return () => {
throw new Error(`Invoking channel ${name} on main is invalid`);
};
};
}
const registerChannelInjectable = getInjectable({
instantiate: (di) => registerChannel({
handle: di.inject(ipcHandleInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default registerChannelInjectable;

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import registerEmitterChannelInjectable from "../../../common/communication/register-emitter.injectable";
import installationStateLoggerInjectable from "../../../extensions/installation-state/logger.injectable";
import { clearInstallingChannel, clearInstallingChannelInjectionToken } from "../../../extensions/installation-state/state-channels";
const clearInstallingChannelInjectable = getInjectable({
instantiate: (di) => {
const registerEmitterChannel = di.inject(registerEmitterChannelInjectable);
const logger = di.inject(installationStateLoggerInjectable);
return registerEmitterChannel(clearInstallingChannel, logger);
},
injectionToken: clearInstallingChannelInjectionToken,
lifecycle: lifecycleEnum.singleton,
});
export default clearInstallingChannelInjectable;

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import registerEmitterChannelInjectable from "../../../common/communication/register-emitter.injectable";
import installationStateLoggerInjectable from "../../../extensions/installation-state/logger.injectable";
import { setInstallingChannel, setInstallingChannelInjectionToken } from "../../../extensions/installation-state/state-channels";
const setInstallingChannelInjectable = getInjectable({
instantiate: (di) => {
const registerEmitterChannel = di.inject(registerEmitterChannelInjectable);
const logger = di.inject(installationStateLoggerInjectable);
return registerEmitterChannel(setInstallingChannel, logger);
},
injectionToken: setInstallingChannelInjectionToken,
lifecycle: lifecycleEnum.singleton,
});
export default setInstallingChannelInjectable;

View File

@ -11,20 +11,22 @@ import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global
import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-electron-app-path.injectable"; import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-electron-app-path.injectable";
import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable"; import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable";
import appNameInjectable from "./app-paths/app-name/app-name.injectable"; import appNameInjectable from "./app-paths/app-name/app-name.injectable";
import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable"; import registerEventSinkInjectable from "../common/communication/register-event-sink.injectable";
import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; import registerChannelInjectable from "./communication/register-channel.injectable";
import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; import { overrideFsFunctions } from "../test-utils/override-fs-functions";
export const getDiForUnitTesting = ( interface DiForTestingOptions {
{ doGeneralOverrides } = { doGeneralOverrides: false }, doGeneralOverrides?: boolean;
) => { doIpcOverrides?: boolean;
}
export async function getDiForUnitTesting({ doGeneralOverrides = false, doIpcOverrides = true }: DiForTestingOptions = {}) {
const di = createContainer(); const di = createContainer();
setLegacyGlobalDiForExtensionApi(di); setLegacyGlobalDiForExtensionApi(di);
for (const filePath of getInjectableFilePaths()) { for (const filePath of getInjectableFilePaths()) {
// eslint-disable-next-line @typescript-eslint/no-var-requires const { default: injectableInstance } = await import(filePath);
const injectableInstance = require(filePath).default;
di.register({ di.register({
id: filePath, id: filePath,
@ -36,26 +38,21 @@ export const getDiForUnitTesting = (
di.preventSideEffects(); di.preventSideEffects();
if (doGeneralOverrides) { if (doGeneralOverrides) {
di.override( di.override(getElectronAppPathInjectable, () => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`);
getElectronAppPathInjectable,
() => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`,
);
di.override(setElectronAppPathInjectable, () => () => undefined); di.override(setElectronAppPathInjectable, () => () => undefined);
di.override(appNameInjectable, () => "some-electron-app-name"); di.override(appNameInjectable, () => "some-electron-app-name");
di.override(registerChannelInjectable, () => () => undefined);
di.override(writeJsonFileInjectable, () => () => { overrideFsFunctions(di);
throw new Error("Tried to write JSON file to file system without specifying explicit override."); }
});
di.override(readJsonFileInjectable, () => () => { if (doIpcOverrides) {
throw new Error("Tried to read JSON file from file system without specifying explicit override."); di.override(registerEventSinkInjectable, () => () => () => undefined);
}); di.override(registerChannelInjectable, () => () => () => undefined);
} }
return di; return di;
}; }
const getInjectableFilePaths = memoize(() => [ const getInjectableFilePaths = memoize(() => [
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }), ...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import ipcMainInjectable from "../communication/ipc-main.injectable";
const emitEventInjectable = getInjectable({
instantiate: (di) => {
const ipcMain = di.inject(ipcMainInjectable);
return (channel: string, ...args: any[]) => {
ipcMain.emit(channel, ...args);
};
},
lifecycle: lifecycleEnum.singleton,
});
export default emitEventInjectable;

View File

@ -9,7 +9,7 @@ import request from "request";
import { ensureDir, pathExists } from "fs-extra"; import { ensureDir, pathExists } from "fs-extra";
import * as tar from "tar"; import * as tar from "tar";
import { isWindows } from "../common/vars"; import { isWindows } from "../common/vars";
import type winston from "winston"; import type { LensLogger } from "../common/logger";
export type LensBinaryOpts = { export type LensBinaryOpts = {
version: string; version: string;
@ -32,7 +32,7 @@ export class LensBinary {
protected arch: string; protected arch: string;
protected originalBinaryName: string; protected originalBinaryName: string;
protected requestOpts: request.Options; protected requestOpts: request.Options;
protected logger: Console | winston.Logger; protected logger: Omit<LensLogger, "silly" | "verbose">;
constructor(opts: LensBinaryOpts) { constructor(opts: LensBinaryOpts) {
const baseDir = opts.baseDir; const baseDir = opts.baseDir;
@ -68,7 +68,7 @@ export class LensBinary {
} }
} }
public setLogger(logger: Console | winston.Logger) { public setLogger(logger: LensLogger) {
this.logger = logger; this.logger = logger;
} }

View File

@ -17,7 +17,7 @@ describe("electron-menu-items", () => {
let extensionsStub: ObservableMap<string, LensMainExtension>; let extensionsStub: ObservableMap<string, LensMainExtension>;
beforeEach(async () => { beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true }); di = await getDiForUnitTesting({ doGeneralOverrides: true });
extensionsStub = new ObservableMap(); extensionsStub = new ObservableMap();

View File

@ -36,7 +36,7 @@ describe("protocol router tests", () => {
let extensionsStore: ExtensionsStore; let extensionsStore: ExtensionsStore;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
mockFs({ mockFs({
"tmp": {}, "tmp": {},

View File

@ -17,7 +17,7 @@ describe("tray-menu-items", () => {
let extensionsStub: ObservableMap<string, LensMainExtension>; let extensionsStub: ObservableMap<string, LensMainExtension>;
beforeEach(async () => { beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true }); di = await getDiForUnitTesting({ doGeneralOverrides: true });
await di.runSetups(); await di.runSetups();

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { appPathsInjectionChannelToken, appPathsIpcChannel } from "../../common/app-paths/app-path-channel-injection-token";
import registerChannelInjectable from "../communication/register-channel.injectable";
const appPathsChannelInjectable = getInjectable({
instantiate: (di) => {
const registerChannel = di.inject(registerChannelInjectable);
return registerChannel(appPathsIpcChannel);
},
injectionToken: appPathsInjectionChannelToken,
lifecycle: lifecycleEnum.singleton,
});
export default appPathsChannelInjectable;

View File

@ -3,24 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { AppPaths, appPathsInjectionToken, appPathsIpcChannel } from "../../common/app-paths/app-path-injection-token"; import { appPathsInjectionChannelToken } from "../../common/app-paths/app-path-channel-injection-token";
import getValueFromRegisteredChannelInjectable from "./get-value-from-registered-channel/get-value-from-registered-channel.injectable"; import { appPathsInjectionToken } from "../../common/app-paths/app-path-injection-token";
import type { AppPaths } from "../../common/app-paths/app-paths";
let syncAppPaths: AppPaths; let syncAppPaths: AppPaths;
const appPathsInjectable = getInjectable({ const appPathsInjectable = getInjectable({
setup: async (di) => { setup: async (di) => {
const getValueFromRegisteredChannel = di.inject( const appPathsChannel = di.inject(appPathsInjectionChannelToken);
getValueFromRegisteredChannelInjectable,
);
syncAppPaths = await getValueFromRegisteredChannel(appPathsIpcChannel); syncAppPaths = await appPathsChannel();
}, },
instantiate: () => syncAppPaths, instantiate: () => syncAppPaths,
injectionToken: appPathsInjectionToken, injectionToken: appPathsInjectionToken,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -1,16 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable";
import { getValueFromRegisteredChannel } from "./get-value-from-registered-channel";
const getValueFromRegisteredChannelInjectable = getInjectable({
instantiate: (di) =>
getValueFromRegisteredChannel({ ipcRenderer: di.inject(ipcRendererInjectable) }),
lifecycle: lifecycleEnum.singleton,
});
export default getValueFromRegisteredChannelInjectable;

View File

@ -1,17 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IpcRenderer } from "electron";
import type { Channel } from "../../../common/ipc-channel/channel";
interface Dependencies {
ipcRenderer: IpcRenderer;
}
export const getValueFromRegisteredChannel =
({ ipcRenderer }: Dependencies) =>
<TChannel extends Channel<TInstance>, TInstance>(
channel: TChannel,
): Promise<TChannel["_template"]> =>
ipcRenderer.invoke(channel.name);

View File

@ -30,7 +30,6 @@ import { DiContextProvider } from "@ogre-tools/injectable-react";
import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable"; import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable"; import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable";
import extensionInstallationStateStoreInjectable from "../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
import userStoreInjectable from "../common/user-store/user-store.injectable"; import userStoreInjectable from "../common/user-store/user-store.injectable";
import initRootFrameInjectable from "./frames/root-frame/init-root-frame/init-root-frame.injectable"; import initRootFrameInjectable from "./frames/root-frame/init-root-frame/init-root-frame.injectable";
@ -114,10 +113,6 @@ export async function bootstrap(di: DependencyInjectionContainer) {
WeblinkStore.createInstance(); WeblinkStore.createInstance();
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

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import ipcRendererInjectable from "./ipc-renderer.injectable";
const ipcInvokeInjectable = getInjectable({
instantiate: (di) => {
const ipcRenderer = di.inject(ipcRendererInjectable);
return (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args);
},
lifecycle: lifecycleEnum.singleton,
});
export default ipcInvokeInjectable;

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IpcRendererEvent } from "electron";
import { ipcOnEventInjectionToken } from "../../common/communication/ipc-on-event-injection-token";
import ipcRendererInjectable from "./ipc-renderer.injectable";
const ipcOnInjectable = getInjectable({
instantiate: (di) => {
const ipcRenderer = di.inject(ipcRendererInjectable);
return (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): void => {
ipcRenderer.on(channel, listener);
};
},
injectionToken: ipcOnEventInjectionToken,
lifecycle: lifecycleEnum.singleton,
});
export default ipcOnInjectable;

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import ipcInvokeInjectable from "./ipc-invoke.injectable";
import type { Channel } from "../../common/communication/channel";
interface Dependencies {
invoke: (channel: string, ...args: any[]) => any;
}
function registerChannel({ invoke }: Dependencies) {
return function <Parameters extends any[], Value>(name: string): Channel<Parameters, Value> {
return (...args: Parameters) => invoke(name, ...args);
};
}
const registerChannelInjectable = getInjectable({
instantiate: (di) => registerChannel({
invoke: di.inject(ipcInvokeInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default registerChannelInjectable;

View File

@ -37,8 +37,8 @@ class TestCategory extends CatalogCategory {
describe("Custom Category Columns", () => { describe("Custom Category Columns", () => {
let di: ConfigurableDependencyInjectionContainer; let di: ConfigurableDependencyInjectionContainer;
beforeEach(() => { beforeEach(async () => {
di = getDiForUnitTesting(); di = await getDiForUnitTesting();
}); });
describe("without extensions", () => { describe("without extensions", () => {

View File

@ -15,8 +15,8 @@ import customCategoryViewsInjectable from "../custom-views.injectable";
describe("Custom Category Views", () => { describe("Custom Category Views", () => {
let di: ConfigurableDependencyInjectionContainer; let di: ConfigurableDependencyInjectionContainer;
beforeEach(() => { beforeEach(async () => {
di = getDiForUnitTesting(); di = await getDiForUnitTesting();
}); });
it("should order items correctly over all extensions", () => { it("should order items correctly over all extensions", () => {

View File

@ -101,7 +101,7 @@ describe("<Catalog />", () => {
let render: DiRender; let render: DiRender;
beforeEach(async () => { beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true }); di = await getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
import removeDirInjectable from "../../../../common/fs/remove-dir.injectable";
import getInstalledExtensionInjectable from "../../../../extensions/extension-loader/get-installed-extension.injectable";
import { InstallationState } from "../../../../extensions/installation-state/state";
import { getDisForUnitTesting } from "../../../../test-utils/get-dis-for-unit-testing";
import getInstallationStateInjectable from "../../../extensions/installation-state/get-installation-state.injectable";
import { noop } from "../../../utils";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
import createTempFilesAndValidateInjectable from "../attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable";
import unpackExtensionInjectable from "../attempt-install/unpack-extension/unpack-extension.injectable";
describe("attemptInstall()", () => {
let rendererDi: ConfigurableDependencyInjectionContainer;
beforeEach(async () => {
const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
rendererDi = dis.rendererDi;
await dis.runSetups();
});
it("should attempt to remove any broken remnants of a previous install", async () => {
const removeDir = jest.fn();
rendererDi.override(createTempFilesAndValidateInjectable, () => ({ fileName }) => Promise.resolve({
fileName,
data: Buffer.from([]),
id: "some-extension-id",
manifest: {
name: "some-extension-name",
version: "1.0.0",
},
tempFile: "/some-fole-path",
}));
rendererDi.override(getInstallationStateInjectable, () => () => InstallationState.IDLE);
rendererDi.override(getInstalledExtensionInjectable, () => () => undefined);
rendererDi.override(unpackExtensionInjectable, () => () => Promise.resolve());
rendererDi.override(removeDirInjectable, () => removeDir);
const attemptInstall = rendererDi.inject(attemptInstallInjectable);
await attemptInstall({ fileName: "foobar", dataP: Promise.resolve(Buffer.from([])) }, noop);
expect(removeDir).toBeCalledTimes(1);
});
});

View File

@ -48,7 +48,7 @@ describe("Extensions", () => {
let render: DiRender; let render: DiRender;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(directoryForDownloadsInjectable, () => "some-directory-for-downloads"); di.override(directoryForDownloadsInjectable, () => "some-directory-for-downloads");

View File

@ -6,15 +6,14 @@ 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 import startPreInstallInjectable from "../../../extensions/installation-state/start-pre-install.injectable";
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), startPreInstall: di.inject(startPreInstallInjectable),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { downloadFile, downloadJson, ExtendableDisposer } from "../../../../common/utils"; import { Disposer, downloadFile, downloadJson } from "../../../../common/utils";
import { Notifications } from "../../notifications"; import { Notifications } from "../../notifications";
import { ConfirmDialog } from "../../confirm-dialog"; import { ConfirmDialog } from "../../confirm-dialog";
import React from "react"; import React from "react";
@ -11,7 +11,6 @@ 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;
@ -20,17 +19,19 @@ export interface ExtensionInfo {
} }
interface Dependencies { interface Dependencies {
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>; attemptInstall: (request: InstallRequest, d: Disposer) => Promise<void>;
getBaseRegistryUrl: () => Promise<string>; getBaseRegistryUrl: () => Promise<string>;
extensionInstallationStateStore: ExtensionInstallationStateStore startPreInstall: () => Disposer;
} }
export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl, extensionInstallationStateStore }: Dependencies) => async ({ export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl, startPreInstall }: Dependencies) => async (
{
name, name,
version, version,
requireConfirmation = false, requireConfirmation = false,
}: ExtensionInfo) => { }: ExtensionInfo,
const disposer = extensionInstallationStateStore.startPreInstall(); disposer = 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;

View File

@ -3,25 +3,25 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
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 import getExtensionDestFolderInjectable from "./get-extension-dest-folder/get-extension-dest-folder.injectable";
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 createTempFilesAndValidateInjectable from "./create-temp-files-and-validate/create-temp-files-and-validate.injectable";
import extensionInstallationStateStoreInjectable import removeDirInjectable from "../../../../common/fs/remove-dir.injectable";
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import getInstallationStateInjectable from "../../../extensions/installation-state/get-installation-state.injectable";
import getInstalledExtensionInjectable from "../../../../extensions/extension-loader/get-installed-extension.injectable";
const attemptInstallInjectable = getInjectable({ const attemptInstallInjectable = getInjectable({
instantiate: (di) => instantiate: (di) =>
attemptInstall({ attemptInstall({
extensionLoader: di.inject(extensionLoaderInjectable), getInstalledExtension: di.inject(getInstalledExtensionInjectable),
uninstallExtension: di.inject(uninstallExtensionInjectable), uninstallExtension: di.inject(uninstallExtensionInjectable),
unpackExtension: di.inject(unpackExtensionInjectable), unpackExtension: di.inject(unpackExtensionInjectable),
createTempFilesAndValidate: di.inject(createTempFilesAndValidateInjectable), createTempFilesAndValidate: di.inject(createTempFilesAndValidateInjectable),
getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable), getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), getInstallationState: di.inject(getInstallationStateInjectable),
removeDir: di.inject(removeDirInjectable),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,

View File

@ -2,70 +2,51 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { import type { Disposer } from "../../../../common/utils";
Disposer,
disposer,
ExtendableDisposer,
} from "../../../../common/utils";
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 { LensExtensionId } from "../../../../extensions/lens-extension"; import type { LensExtensionId } from "../../../../extensions/lens-extension";
import React from "react"; import React from "react";
import { remove as removeDir } from "fs-extra";
import { shell } from "electron"; import { shell } from "electron";
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 type { InstallRequest } from "./install-request"; import type { InstallRequest } from "./install-request";
import { import { InstallationState } from "../../../../extensions/installation-state/state";
ExtensionInstallationState, import type { InstalledExtension } from "../../../../extensions/extension-discovery/extension-discovery";
ExtensionInstallationStateStore,
} from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
interface Dependencies { interface Dependencies {
extensionLoader: ExtensionLoader; getInstalledExtension: (extId: string) => InstalledExtension | undefined;
uninstallExtension: (id: LensExtensionId) => Promise<boolean>; uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
unpackExtension: (request: InstallRequestValidated, disposeDownloading: Disposer) => Promise<void>;
unpackExtension: ( createTempFilesAndValidate: (installRequest: InstallRequest) => Promise<InstallRequestValidated | null>;
request: InstallRequestValidated, getExtensionDestFolder: (name: string) => string;
disposeDownloading: Disposer, getInstallationState: (extId: string) => InstallationState;
) => Promise<void>; removeDir: (dir: string) => Promise<void>;
createTempFilesAndValidate: (
installRequest: InstallRequest,
) => Promise<InstallRequestValidated | null>;
getExtensionDestFolder: (name: string) => string
extensionInstallationStateStore: ExtensionInstallationStateStore
} }
export const attemptInstall = export const attemptInstall =
({ ({
extensionLoader, getInstalledExtension,
uninstallExtension, uninstallExtension,
unpackExtension, unpackExtension,
createTempFilesAndValidate, createTempFilesAndValidate,
getExtensionDestFolder, getExtensionDestFolder,
extensionInstallationStateStore, getInstallationState,
removeDir,
}: Dependencies) => }: Dependencies) =>
async (request: InstallRequest, d?: ExtendableDisposer): Promise<void> => { async (request: InstallRequest, dispose: Disposer): Promise<void> => {
const dispose = disposer(
extensionInstallationStateStore.startPreInstall(),
d,
);
const validatedRequest = await createTempFilesAndValidate(request); const validatedRequest = await createTempFilesAndValidate(request);
if (!validatedRequest) { if (!validatedRequest) {
return dispose(); return dispose();
} }
const { name, version, description } = validatedRequest.manifest; const {
const curState = extensionInstallationStateStore.getInstallationState( id,
validatedRequest.id, manifest: { name, version, description },
); } = validatedRequest;
const curState = getInstallationState(id);
if (curState !== ExtensionInstallationState.IDLE) { if (curState !== InstallationState.IDLE) {
dispose(); dispose();
return void Notifications.error( return void Notifications.error(
@ -80,21 +61,17 @@ export const attemptInstall =
} }
const extensionFolder = getExtensionDestFolder(name); const extensionFolder = getExtensionDestFolder(name);
const installedExtension = extensionLoader.getExtension(validatedRequest.id); const installedExtension = getInstalledExtension(id);
if (installedExtension) { if (installedExtension) {
const { version: oldVersion } = installedExtension.manifest; const { manifest: { version: oldVersion }} = installedExtension;
// confirm to uninstall old version before installing new version // confirm uninstall and then install new version
const removeNotification = Notifications.info( const removeNotification = Notifications.info(
<div className="InstallingExtensionNotification flex gaps align-center"> <div className="InstallingExtensionNotification flex gaps align-center">
<div className="flex column gaps"> <div className="flex column gaps">
<p> <p>
Install extension{" "} Install extension<b>{name}@{version}</b>?
<b>
{name}@{version}
</b>
?
</p> </p>
<p> <p>
Description: <em>{description}</em> Description: <em>{description}</em>
@ -103,8 +80,7 @@ export const attemptInstall =
className="remove-folder-warning" className="remove-folder-warning"
onClick={() => shell.openPath(extensionFolder)} onClick={() => shell.openPath(extensionFolder)}
> >
<b>Warning:</b> {name}@{oldVersion} will be removed before <b>Warning:</b> {name}@{oldVersion} will be removed before installation.
installation.
</div> </div>
</div> </div>
<Button <Button
@ -113,7 +89,7 @@ export const attemptInstall =
onClick={async () => { onClick={async () => {
removeNotification(); removeNotification();
if (await uninstallExtension(validatedRequest.id)) { if (await uninstallExtension(id)) {
await unpackExtension(validatedRequest, dispose); await unpackExtension(validatedRequest, dispose);
} else { } else {
dispose(); dispose();
@ -126,10 +102,8 @@ export const attemptInstall =
}, },
); );
} else { } else {
// clean up old data if still around // Remove the old dir because it isn't a valid extension anyway
await removeDir(extensionFolder); await removeDir(extensionFolder);
// install extension if not yet exists
await unpackExtension(validatedRequest, dispose); await unpackExtension(validatedRequest, dispose);
} }
}; };

View File

@ -5,17 +5,17 @@
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, 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 import getExtensionDestFolderInjectable from "../get-extension-dest-folder/get-extension-dest-folder.injectable";
from "../get-extension-dest-folder/get-extension-dest-folder.injectable"; import setInstallingInjectable from "../../../../extensions/installation-state/set-installing.injectable";
import extensionInstallationStateStoreInjectable import clearInstallingInjectable from "../../../../extensions/installation-state/clear-installing.injectable";
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), getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), setInstalling: di.inject(setInstallingInjectable),
clearInstalling: di.inject(clearInstallingInjectable),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,

View File

@ -13,20 +13,20 @@ 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 getExtensionDestFolder: (name: string) => string;
extensionInstallationStateStore: ExtensionInstallationStateStore setInstalling: (extId: string) => void;
clearInstalling: (extId: string) => void;
} }
export const unpackExtension = export const unpackExtension = ({
({
extensionLoader, extensionLoader,
getExtensionDestFolder, getExtensionDestFolder,
extensionInstallationStateStore, setInstalling,
}: Dependencies) => clearInstalling,
}: Dependencies) => (
async (request: InstallRequestValidated, disposeDownloading?: Disposer) => { async (request: InstallRequestValidated, disposeDownloading?: Disposer) => {
const { const {
id, id,
@ -35,7 +35,7 @@ export const unpackExtension =
manifest: { name, version }, manifest: { name, version },
} = request; } = request;
extensionInstallationStateStore.setInstalling(id); setInstalling(id);
disposeDownloading?.(); disposeDownloading?.();
const displayName = extensionDisplayName(name, version); const displayName = extensionDisplayName(name, version);
@ -92,10 +92,11 @@ export const unpackExtension =
); );
} finally { } finally {
// Remove install state once finished // Remove install state once finished
extensionInstallationStateStore.clearInstalling(id); 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);
} }
}; }
);

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import attemptInstallInjectable from "./attempt-install/attempt-install.injectable";
import { readFileNotify } from "./read-file-notify/read-file-notify";
import path from "path";
import type { InstallRequest } from "./attempt-install/install-request";
import type { Disposer } from "../../utils";
import startPreInstallInjectable from "../../extensions/installation-state/start-pre-install.injectable";
interface Dependencies {
attemptInstall: (request: InstallRequest, disposer: Disposer) => Promise<void>;
startPreInstall: () => Disposer;
}
export const attemptInstalls = ({ attemptInstall, startPreInstall }: Dependencies) => (
async (filePaths: string[]): Promise<void> => {
const promises: Promise<void>[] = [];
const disposer = startPreInstall();
for (const filePath of filePaths) {
promises.push(
attemptInstall({
fileName: path.basename(filePath),
dataP: readFileNotify(filePath),
}, disposer),
);
}
await Promise.allSettled(promises);
}
);
const attemptInstallsInjectable = getInjectable({
instantiate: (di) => attemptInstalls({
attemptInstall: di.inject(attemptInstallInjectable),
startPreInstall: di.inject(startPreInstallInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default attemptInstallsInjectable;

View File

@ -1,18 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { attemptInstalls } from "./attempt-installs";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
const attemptInstallsInjectable = getInjectable({
instantiate: (di) =>
attemptInstalls({
attemptInstall: di.inject(attemptInstallInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default attemptInstallsInjectable;

View File

@ -1,28 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { readFileNotify } from "../read-file-notify/read-file-notify";
import path from "path";
import type { InstallRequest } from "../attempt-install/install-request";
interface Dependencies {
attemptInstall: (request: InstallRequest) => Promise<void>;
}
export const attemptInstalls =
({ attemptInstall }: Dependencies) =>
async (filePaths: string[]): Promise<void> => {
const promises: Promise<void>[] = [];
for (const filePath of filePaths) {
promises.push(
attemptInstall({
fileName: path.basename(filePath),
dataP: readFileNotify(filePath),
}),
);
}
await Promise.allSettled(promises);
};

View File

@ -4,13 +4,7 @@
*/ */
import "./extensions.scss"; import "./extensions.scss";
import { import { IComputedValue, makeObservable, observable, reaction, when } from "mobx";
IComputedValue,
makeObservable,
observable,
reaction,
when,
} 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/extension-discovery"; import type { InstalledExtension } from "../../../extensions/extension-discovery/extension-discovery";
@ -26,30 +20,25 @@ import userExtensionsInjectable from "./user-extensions/user-extensions.injectab
import enableExtensionInjectable from "./enable-extension/enable-extension.injectable"; import enableExtensionInjectable from "./enable-extension/enable-extension.injectable";
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable"; import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension/confirm-uninstall-extension.injectable"; import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension/confirm-uninstall-extension.injectable";
import installFromInputInjectable from "./install-from-input/install-from-input.injectable";
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
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.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 anyExtensionsInstallingInjectable from "../../extensions/installation-state/any-installing.injectable";
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
interface Dependencies { interface Dependencies {
userExtensions: IComputedValue<InstalledExtension[]>; readonly userExtensions: IComputedValue<InstalledExtension[]>;
enableExtension: (id: LensExtensionId) => void; enableExtension: (id: LensExtensionId) => void;
disableExtension: (id: LensExtensionId) => void; disableExtension: (id: LensExtensionId) => void;
confirmUninstallExtension: (extension: InstalledExtension) => Promise<void>; confirmUninstallExtension: (extension: InstalledExtension) => Promise<void>;
installFromInput: (input: string) => Promise<void>;
installFromSelectFileDialog: () => Promise<void>;
installOnDrop: (files: File[]) => Promise<void>; installOnDrop: (files: File[]) => Promise<void>;
extensionInstallationStateStore: ExtensionInstallationStateStore readonly anyExtensionsInstalling: IComputedValue<boolean>;
} }
@observer @observer
class NonInjectedExtensions extends React.Component<Dependencies> { class NonInjectedExtensions extends React.Component<Dependencies> {
@observable installPath = ""; @observable installPath = "";
constructor(props: Dependencies) { constructor(readonly props: Dependencies) {
super(props); super(props);
makeObservable(this); makeObservable(this);
} }
@ -59,7 +48,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(() => !this.props.extensionInstallationStateStore.anyInstalling, () => this.installPath = ""), when(() => !this.props.anyExtensionsInstalling.get(), () => this.installPath = ""),
]); ]);
} }
}), }),
@ -86,8 +75,6 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
<Install <Install
supportedFormats={supportedExtensionFormats} supportedFormats={supportedExtensionFormats}
onChange={value => (this.installPath = value)} onChange={value => (this.installPath = value)}
installFromInput={() => this.props.installFromInput(this.installPath)}
installFromSelectFileDialog={this.props.installFromSelectFileDialog}
installPath={this.installPath} installPath={this.installPath}
/> />
@ -112,9 +99,7 @@ export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
enableExtension: di.inject(enableExtensionInjectable), enableExtension: di.inject(enableExtensionInjectable),
disableExtension: di.inject(disableExtensionInjectable), disableExtension: di.inject(disableExtensionInjectable),
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable), confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
installFromInput: di.inject(installFromInputInjectable),
installOnDrop: di.inject(installOnDropInjectable), installOnDrop: di.inject(installOnDropInjectable),
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable), anyExtensionsInstalling: di.inject(anyExtensionsInstallingInjectable),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
}), }),
}); });

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import attemptInstallInjectable from "./attempt-install/attempt-install.injectable";
import attemptInstallByInfoInjectable from "./attempt-install-by-info/attempt-install-by-info.injectable";
import startPreInstallInjectable from "../../extensions/installation-state/start-pre-install.injectable";
import { Disposer, downloadFile } from "../../../common/utils";
import { InputValidators } from "../input";
import { getMessageFromError } from "./get-message-from-error/get-message-from-error";
import logger from "../../../main/logger";
import { Notifications } from "../notifications";
import path from "path";
import { readFileNotify } from "./read-file-notify/read-file-notify";
import type { InstallRequest } from "./attempt-install/install-request";
import type { ExtensionInfo } from "./attempt-install-by-info/attempt-install-by-info";
interface Dependencies {
attemptInstall: (request: InstallRequest, disposer: Disposer) => Promise<void>,
attemptInstallByInfo: (extensionInfo: ExtensionInfo, disposer: Disposer) => Promise<void>,
startPreInstall: () => Disposer;
}
const installFromInput = ({ attemptInstall, attemptInstallByInfo, startPreInstall }: Dependencies) => (
async (input: string) => {
const disposer = startPreInstall();
try {
// fixme: improve error messages for non-tar-file URLs
if (InputValidators.isUrl.validate(input)) {
// install via url
const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 });
const fileName = path.basename(input);
await attemptInstall({ fileName, dataP: promise }, disposer);
} else if (InputValidators.isPath.validate(input)) {
// install from system path
const fileName = path.basename(input);
await attemptInstall({ fileName, dataP: readFileNotify(input) }, disposer);
} else if (InputValidators.isExtensionNameInstall.validate(input)) {
const [{ groups: { name, version }}] = [...input.matchAll(InputValidators.isExtensionNameInstallRegex)];
await attemptInstallByInfo({ name, version }, disposer);
} else {
throw new Error("unknown input format");
}
} catch (error) {
const message = getMessageFromError(error);
logger.info(`[EXTENSION-INSTALL]: installation has failed: ${message}`, { error, installPath: input });
Notifications.error(<p>Installation has failed: <b>{message}</b></p>);
} finally {
disposer();
}
}
);
const installFromInputInjectable = getInjectable({
instantiate: (di) =>
installFromInput({
attemptInstall: di.inject(attemptInstallInjectable),
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
startPreInstall: di.inject(startPreInstallInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default installFromInputInjectable;

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
import { installFromInput } from "./install-from-input";
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({
instantiate: (di) =>
installFromInput({
attemptInstall: di.inject(attemptInstallInjectable),
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default installFromInputInjectable;

View File

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

View File

@ -5,15 +5,17 @@
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { requestOpenFilePickingDialog } from "../../ipc"; import { requestOpenFilePickingDialog } from "../../ipc";
import { supportedExtensionFormats } from "./supported-extension-formats"; import { supportedExtensionFormats } from "./supported-extension-formats";
import attemptInstallsInjectable from "./attempt-installs/attempt-installs.injectable"; import attemptInstallsInjectable from "./attempt-installs.injectable";
import directoryForDownloadsInjectable from "../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable"; import directoryForDownloadsInjectable from "../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable";
interface Dependencies { interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void> attemptInstalls: (filePaths: string[]) => Promise<void>;
directoryForDownloads: string directoryForDownloads: string;
} }
const installFromSelectFileDialog = ({ attemptInstalls, directoryForDownloads }: Dependencies) => async () => { const installFromSelectFileDialog = ({ attemptInstalls, directoryForDownloads }: Dependencies) => (
async () => {
const { canceled, filePaths } = await requestOpenFilePickingDialog({ const { canceled, filePaths } = await requestOpenFilePickingDialog({
defaultPath: directoryForDownloads, defaultPath: directoryForDownloads,
properties: ["openFile", "multiSelections"], properties: ["openFile", "multiSelections"],
@ -25,7 +27,8 @@ const installFromSelectFileDialog = ({ attemptInstalls, directoryForDownloads }:
if (!canceled) { if (!canceled) {
await attemptInstalls(filePaths); await attemptInstalls(filePaths);
} }
}; }
);
const installFromSelectFileDialogInjectable = getInjectable({ const installFromSelectFileDialogInjectable = getInjectable({
instantiate: (di) => installFromSelectFileDialog({ instantiate: (di) => installFromSelectFileDialog({

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import attemptInstallsInjectable from "./attempt-installs.injectable";
import logger from "../../../main/logger";
interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void>;
}
const installOnDrop = ({ attemptInstalls }: Dependencies) => (
async (files: File[]) => {
logger.info("Install from D&D");
await attemptInstalls(files.map(({ path }) => path));
}
);
const installOnDropInjectable = getInjectable({
instantiate: (di) => installOnDrop({
attemptInstalls: di.inject(attemptInstallsInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default installOnDropInjectable;

View File

@ -1,18 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { installOnDrop } from "./install-on-drop";
import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable";
const installOnDropInjectable = getInjectable({
instantiate: (di) =>
installOnDrop({
attemptInstalls: di.inject(attemptInstallsInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default installOnDropInjectable;

View File

@ -1,16 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import logger from "../../../../main/logger";
interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void>;
}
export const installOnDrop =
({ attemptInstalls }: Dependencies) =>
async (files: File[]) => {
logger.info("Install from D&D");
await attemptInstalls(files.map(({ path }) => path));
};

View File

@ -12,21 +12,22 @@ 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 type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
import extensionInstallationStateStoreInjectable
from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import installFromInputInjectable from "./install-from-input.injectable";
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
import isCurrentlyIdleInjectable from "../../extensions/installation-state/is-currently-idle.injectable";
interface Props { export interface InstallProps {
installPath: string; installPath: string;
supportedFormats: string[]; supportedFormats: string[];
onChange: (path: string) => void; onChange: (path: string) => void;
installFromInput: () => void;
installFromSelectFileDialog: () => void;
} }
interface Dependencies { interface Dependencies {
extensionInstallationStateStore: ExtensionInstallationStateStore; isCurrentlyIdle: IComputedValue<boolean>;
installFromInput: (input: string) => Promise<void>;
installFromSelectFileDialog: () => Promise<void>;
} }
const installInputValidators = [ const installInputValidators = [
@ -42,28 +43,26 @@ const installInputValidator: InputValidator = {
), ),
}; };
const NonInjectedInstall: React.FC<Dependencies & Props> = ({ const NonInjectedInstall = observer(({
installPath, installPath,
supportedFormats, supportedFormats,
onChange, onChange,
installFromInput, installFromInput,
installFromSelectFileDialog, installFromSelectFileDialog,
extensionInstallationStateStore, isCurrentlyIdle,
}) => ( }: Dependencies & InstallProps) => {
const showAsWaiting = isCurrentlyIdle.get();
const formats = supportedFormats.join(", ");
return (
<section className="mt-2"> <section className="mt-2">
<SubTitle <SubTitle title={`Name or file path or URL to an extension package (${formats})`} />
title={`Name or file path or URL to an extension package (${supportedFormats.join(
", ",
)})`}
/>
<div className="flex"> <div className="flex">
<div className="flex-1"> <div className="flex-1">
<Input <Input
className="box grow mr-6" className="box grow mr-6"
theme="round-black" theme="round-black"
disabled={ disabled={showAsWaiting}
extensionInstallationStateStore.anyPreInstallingOrInstalling
}
placeholder={"Name or file path or URL"} placeholder={"Name or file path or URL"}
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }} showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
validators={installPath ? installInputValidator : undefined} validators={installPath ? installInputValidator : undefined}
@ -85,11 +84,9 @@ const NonInjectedInstall: React.FC<Dependencies & Props> = ({
primary primary
label="Install" label="Install"
className="w-80 h-full" className="w-80 h-full"
disabled={ disabled={showAsWaiting}
extensionInstallationStateStore.anyPreInstallingOrInstalling waiting={showAsWaiting}
} onClick={() => installFromInput(installPath)}
waiting={extensionInstallationStateStore.anyPreInstallingOrInstalling}
onClick={installFromInput}
/> />
</div> </div>
</div> </div>
@ -97,17 +94,14 @@ const NonInjectedInstall: React.FC<Dependencies & Props> = ({
<b>Pro-Tip</b>: you can drag-n-drop tarball-file to this area <b>Pro-Tip</b>: you can drag-n-drop tarball-file to this area
</small> </small>
</section> </section>
); );
});
export const Install = withInjectables<Dependencies, Props>( export const Install = withInjectables<Dependencies, InstallProps>(NonInjectedInstall, {
observer(NonInjectedInstall),
{
getProps: (di, props) => ({ getProps: (di, props) => ({
extensionInstallationStateStore: di.inject( isCurrentlyIdle: di.inject(isCurrentlyIdleInjectable),
extensionInstallationStateStoreInjectable, installFromInput: di.inject(installFromInputInjectable),
), installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
...props, ...props,
}), }),
}, });
);

View File

@ -5,10 +5,7 @@
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 type { import type { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery/extension-discovery";
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";
@ -17,15 +14,13 @@ 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 import extensionDiscoveryInjectable from "../../../extensions/extension-discovery/extension-discovery.injectable";
from "../../../extensions/extension-discovery/extension-discovery.injectable";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import extensionInstallationStateStoreInjectable import type { IComputedValue } from "mobx";
from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import isUninstallingInjectable from "../../extensions/installation-state/is-uninstalling.injectable";
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; import anyExtensionsUninstallingInjectable from "../../extensions/installation-state/any-uninstalling.injectable";
interface Props { export interface InstalledExtensionsProps {
extensions: InstalledExtension[]; extensions: InstalledExtension[];
enable: (id: LensExtensionId) => void; enable: (id: LensExtensionId) => void;
disable: (id: LensExtensionId) => void; disable: (id: LensExtensionId) => void;
@ -34,7 +29,8 @@ interface Props {
interface Dependencies { interface Dependencies {
extensionDiscovery: ExtensionDiscovery; extensionDiscovery: ExtensionDiscovery;
extensionInstallationStateStore: ExtensionInstallationStateStore; isUninstalling: (extId: string) => boolean;
anyUninstalling: IComputedValue<boolean>;
} }
function getStatus(extension: InstalledExtension) { function getStatus(extension: InstalledExtension) {
@ -45,7 +41,7 @@ function getStatus(extension: InstalledExtension) {
return extension.isEnabled ? "Enabled" : "Disabled"; return extension.isEnabled ? "Enabled" : "Disabled";
} }
const NonInjectedInstalledExtensions : React.FC<Dependencies & Props> = (({ extensionDiscovery, extensionInstallationStateStore, extensions, uninstall, enable, disable }) => { const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, isUninstalling, anyUninstalling, extensions, uninstall, enable, disable }: Dependencies & InstalledExtensionsProps) => {
const filters = [ const filters = [
(extension: InstalledExtension) => extension.manifest.name, (extension: InstalledExtension) => extension.manifest.name,
(extension: InstalledExtension) => getStatus(extension), (extension: InstalledExtension) => getStatus(extension),
@ -86,12 +82,10 @@ const NonInjectedInstalledExtensions : React.FC<Dependencies & Props> = (({ exte
], [], ], [],
); );
const data = useMemo( const data = useMemo(() => 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 uninstalling = isUninstalling(id);
return { return {
extension: ( extension: (
@ -110,41 +104,39 @@ const NonInjectedInstalledExtensions : React.FC<Dependencies & Props> = (({ exte
), ),
actions: ( actions: (
<MenuActions usePortal toolbar={false}> <MenuActions usePortal toolbar={false}>
{ isCompatible && ( {isCompatible && (
<> <>
{isEnabled ? ( {isEnabled ? (
<MenuItem <MenuItem
disabled={isUninstalling} disabled={uninstalling}
onClick={() => disable(id)} onClick={() => disable(id)}
> >
<Icon material="unpublished"/> <Icon material="unpublished" />
<span className="title" aria-disabled={isUninstalling}>Disable</span> <span className="title" aria-disabled={uninstalling}>Disable</span>
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem <MenuItem
disabled={isUninstalling} disabled={uninstalling}
onClick={() => enable(id)} onClick={() => enable(id)}
> >
<Icon material="check_circle"/> <Icon material="check_circle" />
<span className="title" aria-disabled={isUninstalling}>Enable</span> <span className="title" aria-disabled={uninstalling}>Enable</span>
</MenuItem> </MenuItem>
)} )}
</> </>
)} )}
<MenuItem <MenuItem
disabled={isUninstalling} disabled={uninstalling}
onClick={() => uninstall(extension)} onClick={() => uninstall(extension)}
> >
<Icon material="delete"/> <Icon material="delete" />
<span className="title" aria-disabled={isUninstalling}>Uninstall</span> <span className="title" aria-disabled={uninstalling}>Uninstall</span>
</MenuItem> </MenuItem>
</MenuActions> </MenuActions>
), ),
}; };
}); }), [extensions, anyUninstalling.get()]);
}, [extensions, extensionInstallationStateStore.anyUninstalling],
);
if (!extensionDiscovery.isLoaded) { if (!extensionDiscovery.isLoaded) {
return <div><Spinner center /></div>; return <div><Spinner center /></div>;
@ -175,15 +167,11 @@ const NonInjectedInstalledExtensions : React.FC<Dependencies & Props> = (({ exte
); );
}); });
export const InstalledExtensions = withInjectables<Dependencies, Props>( export const InstalledExtensions = withInjectables<Dependencies, InstalledExtensionsProps>(NonInjectedInstalledExtensions, {
observer(NonInjectedInstalledExtensions),
{
getProps: (di, props) => ({ getProps: (di, props) => ({
extensionDiscovery: di.inject(extensionDiscoveryInjectable), extensionDiscovery: di.inject(extensionDiscoveryInjectable),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), isUninstalling: di.inject(isUninstallingInjectable),
anyUninstalling: di.inject(anyExtensionsUninstallingInjectable),
...props, ...props,
}), }),
}, });
);

View File

@ -5,17 +5,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 import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable";
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import clearUninstallingInjectable from "../../../extensions/installation-state/clear-uninstalling.injectable";
import extensionDiscoveryInjectable import setUninstallingInjectable from "../../../extensions/installation-state/set-uninstalling.injectable";
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), extensionDiscovery: di.inject(extensionDiscoveryInjectable),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), setUninstalling: di.inject(setUninstallingInjectable),
clearUninstalling: di.inject(clearUninstallingInjectable),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,

View File

@ -10,23 +10,22 @@ 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 extensionDiscovery: ExtensionDiscovery;
extensionInstallationStateStore: ExtensionInstallationStateStore setUninstalling: (extId: string) => void;
clearUninstalling: (extId: string) => void;
} }
export const uninstallExtension = export const uninstallExtension = ({ extensionLoader, extensionDiscovery, setUninstalling, clearUninstalling }: 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); setUninstalling(extensionId);
await extensionDiscovery.uninstallExtension(extensionId); await extensionDiscovery.uninstallExtension(extensionId);
@ -57,6 +56,7 @@ export const uninstallExtension =
return false; return false;
} finally { } finally {
// Remove uninstall state on uninstall failure // Remove uninstall state on uninstall failure
extensionInstallationStateStore.clearUninstalling(extensionId); clearUninstalling(extensionId);
} }
}; }
);

View File

@ -17,7 +17,7 @@ describe("ClusterRoleBindingDialog tests", () => {
let render: DiRender; let render: DiRender;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
await di.runSetups(); await di.runSetups();

View File

@ -20,7 +20,7 @@ describe("RoleBindingDialog tests", () => {
let render: DiRender; let render: DiRender;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");

View File

@ -31,7 +31,7 @@ describe("<Welcome/>", () => {
let welcomeBannersStub: WelcomeBannerRegistration[]; let welcomeBannersStub: WelcomeBannerRegistration[];
beforeEach(async () => { beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true }); di = await getDiForUnitTesting({ doGeneralOverrides: true });
await di.runSetups(); await di.runSetups();

View File

@ -37,7 +37,7 @@ describe("<PodTolerations />", () => {
let render: DiRender; let render: DiRender;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
di.override( di.override(
directoryForLensLocalStorageInjectable, directoryForLensLocalStorageInjectable,

View File

@ -89,7 +89,7 @@ describe("<DeleteClusterDialog />", () => {
let createCluster: (model: ClusterModel) => Cluster; let createCluster: (model: ClusterModel) => Cluster;
beforeEach(async () => { beforeEach(async () => {
const { mainDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true }); const { mainDi, runSetups } = await getDisForUnitTesting({ doGeneralOverrides: true });
mockFs(); mockFs();

View File

@ -59,7 +59,7 @@ describe("<DockTabs />", () => {
let render: DiRender; let render: DiRender;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = await getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di); render = renderFor(di);

Some files were not shown because too many files have changed in this diff Show More