diff --git a/packages/core/src/common/utils/channel/message-to-channel-injection-token.ts b/packages/core/src/common/utils/channel/message-to-channel-injection-token.ts index 3ffd75f4f7..aed702b442 100644 --- a/packages/core/src/common/utils/channel/message-to-channel-injection-token.ts +++ b/packages/core/src/common/utils/channel/message-to-channel-injection-token.ts @@ -7,7 +7,7 @@ import type { MessageChannel } from "./message-channel-listener-injection-token" export interface SendMessageToChannel { (channel: MessageChannel): void; - (channel: MessageChannel, message: Message): void; + >(channel: Channel, message: Channel extends MessageChannel ? Message : never): void; } export type MessageChannelSender = Channel extends MessageChannel diff --git a/packages/core/src/common/utils/iter.ts b/packages/core/src/common/utils/iter.ts index dc1c4621fd..591337396f 100644 --- a/packages/core/src/common/utils/iter.ts +++ b/packages/core/src/common/utils/iter.ts @@ -14,6 +14,7 @@ interface Iterator extends Iterable { flatMap(fn: (val: T) => U[]): Iterator; concat(src2: IterableIterator): Iterator; join(sep?: string): string; + count(): number; } export function chain(src: IterableIterator): Iterator { @@ -26,6 +27,7 @@ export function chain(src: IterableIterator): Iterator { join: (sep) => join(src, sep), collect: (fn) => fn(src), concat: (src2) => chain(concat(src, src2)), + count: () => count(src), [Symbol.iterator]: () => src, }; } @@ -246,3 +248,14 @@ export function* concat(...sources: IterableIterator[]): IterableIterator< } } } + +export function count(src: Iterable): number { + let i = 0; + + for (const x of src) { + void x; + i += 1; + } + + return i; +} diff --git a/packages/core/src/extensions/extension-installation-state-store/extension-installation-state-store.injectable.ts b/packages/core/src/extensions/extension-installation-state-store/extension-installation-state-store.injectable.ts deleted file mode 100644 index e7ad2285bc..0000000000 --- a/packages/core/src/extensions/extension-installation-state-store/extension-installation-state-store.injectable.ts +++ /dev/null @@ -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 } from "@ogre-tools/injectable"; -import loggerInjectable from "../../common/logger.injectable"; -import { ExtensionInstallationStateStore } from "./extension-installation-state-store"; - -const extensionInstallationStateStoreInjectable = getInjectable({ - id: "extension-installation-state-store", - instantiate: (di) => new ExtensionInstallationStateStore({ - logger: di.inject(loggerInjectable), - }), -}); - -export default extensionInstallationStateStoreInjectable; diff --git a/packages/core/src/extensions/extension-installation-state-store/extension-installation-state-store.ts b/packages/core/src/extensions/extension-installation-state-store/extension-installation-state-store.ts deleted file mode 100644 index 093c80934b..0000000000 --- a/packages/core/src/extensions/extension-installation-state-store/extension-installation-state-store.ts +++ /dev/null @@ -1,247 +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 { disposer } from "../../renderer/utils"; -import type { ExtendableDisposer } from "../../renderer/utils"; -import * as uuid from "uuid"; -import { broadcastMessage } from "../../common/ipc"; -import { ipcRenderer } from "electron"; -import type { Logger } from "../../common/logger"; - -export enum ExtensionInstallationState { - INSTALLING = "installing", - UNINSTALLING = "uninstalling", - IDLE = "idle", -} - -interface Dependencies { - readonly logger: Logger; -} - -const Prefix = "[ExtensionInstallationStore]"; - -const installingFromMainChannel = "extension-installation-state-store:install"; -const clearInstallingFromMainChannel = "extension-installation-state-store:clear-install"; - -export class ExtensionInstallationStateStore { - private readonly preInstallIds = observable.set(); - private readonly uninstallingExtensions = observable.set(); - private readonly installingExtensions = observable.set(); - - constructor(private readonly dependencies: Dependencies) {} - - bindIpcListeners = () => { - ipcRenderer - .on(installingFromMainChannel, (event, extId) => { - this.setInstalling(extId); - }) - - .on(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 => { - this.dependencies.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(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(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(); - - this.dependencies.logger.debug( - `${Prefix}: starting a new preinstall phase: ${preInstallStepId}`, - ); - this.preInstallIds.add(preInstallStepId); - - return disposer(() => { - this.preInstallIds.delete(preInstallStepId); - this.dependencies.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 => { - this.dependencies.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 => { - this.dependencies.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 => { - this.dependencies.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; - } -} diff --git a/packages/core/src/features/extensions/discovery/main/extension-file-add.injectable.ts b/packages/core/src/features/extensions/discovery/main/extension-file-add.injectable.ts index a3d86ea879..074eb80f8a 100644 --- a/packages/core/src/features/extensions/discovery/main/extension-file-add.injectable.ts +++ b/packages/core/src/features/extensions/discovery/main/extension-file-add.injectable.ts @@ -8,8 +8,9 @@ import getDirnameOfPathInjectable from "../../../../common/path/get-dirname.inje import getRelativePathInjectable from "../../../../common/path/get-relative-path.injectable"; import fileSystemSeparatorInjectable from "../../../../common/path/separator.injectable"; import { manifestFilename } from "../../../../common/vars"; -import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import installedExtensionsInjectable from "../../common/installed-extensions.injectable"; +import setExtensionAsInstallingInjectable from "../../installation-states/main/set-as-installing.injectable"; +import clearExtensionAsInstallingInjectable from "../../installation-states/renderer/clear-as-installing.injectable"; import installExtensionPackageInjectable from "../common/install-package.injectable"; import localExtensionsDirectoryPathInjectable from "../common/local-extensions-directory-path.injectable"; import extensionDiscoveryLoggerInjectable from "../common/logger.injectable"; @@ -22,12 +23,13 @@ const extensionFileAddedInjectable = getInjectable({ const localExtensionsDirectoryPath = di.inject(localExtensionsDirectoryPathInjectable); const fileSystemSeparator = di.inject(fileSystemSeparatorInjectable); const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); - const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); const loadUserExtensionFromFolder = di.inject(loadUserExtensionFromFolderInjectable); const installExtensionPackage = di.inject(installExtensionPackageInjectable); const installedExtensions = di.inject(installedExtensionsInjectable); const logger = di.inject(extensionDiscoveryLoggerInjectable); + const setExtensionAsInstalling = di.inject(setExtensionAsInstallingInjectable); + const clearExtensionAsInstalling = di.inject(clearExtensionAsInstallingInjectable); return async (manifestPath: string): Promise => { // e.g. "foo/package.json" @@ -40,7 +42,7 @@ const extensionFileAddedInjectable = getInjectable({ if (getBasenameOfPath(manifestPath) === manifestFilename && isUnderLocalFolderPath) { try { - extensionInstallationStateStore.setInstallingFromMain(manifestPath); + setExtensionAsInstalling(manifestPath); const absPath = getDirnameOfPath(manifestPath); // this.loadExtensionFromPath updates this.packagesJson @@ -56,7 +58,7 @@ const extensionFileAddedInjectable = getInjectable({ } catch (error) { logger.error(`failed to add extension: ${error}`, { error }); } finally { - extensionInstallationStateStore.clearInstallingFromMain(manifestPath); + clearExtensionAsInstalling(manifestPath); } } }; diff --git a/packages/core/src/features/extensions/installation-states/common/channels.ts b/packages/core/src/features/extensions/installation-states/common/channels.ts new file mode 100644 index 0000000000..41dfd30cec --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/common/channels.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { MessageChannel } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import type { LensExtensionId } from "../../../../extensions/lens-extension"; + +export interface ExtensionInstallPhaseData { + id: LensExtensionId; + phase: "installing" | "clear-installing"; +} + +export const setExtensionInstallPhaseChannel: MessageChannel = { + id: "set-extension-install-phase", +}; diff --git a/packages/core/src/features/extensions/installation-states/common/tokens.ts b/packages/core/src/features/extensions/installation-states/common/tokens.ts new file mode 100644 index 0000000000..d55db687fb --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/common/tokens.ts @@ -0,0 +1,19 @@ +/** + * 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 { LensExtensionId } from "../../../../extensions/lens-extension"; + +export type SetExtensionAsInstalling = (id: LensExtensionId) => void; + +export const setExtensionAsInstallingInjectionToken = getInjectionToken({ + id: "set-extension-as-installing-token", +}); + +export type ClearExtensionAsInstalling = (id: LensExtensionId) => void; + +export const clearExtensionAsInstallingInjectionToken = getInjectionToken({ + id: "clear-extension-as-installing-token", +}); diff --git a/packages/core/src/features/extensions/installation-states/main/clear-as-installing.injectable.ts b/packages/core/src/features/extensions/installation-states/main/clear-as-installing.injectable.ts new file mode 100644 index 0000000000..4ad1c5cf05 --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/main/clear-as-installing.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { sendMessageToChannelInjectionToken } from "../../../../common/utils/channel/message-to-channel-injection-token"; +import { setExtensionInstallPhaseChannel } from "../common/channels"; +import { clearExtensionAsInstallingInjectionToken } from "../common/tokens"; + +const clearExtensionAsInstallingInjectable = getInjectable({ + id: "clear-extension-as-installing", + instantiate: (di) => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + return (id) => sendMessageToChannel(setExtensionInstallPhaseChannel, { + id, + phase: "clear-installing", + }); + }, + injectionToken: clearExtensionAsInstallingInjectionToken, +}); + +export default clearExtensionAsInstallingInjectable; diff --git a/packages/core/src/features/extensions/installation-states/main/set-as-installing.injectable.ts b/packages/core/src/features/extensions/installation-states/main/set-as-installing.injectable.ts new file mode 100644 index 0000000000..8f962741c0 --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/main/set-as-installing.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectable } from "@ogre-tools/injectable"; +import { sendMessageToChannelInjectionToken } from "../../../../common/utils/channel/message-to-channel-injection-token"; +import { setExtensionInstallPhaseChannel } from "../common/channels"; +import { setExtensionAsInstallingInjectionToken } from "../common/tokens"; + +const setExtensionAsInstallingInjectable = getInjectable({ + id: "set-extension-as-installing", + instantiate: (di) => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + return (id) => sendMessageToChannel(setExtensionInstallPhaseChannel, { + id, + phase: "installing", + }); + }, + injectionToken: setExtensionAsInstallingInjectionToken, +}); + +export default setExtensionAsInstallingInjectable; diff --git a/packages/core/src/features/extensions/installation-states/renderer/clear-as-installing.injectable.ts b/packages/core/src/features/extensions/installation-states/renderer/clear-as-installing.injectable.ts new file mode 100644 index 0000000000..901041811d --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/renderer/clear-as-installing.injectable.ts @@ -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 } from "@ogre-tools/injectable"; +import { clearExtensionAsInstallingInjectionToken } from "../common/tokens"; +import extensionInstallationStatesInjectable from "./states.injectable"; + +const clearExtensionAsInstallingInjectable = getInjectable({ + id: "clear-extension-as-installing", + instantiate: (di) => { + const states = di.inject(extensionInstallationStatesInjectable); + + return (id) => states.delete(id); + }, + injectionToken: clearExtensionAsInstallingInjectionToken, +}); + +export default clearExtensionAsInstallingInjectable; diff --git a/packages/core/src/features/extensions/installation-states/renderer/get-phase.injectable.ts b/packages/core/src/features/extensions/installation-states/renderer/get-phase.injectable.ts new file mode 100644 index 0000000000..d45b16125b --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/renderer/get-phase.injectable.ts @@ -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 } from "@ogre-tools/injectable"; +import type { LensExtensionId } from "../../../../extensions/lens-extension"; +import type { InstallationState } from "./states.injectable"; +import extensionInstallationStatesInjectable from "./states.injectable"; + +export type GetExtensionInstallationPhase = (id: LensExtensionId) => InstallationState; + +const getExtensionInstallationPhaseInjectable = getInjectable({ + id: "get-extension-installation-phase", + instantiate: (di): GetExtensionInstallationPhase => { + const states = di.inject(extensionInstallationStatesInjectable); + + return (id) => states.get(id) ?? "idle"; + }, +}); + +export default getExtensionInstallationPhaseInjectable; diff --git a/packages/core/src/features/extensions/installation-states/renderer/installing-count.injectable.ts b/packages/core/src/features/extensions/installation-states/renderer/installing-count.injectable.ts new file mode 100644 index 0000000000..f958c71873 --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/renderer/installing-count.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { iter } from "../../../../common/utils"; +import extensionInstallationStatesInjectable from "./states.injectable"; + +const extensionsInstallingCountInjectable = getInjectable({ + id: "extensions-installing-count", + instantiate: (di) => { + const states = di.inject(extensionInstallationStatesInjectable); + + return computed(() => ( + iter.chain(states.entries()) + .filter(([, state]) => state === "installing") + .count() + )); + }, +}); + +export default extensionsInstallingCountInjectable; diff --git a/packages/core/src/features/extensions/installation-states/renderer/preinstalling-count.injectable.ts b/packages/core/src/features/extensions/installation-states/renderer/preinstalling-count.injectable.ts new file mode 100644 index 0000000000..c80a1f935b --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/renderer/preinstalling-count.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import preinstallingPhasesInjectable from "./preinstalling.injectable"; + +const extensionsPreinstallingCountInjectable = getInjectable({ + id: "extensions-preinstalling-count", + instantiate: (di) => { + const preinstallingPhases = di.inject(preinstallingPhasesInjectable); + + return computed(() => preinstallingPhases.size); + }, +}); + +export default extensionsPreinstallingCountInjectable; diff --git a/packages/core/src/features/extensions/installation-states/renderer/preinstalling.injectable.ts b/packages/core/src/features/extensions/installation-states/renderer/preinstalling.injectable.ts new file mode 100644 index 0000000000..9c64f0bf70 --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/renderer/preinstalling.injectable.ts @@ -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 } from "@ogre-tools/injectable"; +import { observable } from "mobx"; + +const preinstallingPhasesInjectable = getInjectable({ + id: "preinstalling-phases", + instantiate: () => observable.set(), +}); + +export default preinstallingPhasesInjectable; diff --git a/packages/core/src/features/extensions/installation-states/renderer/set-as-installing.injectable.ts b/packages/core/src/features/extensions/installation-states/renderer/set-as-installing.injectable.ts new file mode 100644 index 0000000000..78b5a78c59 --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/renderer/set-as-installing.injectable.ts @@ -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 } from "@ogre-tools/injectable"; +import { setExtensionAsInstallingInjectionToken } from "../common/tokens"; +import extensionInstallationStatesInjectable from "./states.injectable"; + +const setExtensionAsInstallingInjectable = getInjectable({ + id: "set-extension-as-installing", + instantiate: (di) => { + const states = di.inject(extensionInstallationStatesInjectable); + + return (id) => states.set(id, "installing"); + }, + injectionToken: setExtensionAsInstallingInjectionToken, +}); + +export default setExtensionAsInstallingInjectable; diff --git a/packages/core/src/features/extensions/installation-states/renderer/set-as-uninstalling.injectable.ts b/packages/core/src/features/extensions/installation-states/renderer/set-as-uninstalling.injectable.ts new file mode 100644 index 0000000000..cf4079ae32 --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/renderer/set-as-uninstalling.injectable.ts @@ -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 } from "@ogre-tools/injectable"; +import type { LensExtensionId } from "../../../../extensions/lens-extension"; +import extensionInstallationStatesInjectable from "./states.injectable"; + +export type SetExtensionAsUninstalling = (id: LensExtensionId) => void; + +const setExtensionAsUninstallingInjectable = getInjectable({ + id: "set-extension-as-uninstalling", + instantiate: (di): SetExtensionAsUninstalling => { + const states = di.inject(extensionInstallationStatesInjectable); + + return (id) => states.set(id, "uninstalling"); + }, +}); + +export default setExtensionAsUninstallingInjectable; diff --git a/packages/core/src/features/extensions/installation-states/renderer/start-pre-install-phase.injectable.ts b/packages/core/src/features/extensions/installation-states/renderer/start-pre-install-phase.injectable.ts new file mode 100644 index 0000000000..d9a4eae04c --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/renderer/start-pre-install-phase.injectable.ts @@ -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 } from "@ogre-tools/injectable"; +import { action } from "mobx"; +import type { Disposer } from "../../../../common/utils"; +import preinstallingPhasesInjectable from "./preinstalling.injectable"; +import * as uuid from "uuid"; + +export type StartPreInstallPhase = () => Disposer; + +const startPreInstallPhaseInjectable = getInjectable({ + id: "start-pre-install-phase", + instantiate: (di): StartPreInstallPhase => { + const preinstalling = di.inject(preinstallingPhasesInjectable); + + return action(() => { + const preInstallStepId = uuid.v4(); + + preinstalling.add(preInstallStepId); + + return () => preinstalling.delete(preInstallStepId); + }); + }, +}); + +export default startPreInstallPhaseInjectable; diff --git a/packages/core/src/features/extensions/installation-states/renderer/states.injectable.ts b/packages/core/src/features/extensions/installation-states/renderer/states.injectable.ts new file mode 100644 index 0000000000..f2125bc706 --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/renderer/states.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectable } from "@ogre-tools/injectable"; +import { observable } from "mobx"; +import type { LensExtensionId } from "../../../../extensions/lens-extension"; + +export type InstallationState = ActiveInstallationState | "idle"; +export type ActiveInstallationState = "installing" | "uninstalling"; + +const extensionInstallationStatesInjectable = getInjectable({ + id: "extension-installation-states", + instantiate: () => observable.map(), +}); + +export default extensionInstallationStatesInjectable; diff --git a/packages/core/src/features/extensions/installation-states/renderer/uninstalling-count.injectable.ts b/packages/core/src/features/extensions/installation-states/renderer/uninstalling-count.injectable.ts new file mode 100644 index 0000000000..7a061d504d --- /dev/null +++ b/packages/core/src/features/extensions/installation-states/renderer/uninstalling-count.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { iter } from "../../../../common/utils"; +import extensionInstallationStatesInjectable from "./states.injectable"; + +const extensionsUninstallingCountInjectable = getInjectable({ + id: "extensions-uninstalling-count", + instantiate: (di) => { + const states = di.inject(extensionInstallationStatesInjectable); + + return computed(() => ( + iter.chain(states.entries()) + .filter(([, state]) => state === "uninstalling") + .count() + )); + }, +}); + +export default extensionsUninstallingCountInjectable; diff --git a/packages/core/src/renderer/bootstrap.tsx b/packages/core/src/renderer/bootstrap.tsx index c8d1d71b10..9931fbed3a 100644 --- a/packages/core/src/renderer/bootstrap.tsx +++ b/packages/core/src/renderer/bootstrap.tsx @@ -10,7 +10,6 @@ import { render, unmountComponentAtNode } from "react-dom"; import { DefaultProps } from "./mui-base-theme"; import { DiContextProvider } from "@ogre-tools/injectable-react"; import type { DiContainer } from "@ogre-tools/injectable"; -import extensionInstallationStateStoreInjectable from "../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import initRootFrameInjectable from "./frames/root-frame/init-root-frame.injectable"; import initClusterFrameInjectable from "./frames/cluster-frame/init-cluster-frame.injectable"; import { Router } from "react-router"; @@ -27,8 +26,6 @@ export async function bootstrap(di: DiContainer) { assert(rootElem, "#app MUST exist"); - di.inject(extensionInstallationStateStoreInjectable).bindIpcListeners(); - let App; let initializeApp; diff --git a/packages/core/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/packages/core/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 227a0ee763..82b7d89cfe 100644 --- a/packages/core/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/packages/core/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -17,8 +17,6 @@ import directoryForDownloadsInjectable from "../../../../common/app-paths/direct import assert from "assert"; import type { InstallExtensionFromInput } from "../install-extension-from-input.injectable"; import installExtensionFromInputInjectable from "../install-extension-from-input.injectable"; -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 type { IObservableValue, ObservableMap } from "mobx"; import { computed, observable, when } from "mobx"; import type { RemovePath } from "../../../../common/fs/remove.injectable"; @@ -31,16 +29,18 @@ import installedExtensionsInjectable from "../../../../features/extensions/commo import initialDiscoveryLoadCompletedInjectable from "../../../../features/extensions/discovery/common/initial-load-completed.injectable"; import type { RemoveExtensionFiles } from "../../../../features/extensions/discovery/common/uninstall-extension.injectable"; import removeExtensionFilesInjectable from "../../../../features/extensions/discovery/common/uninstall-extension.injectable"; +import type { StartPreInstallPhase } from "../../../../features/extensions/installation-states/renderer/start-pre-install-phase.injectable"; +import startPreInstallPhaseInjectable from "../../../../features/extensions/installation-states/renderer/start-pre-install-phase.injectable"; describe("Extensions", () => { let installedExtensions: ObservableMap; let installExtensionFromInput: jest.MockedFunction; - let extensionInstallationStateStore: ExtensionInstallationStateStore; let render: DiRender; let deleteFileMock: jest.MockedFunction; let downloadBinary: jest.MockedFunction; let isLoaded: IObservableValue; let removeExtensionFilesMock: jest.MockedFunction; + let startPreInstallPhase: StartPreInstallPhase; beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); @@ -69,7 +69,7 @@ describe("Extensions", () => { di.override(downloadBinaryInjectable, () => downloadBinary); installedExtensions = di.inject(installedExtensionsInjectable); - extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); + startPreInstallPhase = di.inject(startPreInstallPhaseInjectable); installedExtensions.set("extensionId", { id: "extensionId", @@ -133,10 +133,10 @@ describe("Extensions", () => { installExtensionFromInput.mockImplementation(async (input) => { expect(input).toBe("https://test.extensionurl/package.tgz"); - const clear = extensionInstallationStateStore.startPreInstall(); + const clearPreInstallPhase = startPreInstallPhase(); await when(() => resolveInstall.get()); - clear(); + clearPreInstallPhase(); }); fireEvent.change(await screen.findByPlaceholderText("File path or URL", { diff --git a/packages/core/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx b/packages/core/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx index 1f3d65c802..d5df32cc64 100644 --- a/packages/core/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx @@ -9,7 +9,6 @@ import URLParse from "url-parse"; import { getInjectable } from "@ogre-tools/injectable"; 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 confirmInjectable from "../confirm-dialog/confirm.injectable"; import { reduce } from "lodash"; import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; @@ -19,6 +18,7 @@ import downloadJsonInjectable from "../../../common/fetch/download-json/normal.i import type { PackageJson } from "type-fest"; import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; import loggerInjectable from "../../../common/logger.injectable"; +import startPreInstallPhaseInjectable from "../../../features/extensions/installation-states/renderer/start-pre-install-phase.injectable"; export interface ExtensionInfo { name: string; @@ -46,17 +46,17 @@ const attemptInstallByInfoInjectable = getInjectable({ instantiate: (di): AttemptInstallByInfo => { const attemptInstall = di.inject(attemptInstallInjectable); const getBaseRegistryUrl = di.inject(getBaseRegistryUrlInjectable); - const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); const confirm = di.inject(confirmInjectable); const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); const downloadJson = di.inject(downloadJsonInjectable); const downloadBinary = di.inject(downloadBinaryInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); const logger = di.inject(loggerInjectable); + const startPreInstallPhase = di.inject(startPreInstallPhaseInjectable); return async (info) => { const { name, version: versionOrTagName, requireConfirmation = false } = info; - const disposer = extensionInstallationStateStore.startPreInstall(); + const clearPreInstallPhase = startPreInstallPhase(); const baseUrl = await getBaseRegistryUrl(); const registryUrl = new URLParse(baseUrl).set("pathname", name).toString(); let json: NpmRegistryPackageDescriptor; @@ -67,13 +67,13 @@ const attemptInstallByInfoInjectable = getInjectable({ if (!result.callWasSuccessful) { showErrorNotification(`Failed to get registry information for extension: ${result.error}`); - return disposer(); + return clearPreInstallPhase(); } if (!isObject(result.response) || Array.isArray(result.response)) { showErrorNotification("Failed to get registry information for extension"); - return disposer(); + return clearPreInstallPhase(); } if (result.response.error || !isObject(result.response.versions)) { @@ -81,7 +81,7 @@ const attemptInstallByInfoInjectable = getInjectable({ showErrorNotification(`Failed to get registry information for extension${message}`); - return disposer(); + return clearPreInstallPhase(); } json = result.response as unknown as NpmRegistryPackageDescriptor; @@ -95,7 +95,7 @@ const attemptInstallByInfoInjectable = getInjectable({ showErrorNotification(`Failed to get valid registry information for extension. ${error}`); } - return disposer(); + return clearPreInstallPhase(); } let version = versionOrTagName; @@ -119,7 +119,7 @@ const attemptInstallByInfoInjectable = getInjectable({

)); - return disposer(); + return clearPreInstallPhase(); } version = potentialVersion; @@ -137,7 +137,7 @@ const attemptInstallByInfoInjectable = getInjectable({

)); - return disposer(); + return clearPreInstallPhase(); } } else { const versions = Object.keys(json.versions) @@ -154,7 +154,7 @@ const attemptInstallByInfoInjectable = getInjectable({ logger.error("No versions supplied for extension", { name }); showErrorNotification(`No versions found for ${name}`); - return disposer(); + return clearPreInstallPhase(); } const versionInfo = json.versions[version]; @@ -164,7 +164,7 @@ const attemptInstallByInfoInjectable = getInjectable({ showErrorNotification("Configured registry has invalid data model. Please verify that it is like NPM's."); logger.warn(`[ATTEMPT-INSTALL-BY-INFO]: registry returned unexpected data, final version is ${version} but the versions object is missing .dist.tarball as a string`, versionInfo); - return disposer(); + return clearPreInstallPhase(); } if (requireConfirmation) { @@ -183,7 +183,7 @@ const attemptInstallByInfoInjectable = getInjectable({ }); if (!proceed) { - return disposer(); + return clearPreInstallPhase(); } } @@ -194,10 +194,10 @@ const attemptInstallByInfoInjectable = getInjectable({ if (!request.callWasSuccessful) { showErrorNotification(`Failed to download extension: ${request.error}`); - return disposer(); + return clearPreInstallPhase(); } - return attemptInstall({ fileName, data: request.response }, disposer); + return attemptInstall({ fileName, data: request.response }, clearPreInstallPhase); }; }, }); diff --git a/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx b/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx index 4c59387b74..d62bffd017 100644 --- a/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/attempt-install/attempt-install.injectable.tsx @@ -7,17 +7,17 @@ import uninstallExtensionInjectable from "../uninstall-extension.injectable"; import unpackExtensionInjectable from "./unpack-extension.injectable"; import getExtensionDestFolderInjectable from "./get-extension-dest-folder.injectable"; import createTempFilesAndValidateInjectable from "./create-temp-files-and-validate.injectable"; -import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import type { Disposer } from "../../../../common/utils"; import { disposer } from "../../../../common/utils"; import { Button } from "../../button"; import React from "react"; import { remove as removeDir } from "fs-extra"; import { shell } from "electron"; -import { ExtensionInstallationState } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable"; import showInfoNotificationInjectable from "../../notifications/show-info-notification.injectable"; import getInstalledExtensionInjectable from "../../../../features/extensions/common/get-installed-extension.injectable"; +import startPreInstallPhaseInjectable from "../../../../features/extensions/installation-states/renderer/start-pre-install-phase.injectable"; +import getExtensionInstallationPhaseInjectable from "../../../../features/extensions/installation-states/renderer/get-phase.injectable"; export interface InstallRequest { fileName: string; @@ -33,14 +33,15 @@ const attemptInstallInjectable = getInjectable({ const unpackExtension = di.inject(unpackExtensionInjectable); const createTempFilesAndValidate = di.inject(createTempFilesAndValidateInjectable); const getExtensionDestFolder = di.inject(getExtensionDestFolderInjectable); - const installStateStore = di.inject(extensionInstallationStateStoreInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); const showInfoNotification = di.inject(showInfoNotificationInjectable); const getInstalledExtension = di.inject(getInstalledExtensionInjectable); + const startPreInstallPhase = di.inject(startPreInstallPhaseInjectable); + const getExtensionInstallationPhase = di.inject(getExtensionInstallationPhaseInjectable); return async (request, cleanup) => { const dispose = disposer( - installStateStore.startPreInstall(), + startPreInstallPhase(), cleanup, ); @@ -51,9 +52,9 @@ const attemptInstallInjectable = getInjectable({ } const { name, version, description } = validatedRequest.manifest; - const curState = installStateStore.getInstallationState(validatedRequest.id); + const curState = getExtensionInstallationPhase(validatedRequest.id); - if (curState !== ExtensionInstallationState.IDLE) { + if (curState !== "idle") { dispose(); return void showErrorNotification( @@ -115,7 +116,7 @@ const attemptInstallInjectable = getInjectable({ }, ); } else { - // clean up old data if still around + // clean up old data if still around await removeDir(extensionFolder); // install extension if not yet exists diff --git a/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx b/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx index c479da09d4..39608ac9da 100644 --- a/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/attempt-install/unpack-extension.injectable.tsx @@ -4,7 +4,6 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import getExtensionDestFolderInjectable from "./get-extension-dest-folder.injectable"; -import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import type { Disposer } from "../../../../common/utils"; import { noop } from "../../../../common/utils"; import { extensionDisplayName } from "../../../../extensions/lens-extension"; @@ -20,6 +19,8 @@ import showInfoNotificationInjectable from "../../notifications/show-info-notifi import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable"; import installedUserExtensionsInjectable from "../../../../features/extensions/common/user-extensions.injectable"; import enableExtensionInjectable from "../enable-extension.injectable"; +import setExtensionAsInstallingInjectable from "../../../../features/extensions/installation-states/renderer/set-as-installing.injectable"; +import clearExtensionAsInstallingInjectable from "../../../../features/extensions/installation-states/renderer/clear-as-installing.injectable"; export type UnpackExtension = (request: InstallRequestValidated, disposeDownloading?: Disposer) => Promise; @@ -27,13 +28,14 @@ const unpackExtensionInjectable = getInjectable({ id: "unpack-extension", instantiate: (di): UnpackExtension => { const getExtensionDestFolder = di.inject(getExtensionDestFolderInjectable); - const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); const extractTar = di.inject(extractTarInjectable); const logger = di.inject(loggerInjectable); const showInfoNotification = di.inject(showInfoNotificationInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); const installedUserExtensions = di.inject(installedUserExtensionsInjectable); const enableExtension = di.inject(enableExtensionInjectable); + const setExtensionAsInstalling = di.inject(setExtensionAsInstallingInjectable); + const clearExtensionAsInstalling = di.inject(clearExtensionAsInstallingInjectable); return async (request, disposeDownloading) => { const { @@ -43,7 +45,7 @@ const unpackExtensionInjectable = getInjectable({ manifest: { name, version }, } = request; - extensionInstallationStateStore.setInstalling(id); + setExtensionAsInstalling(id); disposeDownloading?.(); const displayName = extensionDisplayName(name, version); @@ -103,8 +105,8 @@ const unpackExtensionInjectable = getInjectable({

)); } finally { - // Remove install state once finished - extensionInstallationStateStore.clearInstalling(id); + // Remove install state once finished + clearExtensionAsInstalling(id); // clean up fse.remove(unpackingTempFolder).catch(noop); diff --git a/packages/core/src/renderer/components/+extensions/extensions.tsx b/packages/core/src/renderer/components/+extensions/extensions.tsx index d1542a336c..cfcc03dffa 100644 --- a/packages/core/src/renderer/components/+extensions/extensions.tsx +++ b/packages/core/src/renderer/components/+extensions/extensions.tsx @@ -35,9 +35,8 @@ import type { LensExtensionId } from "../../../extensions/lens-extension"; import type { InstallOnDrop } from "./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 Gutter from "../gutter/gutter"; +import extensionsInstallingCountInjectable from "../../../features/extensions/installation-states/renderer/installing-count.injectable"; interface Dependencies { userExtensions: IComputedValue; @@ -47,7 +46,7 @@ interface Dependencies { installExtensionFromInput: InstallExtensionFromInput; installFromSelectFileDialog: () => Promise; installOnDrop: InstallOnDrop; - extensionInstallationStateStore: ExtensionInstallationStateStore; + extensionsInstallingCount: IComputedValue; } @observer @@ -64,7 +63,7 @@ class NonInjectedExtensions extends React.Component { reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => { if (curSize > prevSize) { disposeOnUnmount(this, [ - when(() => !this.props.extensionInstallationStateStore.anyInstalling, () => this.installPath = ""), + when(() => this.props.extensionsInstallingCount.get() === 0, () => this.installPath = ""), ]); } }), @@ -138,6 +137,6 @@ export const Extensions = withInjectables(NonInjectedExtensions, { installExtensionFromInput: di.inject(installExtensionFromInputInjectable), installOnDrop: di.inject(installOnDropInjectable), installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable), - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), + extensionsInstallingCount: di.inject(extensionsInstallingCountInjectable), }), }); diff --git a/packages/core/src/renderer/components/+extensions/install-extension-from-input.injectable.tsx b/packages/core/src/renderer/components/+extensions/install-extension-from-input.injectable.tsx index 3f9e5b0949..ccb4ffc986 100644 --- a/packages/core/src/renderer/components/+extensions/install-extension-from-input.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/install-extension-from-input.injectable.tsx @@ -3,19 +3,20 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import React from "react"; -import type { ExtendableDisposer } from "../../../common/utils"; +import type { Disposer } from "../../../common/utils"; +import { noop } from "../../../common/utils"; import { InputValidators } from "../input"; import { getMessageFromError } from "./get-message-from-error/get-message-from-error"; import { getInjectable } from "@ogre-tools/injectable"; import attemptInstallInjectable from "./attempt-install/attempt-install.injectable"; import attemptInstallByInfoInjectable from "./attempt-install-by-info.injectable"; -import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import readFileNotifyInjectable from "./read-file-notify/read-file-notify.injectable"; import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; import loggerInjectable from "../../../common/logger.injectable"; import downloadBinaryInjectable from "../../../common/fetch/download-binary.injectable"; import { withTimeout } from "../../../common/fetch/timeout-controller"; +import startPreInstallPhaseInjectable from "../../../features/extensions/installation-states/renderer/start-pre-install-phase.injectable"; export type InstallExtensionFromInput = (input: string) => Promise; @@ -25,33 +26,34 @@ const installExtensionFromInputInjectable = getInjectable({ instantiate: (di): InstallExtensionFromInput => { const attemptInstall = di.inject(attemptInstallInjectable); const attemptInstallByInfo = di.inject(attemptInstallByInfoInjectable); - const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); const readFileNotify = di.inject(readFileNotifyInjectable); const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); const logger = di.inject(loggerInjectable); const downloadBinary = di.inject(downloadBinaryInjectable); + const startPreInstallPhase = di.inject(startPreInstallPhaseInjectable); return async (input) => { - let disposer: ExtendableDisposer | undefined = undefined; + let clearPreInstallPhase: Disposer = noop; try { // fixme: improve error messages for non-tar-file URLs if (InputValidators.isUrl.validate(input)) { // install via url - disposer = extensionInstallationStateStore.startPreInstall(); + clearPreInstallPhase = startPreInstallPhase(); const { signal } = withTimeout(10 * 60 * 1000); const result = await downloadBinary(input, { signal }); if (!result.callWasSuccessful) { showErrorNotification(`Failed to download extension: ${result.error}`); + clearPreInstallPhase(); - return disposer(); + return; } const fileName = getBasenameOfPath(input); - return await attemptInstall({ fileName, data: result.response }, disposer); + return await attemptInstall({ fileName, data: result.response }, clearPreInstallPhase); } try { @@ -88,7 +90,7 @@ const installExtensionFromInputInjectable = getInjectable({

)); } finally { - disposer?.(); + clearPreInstallPhase(); } }; }, diff --git a/packages/core/src/renderer/components/+extensions/install.tsx b/packages/core/src/renderer/components/+extensions/install.tsx index 7572063796..2b96ed4faf 100644 --- a/packages/core/src/renderer/components/+extensions/install.tsx +++ b/packages/core/src/renderer/components/+extensions/install.tsx @@ -8,14 +8,15 @@ import React from "react"; import { prevDefault } from "../../utils"; import { Button } from "../button"; import { Icon } from "../icon"; -import { observer } from "mobx-react"; import { Input, 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 { unionInputValidatorsAsync } from "../input/input_validators"; +import type { IComputedValue } from "mobx"; +import extensionsInstallingCountInjectable from "../../../features/extensions/installation-states/renderer/installing-count.injectable"; +import extensionsPreinstallingCountInjectable from "../../../features/extensions/installation-states/renderer/preinstalling-count.injectable"; +import { observer } from "mobx-react"; export interface InstallProps { installPath: string; @@ -26,7 +27,8 @@ export interface InstallProps { } interface Dependencies { - extensionInstallationStateStore: ExtensionInstallationStateStore; + extensionsInstallingCount: IComputedValue; + extensionsPreinstallingCount: IComputedValue; } const installInputValidator = unionInputValidatorsAsync( @@ -38,71 +40,67 @@ const installInputValidator = unionInputValidatorsAsync( InputValidators.isPath, ); -const NonInjectedInstall: React.FC = ({ +const NonInjectedInstall = observer(({ installPath, supportedFormats, onChange, installFromInput, installFromSelectFileDialog, - extensionInstallationStateStore, -}) => ( -
- -
-
- - )} - /> -
-
-
-
- - Pro-Tip - : you can drag and drop a tarball file to this area - -
-); + extensionsInstallingCount, + extensionsPreinstallingCount, +}: Dependencies & InstallProps) => { + const anyPreInstallingOrInstalling = extensionsInstallingCount.get() > 0 || extensionsPreinstallingCount.get() > 0; -export const Install = withInjectables( - observer(NonInjectedInstall), - { - getProps: (di, props) => ({ - extensionInstallationStateStore: di.inject( - extensionInstallationStateStoreInjectable, - ), + return ( +
+ +
+
+ + )} + /> +
+
+
+
+ + Pro-Tip + : you can drag and drop a tarball file to this area + +
+ ); +}); - ...props, - }), - }, -); +export const Install = withInjectables(NonInjectedInstall, { + getProps: (di, props) => ({ + ...props, + extensionsInstallingCount: di.inject(extensionsInstallingCountInjectable), + extensionsPreinstallingCount: di.inject(extensionsPreinstallingCountInjectable), + }), +}); diff --git a/packages/core/src/renderer/components/+extensions/installed-extensions.tsx b/packages/core/src/renderer/components/+extensions/installed-extensions.tsx index 4ddad164df..8ef970c67f 100644 --- a/packages/core/src/renderer/components/+extensions/installed-extensions.tsx +++ b/packages/core/src/renderer/components/+extensions/installed-extensions.tsx @@ -5,9 +5,7 @@ import styles from "./installed-extensions.module.scss"; import React, { useMemo } from "react"; -import type { - InstalledExtension, -} from "../../../extensions/extension-discovery/extension-discovery"; +import type { InstalledExtension } from "../../../extensions/extension-discovery/extension-discovery"; import { Icon } from "../icon"; import { List } from "../list/list"; import { MenuActions, MenuItem } from "../menu"; @@ -16,14 +14,12 @@ import { cssNames, toJS } from "../../utils"; import { observer } from "mobx-react"; import type { Row } from "react-table"; import type { LensExtensionId } from "../../../extensions/lens-extension"; - - 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 initialDiscoveryLoadCompletedInjectable from "../../../features/extensions/discovery/common/initial-load-completed.injectable"; +import type { GetExtensionInstallationPhase } from "../../../features/extensions/installation-states/renderer/get-phase.injectable"; +import getExtensionInstallationPhaseInjectable from "../../../features/extensions/installation-states/renderer/get-phase.injectable"; +import extensionsUninstallingCountInjectable from "../../../features/extensions/installation-states/renderer/uninstalling-count.injectable"; export interface InstalledExtensionsProps { extensions: InstalledExtension[]; @@ -34,7 +30,8 @@ export interface InstalledExtensionsProps { interface Dependencies { initialDiscoveryLoadCompleted: IComputedValue; - extensionInstallationStateStore: ExtensionInstallationStateStore; + getExtensionInstallationPhase: GetExtensionInstallationPhase; + extensionsUninstallingCount: IComputedValue; } function getStatus(extension: InstalledExtension) { @@ -47,11 +44,12 @@ function getStatus(extension: InstalledExtension) { const NonInjectedInstalledExtensions = observer(({ initialDiscoveryLoadCompleted, - extensionInstallationStateStore, + getExtensionInstallationPhase, extensions, uninstall, enable, disable, + extensionsUninstallingCount, }: Dependencies & InstalledExtensionsProps) => { const columns = useMemo( () => [ @@ -91,7 +89,7 @@ const NonInjectedInstalledExtensions = observer(({ () => extensions.map(extension => { const { id, isEnabled, isCompatible, manifest } = extension; const { name, description, version } = manifest; - const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id); + const isUninstalling = getExtensionInstallationPhase(id) === "uninstalling"; return { extension: ( @@ -145,7 +143,7 @@ const NonInjectedInstalledExtensions = observer(({ ), }; - }), [toJS(extensions), extensionInstallationStateStore.anyUninstalling], + }), [toJS(extensions), extensionsUninstallingCount.get() > 0], ); if (!initialDiscoveryLoadCompleted.get()) { @@ -184,7 +182,8 @@ const NonInjectedInstalledExtensions = observer(({ export const InstalledExtensions = withInjectables(NonInjectedInstalledExtensions, { getProps: (di, props) => ({ ...props, - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), initialDiscoveryLoadCompleted: di.inject(initialDiscoveryLoadCompletedInjectable).value, + getExtensionInstallationPhase: di.inject(getExtensionInstallationPhaseInjectable), + extensionsUninstallingCount: di.inject(extensionsUninstallingCountInjectable), }), }); diff --git a/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx b/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx index dae0781af1..3f4a3b036b 100644 --- a/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx +++ b/packages/core/src/renderer/components/+extensions/uninstall-extension.injectable.tsx @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import loggerInjectable from "../../../common/logger.injectable"; import type { LensExtensionId } from "../../../extensions/lens-extension"; import { extensionDisplayName } from "../../../extensions/lens-extension"; @@ -15,18 +14,21 @@ import showErrorNotificationInjectable from "../notifications/show-error-notific import installedUserExtensionsInjectable from "../../../features/extensions/common/user-extensions.injectable"; import getInstalledExtensionInjectable from "../../../features/extensions/common/get-installed-extension.injectable"; import removeExtensionFilesInjectable from "../../../features/extensions/discovery/common/uninstall-extension.injectable"; +import clearExtensionAsInstallingInjectable from "../../../features/extensions/installation-states/renderer/clear-as-installing.injectable"; +import setExtensionAsUninstallingInjectable from "../../../features/extensions/installation-states/renderer/set-as-uninstalling.injectable"; const uninstallExtensionInjectable = getInjectable({ id: "uninstall-extension", instantiate: (di) => { - const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); const logger = di.inject(loggerInjectable); const showSuccessNotification = di.inject(showSuccessNotificationInjectable); const showErrorNotification = di.inject(showErrorNotificationInjectable); const installedUserExtensions = di.inject(installedUserExtensionsInjectable); const getInstalledExtension = di.inject(getInstalledExtensionInjectable); const removeExtensionFiles = di.inject(removeExtensionFilesInjectable); + const clearExtensionAsInstalling = di.inject(clearExtensionAsInstallingInjectable); + const setExtensionAsUninstalling = di.inject(setExtensionAsUninstallingInjectable); return async (extensionId: LensExtensionId): Promise => { const ext = getInstalledExtension(extensionId); @@ -42,8 +44,8 @@ const uninstallExtensionInjectable = getInjectable({ try { logger.debug(`[EXTENSIONS]: trying to uninstall ${extensionId}`); - extensionInstallationStateStore.setUninstalling(extensionId); + setExtensionAsUninstalling(extensionId); await removeExtensionFiles(extensionId); // wait for the ExtensionLoader to actually uninstall the extension @@ -76,8 +78,7 @@ const uninstallExtensionInjectable = getInjectable({ return false; } finally { - // Remove uninstall state on uninstall failure - extensionInstallationStateStore.clearUninstalling(extensionId); + clearExtensionAsInstalling(extensionId); } }; },