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;
beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
dis.mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory");

View File

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

View File

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

View File

@ -40,7 +40,7 @@ describe("user store tests", () => {
let mainDi: DependencyInjectionContainer;
beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
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.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { PathName } from "./app-path-names";
import { createChannel } from "../ipc-channel/create-channel/create-channel";
export type AppPaths = Record<PathName, string>;
import type { AppPaths } from "./app-paths";
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.
*/
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 { 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 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 path from "path";
import { appPathsInjectionToken } from "./app-path-injection-token";
describe("app-paths", () => {
let mainDi: DependencyInjectionContainer;
let rendererDi: DependencyInjectionContainer;
let runSetups: () => Promise<void[]>;
let runSetups: () => Promise<void>;
beforeEach(() => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
beforeEach(async () => {
const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
mainDi = dis.mainDi;
rendererDi = dis.rendererDi;
@ -45,15 +45,12 @@ describe("app-paths", () => {
mainDi.override(
getElectronAppPathInjectable,
() =>
(key: PathName): string | null =>
defaultAppPathsStub[key],
() => (key: PathName): string | null => defaultAppPathsStub[key],
);
mainDi.override(
setElectronAppPathInjectable,
() =>
(key: PathName, path: string): void => {
() => (key: PathName, path: string): void => {
defaultAppPathsStub[key] = path;
},
);
@ -123,7 +120,7 @@ describe("app-paths", () => {
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);
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", () => {
const { appData, userData } = rendererDi.inject(appPathsInjectionToken);
it("when in main, when injecting path for app data, has integration specific app data path", () => {
const { appData, userData } = mainDi.inject(appPathsInjectionToken);
expect({ appData, userData }).toEqual({
appData: "some-integration-testing-app-data",

View File

@ -4,6 +4,7 @@
*/
import type { app as electronApp } from "electron";
export type AppPaths = Record<PathName, string>;
export type PathName = Parameters<typeof electronApp["getPath"]>[0];
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";
const directoryForBinariesInjectable = getInjectable({
instantiate: (di) =>
path.join(di.inject(directoryForUserDataInjectable), "binaries"),
instantiate: (di) => path.join(di.inject(directoryForUserDataInjectable), "binaries"),
lifecycle: lifecycleEnum.singleton,
});

View File

@ -7,8 +7,7 @@ import directoryForUserDataInjectable from "../directory-for-user-data/directory
import path from "path";
const directoryForKubeConfigsInjectable = getInjectable({
instantiate: (di) =>
path.resolve(di.inject(directoryForUserDataInjectable), "kubeconfigs"),
instantiate: (di) => path.resolve(di.inject(directoryForUserDataInjectable), "kubeconfigs"),
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 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
? process.env.LOG_LEVEL
: isDebugging
@ -67,4 +76,4 @@ if (ipcMain) {
export default winston.createLogger({
format: format.simple(),
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;
beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
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 isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable";
import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable";
import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable";
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 installExtensionsInjectable from "../extension-installer/install-extensions/install-extensions.injectable";
import { clearInstallingChannelInjectionToken, setInstallingChannelInjectionToken } from "../installation-state/state-channels";
const extensionDiscoveryInjectable = getInjectable({
instantiate: (di) =>
new ExtensionDiscovery({
instantiate: (di) => new ExtensionDiscovery({
extensionLoader: di.inject(extensionLoaderInjectable),
extensionsStore: di.inject(extensionsStoreInjectable),
extensionInstallationStateStore: di.inject(
extensionInstallationStateStoreInjectable,
),
isCompatibleBundledExtension: di.inject(
isCompatibleBundledExtensionInjectable,
),
setInstalling: di.inject(setInstallingChannelInjectionToken),
clearInstalling: di.inject(clearInstallingChannelInjectionToken),
isCompatibleBundledExtension: di.inject(isCompatibleBundledExtensionInjectable),
isCompatibleExtension: di.inject(isCompatibleExtensionInjectable),
installExtension: di.inject(installExtensionInjectable),
installExtensions: di.inject(installExtensionsInjectable),
extensionPackageRootDirectory: di.inject(
extensionPackageRootDirectoryInjectable,
),
extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable),
}),
lifecycle: lifecycleEnum.singleton,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,7 +48,7 @@ describe("kubeconfig manager tests", () => {
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
const di = await getDiForUnitTesting({ doGeneralOverrides: true });
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.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { getAppPaths } from "./get-app-paths";
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 path from "path";
import appNameInjectable from "./app-name/app-name.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({
setup: (di) => {
const directoryForIntegrationTesting = di.inject(
directoryForIntegrationTestingInjectable,
);
instantiate: (di) => {
const directoryForIntegrationTesting = di.inject(directoryForIntegrationTestingInjectable);
const setElectronAppPath = di.inject(setElectronAppPathInjectable);
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);
registerAppPathsChannel(di);
// Set path for user data
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,
lifecycle: lifecycleEnum.singleton,
});
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.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { fromPairs } from "lodash/fp";
import { pathNames, PathName } from "../../common/app-paths/app-path-names";
import type { AppPaths } from "../../common/app-paths/app-path-injection-token";
import { pathNames, PathName, AppPaths } from "../../common/app-paths/app-paths";
import { fromEntries } from "../../renderer/utils";
interface Dependencies {
getAppPath: (name: PathName) => string
}
export const getAppPaths = ({ getAppPath }: Dependencies) =>
fromPairs(pathNames.map((name) => [name, getAppPath(name)])) as AppPaths;
export function getAppPaths({ getAppPath }: Dependencies): 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 { getDiForUnitTesting } from "../../getDiForUnitTesting";
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", () => {
let getElectronAppPath: (name: string) => string | null;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: false });
const di = await getDiForUnitTesting({ doGeneralOverrides: false });
const appStub = {
name: "some-app-name",

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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 {
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.
*/
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";
const setElectronAppPathInjectable = getInjectable({
instantiate: (di) => (name: PathName, path: string) : void =>
di.inject(electronAppInjectable).setPath(name, path),
instantiate: (di) => {
const app = di.inject(electronAppInjectable);
return (name: PathName, path: string): void => {
app.setPath(name, path);
};
},
lifecycle: lifecycleEnum.singleton,
});

View File

@ -38,7 +38,7 @@ describe("kubeconfig-sync.source tests", () => {
let computeDiff: ReturnType<typeof computeDiffFor>;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
const di = await getDiForUnitTesting({ doGeneralOverrides: true });
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 setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable";
import appNameInjectable from "./app-paths/app-name/app-name.injectable";
import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable";
import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
import registerEventSinkInjectable from "../common/communication/register-event-sink.injectable";
import registerChannelInjectable from "./communication/register-channel.injectable";
import { overrideFsFunctions } from "../test-utils/override-fs-functions";
export const getDiForUnitTesting = (
{ doGeneralOverrides } = { doGeneralOverrides: false },
) => {
interface DiForTestingOptions {
doGeneralOverrides?: boolean;
doIpcOverrides?: boolean;
}
export async function getDiForUnitTesting({ doGeneralOverrides = false, doIpcOverrides = true }: DiForTestingOptions = {}) {
const di = createContainer();
setLegacyGlobalDiForExtensionApi(di);
for (const filePath of getInjectableFilePaths()) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const injectableInstance = require(filePath).default;
const { default: injectableInstance } = await import(filePath);
di.register({
id: filePath,
@ -36,26 +38,21 @@ export const getDiForUnitTesting = (
di.preventSideEffects();
if (doGeneralOverrides) {
di.override(
getElectronAppPathInjectable,
() => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`,
);
di.override(getElectronAppPathInjectable, () => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`);
di.override(setElectronAppPathInjectable, () => () => undefined);
di.override(appNameInjectable, () => "some-electron-app-name");
di.override(registerChannelInjectable, () => () => undefined);
di.override(writeJsonFileInjectable, () => () => {
throw new Error("Tried to write JSON file to file system without specifying explicit override.");
});
overrideFsFunctions(di);
}
di.override(readJsonFileInjectable, () => () => {
throw new Error("Tried to read JSON file from file system without specifying explicit override.");
});
if (doIpcOverrides) {
di.override(registerEventSinkInjectable, () => () => () => undefined);
di.override(registerChannelInjectable, () => () => () => undefined);
}
return di;
};
}
const getInjectableFilePaths = memoize(() => [
...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 * as tar from "tar";
import { isWindows } from "../common/vars";
import type winston from "winston";
import type { LensLogger } from "../common/logger";
export type LensBinaryOpts = {
version: string;
@ -32,7 +32,7 @@ export class LensBinary {
protected arch: string;
protected originalBinaryName: string;
protected requestOpts: request.Options;
protected logger: Console | winston.Logger;
protected logger: Omit<LensLogger, "silly" | "verbose">;
constructor(opts: LensBinaryOpts) {
const baseDir = opts.baseDir;
@ -68,7 +68,7 @@ export class LensBinary {
}
}
public setLogger(logger: Console | winston.Logger) {
public setLogger(logger: LensLogger) {
this.logger = logger;
}

View File

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

View File

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

View File

@ -17,7 +17,7 @@ describe("tray-menu-items", () => {
let extensionsStub: ObservableMap<string, LensMainExtension>;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di = await getDiForUnitTesting({ doGeneralOverrides: true });
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.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { AppPaths, appPathsInjectionToken, appPathsIpcChannel } from "../../common/app-paths/app-path-injection-token";
import getValueFromRegisteredChannelInjectable from "./get-value-from-registered-channel/get-value-from-registered-channel.injectable";
import { appPathsInjectionChannelToken } from "../../common/app-paths/app-path-channel-injection-token";
import { appPathsInjectionToken } from "../../common/app-paths/app-path-injection-token";
import type { AppPaths } from "../../common/app-paths/app-paths";
let syncAppPaths: AppPaths;
const appPathsInjectable = getInjectable({
setup: async (di) => {
const getValueFromRegisteredChannel = di.inject(
getValueFromRegisteredChannelInjectable,
);
const appPathsChannel = di.inject(appPathsInjectionChannelToken);
syncAppPaths = await getValueFromRegisteredChannel(appPathsIpcChannel);
syncAppPaths = await appPathsChannel();
},
instantiate: () => syncAppPaths,
injectionToken: appPathsInjectionToken,
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 extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.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 userStoreInjectable from "../common/user-store/user-store.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();
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
extensionInstallationStateStore.bindIpcListeners();
HelmRepoManager.createInstance(); // initialize the manager
// 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", () => {
let di: ConfigurableDependencyInjectionContainer;
beforeEach(() => {
di = getDiForUnitTesting();
beforeEach(async () => {
di = await getDiForUnitTesting();
});
describe("without extensions", () => {

View File

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

View File

@ -101,7 +101,7 @@ describe("<Catalog />", () => {
let render: DiRender;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di = await getDiForUnitTesting({ doGeneralOverrides: true });
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;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
const di = await getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
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 attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
import getBaseRegistryUrlInjectable from "../get-base-registry-url/get-base-registry-url.injectable";
import extensionInstallationStateStoreInjectable
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import startPreInstallInjectable from "../../../extensions/installation-state/start-pre-install.injectable";
const attemptInstallByInfoInjectable = getInjectable({
instantiate: (di) =>
attemptInstallByInfo({
attemptInstall: di.inject(attemptInstallInjectable),
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
startPreInstall: di.inject(startPreInstallInjectable),
}),
lifecycle: lifecycleEnum.singleton,

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* 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 { ConfirmDialog } from "../../confirm-dialog";
import React from "react";
@ -11,7 +11,6 @@ import { SemVer } from "semver";
import URLParse from "url-parse";
import type { InstallRequest } from "../attempt-install/install-request";
import lodash from "lodash";
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
export interface ExtensionInfo {
name: string;
@ -20,17 +19,19 @@ export interface ExtensionInfo {
}
interface Dependencies {
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>;
attemptInstall: (request: InstallRequest, d: Disposer) => Promise<void>;
getBaseRegistryUrl: () => Promise<string>;
extensionInstallationStateStore: ExtensionInstallationStateStore
startPreInstall: () => Disposer;
}
export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl, extensionInstallationStateStore }: Dependencies) => async ({
export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl, startPreInstall }: Dependencies) => async (
{
name,
version,
requireConfirmation = false,
}: ExtensionInfo) => {
const disposer = extensionInstallationStateStore.startPreInstall();
}: ExtensionInfo,
disposer = startPreInstall(),
) => {
const baseUrl = await getBaseRegistryUrl();
const registryUrl = new URLParse(baseUrl).set("pathname", name).toString();
let json: any;

View File

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

View File

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

View File

@ -5,17 +5,17 @@
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { unpackExtension } from "./unpack-extension";
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
import getExtensionDestFolderInjectable
from "../get-extension-dest-folder/get-extension-dest-folder.injectable";
import extensionInstallationStateStoreInjectable
from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import getExtensionDestFolderInjectable from "../get-extension-dest-folder/get-extension-dest-folder.injectable";
import setInstallingInjectable from "../../../../extensions/installation-state/set-installing.injectable";
import clearInstallingInjectable from "../../../../extensions/installation-state/clear-installing.injectable";
const unpackExtensionInjectable = getInjectable({
instantiate: (di) =>
unpackExtension({
extensionLoader: di.inject(extensionLoaderInjectable),
getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
setInstalling: di.inject(setInstallingInjectable),
clearInstalling: di.inject(clearInstallingInjectable),
}),
lifecycle: lifecycleEnum.singleton,

View File

@ -13,20 +13,20 @@ import path from "path";
import fse from "fs-extra";
import { when } from "mobx";
import React from "react";
import type { ExtensionInstallationStateStore } from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store";
interface Dependencies {
extensionLoader: ExtensionLoader
getExtensionDestFolder: (name: string) => string
extensionInstallationStateStore: ExtensionInstallationStateStore
extensionLoader: ExtensionLoader;
getExtensionDestFolder: (name: string) => string;
setInstalling: (extId: string) => void;
clearInstalling: (extId: string) => void;
}
export const unpackExtension =
({
export const unpackExtension = ({
extensionLoader,
getExtensionDestFolder,
extensionInstallationStateStore,
}: Dependencies) =>
setInstalling,
clearInstalling,
}: Dependencies) => (
async (request: InstallRequestValidated, disposeDownloading?: Disposer) => {
const {
id,
@ -35,7 +35,7 @@ export const unpackExtension =
manifest: { name, version },
} = request;
extensionInstallationStateStore.setInstalling(id);
setInstalling(id);
disposeDownloading?.();
const displayName = extensionDisplayName(name, version);
@ -92,10 +92,11 @@ export const unpackExtension =
);
} finally {
// Remove install state once finished
extensionInstallationStateStore.clearInstalling(id);
clearInstalling(id);
// clean up
fse.remove(unpackingTempFolder).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 {
IComputedValue,
makeObservable,
observable,
reaction,
when,
} from "mobx";
import { IComputedValue, makeObservable, observable, reaction, when } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
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 disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension/confirm-uninstall-extension.injectable";
import installFromInputInjectable from "./install-from-input/install-from-input.injectable";
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
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 extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
import anyExtensionsInstallingInjectable from "../../extensions/installation-state/any-installing.injectable";
interface Dependencies {
userExtensions: IComputedValue<InstalledExtension[]>;
readonly userExtensions: IComputedValue<InstalledExtension[]>;
enableExtension: (id: LensExtensionId) => void;
disableExtension: (id: LensExtensionId) => void;
confirmUninstallExtension: (extension: InstalledExtension) => Promise<void>;
installFromInput: (input: string) => Promise<void>;
installFromSelectFileDialog: () => Promise<void>;
installOnDrop: (files: File[]) => Promise<void>;
extensionInstallationStateStore: ExtensionInstallationStateStore
readonly anyExtensionsInstalling: IComputedValue<boolean>;
}
@observer
class NonInjectedExtensions extends React.Component<Dependencies> {
@observable installPath = "";
constructor(props: Dependencies) {
constructor(readonly props: Dependencies) {
super(props);
makeObservable(this);
}
@ -59,7 +48,7 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => {
if (curSize > prevSize) {
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
supportedFormats={supportedExtensionFormats}
onChange={value => (this.installPath = value)}
installFromInput={() => this.props.installFromInput(this.installPath)}
installFromSelectFileDialog={this.props.installFromSelectFileDialog}
installPath={this.installPath}
/>
@ -112,9 +99,7 @@ export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
enableExtension: di.inject(enableExtensionInjectable),
disableExtension: di.inject(disableExtensionInjectable),
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
installFromInput: di.inject(installFromInputInjectable),
installOnDrop: di.inject(installOnDropInjectable),
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
anyExtensionsInstalling: di.inject(anyExtensionsInstallingInjectable),
}),
});

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 { requestOpenFilePickingDialog } from "../../ipc";
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";
interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void>
directoryForDownloads: string
attemptInstalls: (filePaths: string[]) => Promise<void>;
directoryForDownloads: string;
}
const installFromSelectFileDialog = ({ attemptInstalls, directoryForDownloads }: Dependencies) => async () => {
const installFromSelectFileDialog = ({ attemptInstalls, directoryForDownloads }: Dependencies) => (
async () => {
const { canceled, filePaths } = await requestOpenFilePickingDialog({
defaultPath: directoryForDownloads,
properties: ["openFile", "multiSelections"],
@ -25,7 +27,8 @@ const installFromSelectFileDialog = ({ attemptInstalls, directoryForDownloads }:
if (!canceled) {
await attemptInstalls(filePaths);
}
};
}
);
const installFromSelectFileDialogInjectable = getInjectable({
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 { SubTitle } from "../layout/sub-title";
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 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;
supportedFormats: string[];
onChange: (path: string) => void;
installFromInput: () => void;
installFromSelectFileDialog: () => void;
}
interface Dependencies {
extensionInstallationStateStore: ExtensionInstallationStateStore;
isCurrentlyIdle: IComputedValue<boolean>;
installFromInput: (input: string) => Promise<void>;
installFromSelectFileDialog: () => Promise<void>;
}
const installInputValidators = [
@ -42,28 +43,26 @@ const installInputValidator: InputValidator = {
),
};
const NonInjectedInstall: React.FC<Dependencies & Props> = ({
const NonInjectedInstall = observer(({
installPath,
supportedFormats,
onChange,
installFromInput,
installFromSelectFileDialog,
extensionInstallationStateStore,
}) => (
isCurrentlyIdle,
}: Dependencies & InstallProps) => {
const showAsWaiting = isCurrentlyIdle.get();
const formats = supportedFormats.join(", ");
return (
<section className="mt-2">
<SubTitle
title={`Name or file path or URL to an extension package (${supportedFormats.join(
", ",
)})`}
/>
<SubTitle title={`Name or file path or URL to an extension package (${formats})`} />
<div className="flex">
<div className="flex-1">
<Input
className="box grow mr-6"
theme="round-black"
disabled={
extensionInstallationStateStore.anyPreInstallingOrInstalling
}
disabled={showAsWaiting}
placeholder={"Name or file path or URL"}
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
validators={installPath ? installInputValidator : undefined}
@ -85,11 +84,9 @@ const NonInjectedInstall: React.FC<Dependencies & Props> = ({
primary
label="Install"
className="w-80 h-full"
disabled={
extensionInstallationStateStore.anyPreInstallingOrInstalling
}
waiting={extensionInstallationStateStore.anyPreInstallingOrInstalling}
onClick={installFromInput}
disabled={showAsWaiting}
waiting={showAsWaiting}
onClick={() => installFromInput(installPath)}
/>
</div>
</div>
@ -98,16 +95,13 @@ const NonInjectedInstall: React.FC<Dependencies & Props> = ({
</small>
</section>
);
});
export const Install = withInjectables<Dependencies, Props>(
observer(NonInjectedInstall),
{
export const Install = withInjectables<Dependencies, InstallProps>(NonInjectedInstall, {
getProps: (di, props) => ({
extensionInstallationStateStore: di.inject(
extensionInstallationStateStoreInjectable,
),
isCurrentlyIdle: di.inject(isCurrentlyIdleInjectable),
installFromInput: di.inject(installFromInputInjectable),
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
...props,
}),
},
);
});

View File

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

View File

@ -5,17 +5,17 @@
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import { uninstallExtension } from "./uninstall-extension";
import extensionInstallationStateStoreInjectable
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import extensionDiscoveryInjectable
from "../../../../extensions/extension-discovery/extension-discovery.injectable";
import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable";
import clearUninstallingInjectable from "../../../extensions/installation-state/clear-uninstalling.injectable";
import setUninstallingInjectable from "../../../extensions/installation-state/set-uninstalling.injectable";
const uninstallExtensionInjectable = getInjectable({
instantiate: (di) =>
uninstallExtension({
extensionLoader: di.inject(extensionLoaderInjectable),
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
setUninstalling: di.inject(setUninstallingInjectable),
clearUninstalling: di.inject(clearUninstallingInjectable),
}),
lifecycle: lifecycleEnum.singleton,

View File

@ -10,23 +10,22 @@ import { Notifications } from "../../notifications";
import React from "react";
import { when } from "mobx";
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 {
extensionLoader: ExtensionLoader
extensionDiscovery: ExtensionDiscovery
extensionInstallationStateStore: ExtensionInstallationStateStore
extensionLoader: ExtensionLoader;
extensionDiscovery: ExtensionDiscovery;
setUninstalling: (extId: string) => void;
clearUninstalling: (extId: string) => void;
}
export const uninstallExtension =
({ extensionLoader, extensionDiscovery, extensionInstallationStateStore }: Dependencies) =>
export const uninstallExtension = ({ extensionLoader, extensionDiscovery, setUninstalling, clearUninstalling }: Dependencies) => (
async (extensionId: LensExtensionId): Promise<boolean> => {
const { manifest } = extensionLoader.getExtension(extensionId);
const displayName = extensionDisplayName(manifest.name, manifest.version);
try {
logger.debug(`[EXTENSIONS]: trying to uninstall ${extensionId}`);
extensionInstallationStateStore.setUninstalling(extensionId);
setUninstalling(extensionId);
await extensionDiscovery.uninstallExtension(extensionId);
@ -57,6 +56,7 @@ export const uninstallExtension =
return false;
} finally {
// Remove uninstall state on uninstall failure
extensionInstallationStateStore.clearUninstalling(extensionId);
clearUninstalling(extensionId);
}
};
}
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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