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);
}
};
},