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:
parent
c46d0036cc
commit
c2a359295b
@ -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");
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
12
src/common/app-paths/app-path-channel-injection-token.ts
Normal file
12
src/common/app-paths/app-path-channel-injection-token.ts
Normal 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";
|
||||
@ -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");
|
||||
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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[] = [
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
13
src/common/communication/broadcast.injectable.ts
Normal file
13
src/common/communication/broadcast.injectable.ts
Normal 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;
|
||||
18
src/common/communication/channel.ts
Normal file
18
src/common/communication/channel.ts
Normal 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;
|
||||
10
src/common/communication/emitter.ts
Normal file
10
src/common/communication/emitter.ts
Normal 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;
|
||||
10
src/common/communication/ipc-on-event-injection-token.ts
Normal file
10
src/common/communication/ipc-on-event-injection-token.ts
Normal 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>();
|
||||
33
src/common/communication/register-emitter.injectable.ts
Normal file
33
src/common/communication/register-emitter.injectable.ts
Normal 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;
|
||||
33
src/common/communication/register-event-sink.injectable.ts
Normal file
33
src/common/communication/register-event-sink.injectable.ts
Normal 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;
|
||||
15
src/common/fs/remove-dir.injectable.ts
Normal file
15
src/common/fs/remove-dir.injectable.ts
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
@ -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;
|
||||
|
||||
13
src/common/logger/base-logger.injectable.ts
Normal file
13
src/common/logger/base-logger.injectable.ts
Normal 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;
|
||||
33
src/common/logger/create-child-logger.injectable.ts
Normal file
33
src/common/logger/create-child-logger.injectable.ts
Normal 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;
|
||||
13
src/common/utils/unique-id.injectable.ts
Normal file
13
src/common/utils/unique-id.injectable.ts
Normal 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;
|
||||
@ -110,7 +110,7 @@ describe("ExtensionLoader", () => {
|
||||
let updateExtensionStateMock: jest.Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
|
||||
const dis = await getDisForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs();
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
17
src/extensions/installation-state/logger.injectable.ts
Normal file
17
src/extensions/installation-state/logger.injectable.ts
Normal 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;
|
||||
16
src/extensions/installation-state/state-channels.ts
Normal file
16
src/extensions/installation-state/state-channels.ts
Normal 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>();
|
||||
13
src/extensions/installation-state/state.ts
Normal file
13
src/extensions/installation-state/state.ts
Normal 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",
|
||||
}
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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": {},
|
||||
|
||||
@ -89,7 +89,7 @@ describe("kube auth proxy tests", () => {
|
||||
"tmp": {},
|
||||
};
|
||||
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
const di = await getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs(mockMinikubeConfig);
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
26
src/main/app-paths/app-paths-channel.injectable.ts
Normal file
26
src/main/app-paths/app-paths-channel.injectable.ts
Normal 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;
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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)]));
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
20
src/main/communication/ipc-handle.injectable.ts
Normal file
20
src/main/communication/ipc-handle.injectable.ts
Normal 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;
|
||||
22
src/main/communication/ipc-on.injectable.ts
Normal file
22
src/main/communication/ipc-on.injectable.ts
Normal 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;
|
||||
31
src/main/communication/register-channel.injectable.ts
Normal file
31
src/main/communication/register-channel.injectable.ts
Normal 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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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 }),
|
||||
|
||||
19
src/main/ipc/emit-event.ts
Normal file
19
src/main/ipc/emit-event.ts
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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": {},
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
19
src/renderer/app-paths/app-paths-channel.injectable.ts
Normal file
19
src/renderer/app-paths/app-paths-channel.injectable.ts
Normal 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;
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
@ -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
|
||||
|
||||
17
src/renderer/communication/ipc-invoke.injectable.ts
Normal file
17
src/renderer/communication/ipc-invoke.injectable.ts
Normal 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;
|
||||
22
src/renderer/communication/ipc-on.injectable.ts
Normal file
22
src/renderer/communication/ipc-on.injectable.ts
Normal 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;
|
||||
27
src/renderer/communication/register-channel.injectable.ts
Normal file
27
src/renderer/communication/register-channel.injectable.ts
Normal 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;
|
||||
@ -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", () => {
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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");
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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);
|
||||
};
|
||||
@ -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),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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?.();
|
||||
}
|
||||
};
|
||||
@ -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({
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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));
|
||||
};
|
||||
@ -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,
|
||||
}),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ describe("<Welcome/>", () => {
|
||||
let welcomeBannersStub: WelcomeBannerRegistration[];
|
||||
|
||||
beforeEach(async () => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
di = await getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user