mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Remove legacy IPC usage
- From extension installation state store Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
edafcc916c
commit
2164b4b010
@ -7,7 +7,7 @@ import type { MessageChannel } from "./message-channel-listener-injection-token"
|
||||
|
||||
export interface SendMessageToChannel {
|
||||
(channel: MessageChannel<void>): void;
|
||||
<Message>(channel: MessageChannel<Message>, message: Message): void;
|
||||
<Channel extends MessageChannel<unknown>>(channel: Channel, message: Channel extends MessageChannel<infer Message> ? Message : never): void;
|
||||
}
|
||||
|
||||
export type MessageChannelSender<Channel> = Channel extends MessageChannel<void | undefined>
|
||||
|
||||
@ -14,6 +14,7 @@ interface Iterator<T> extends Iterable<T> {
|
||||
flatMap<U>(fn: (val: T) => U[]): Iterator<U>;
|
||||
concat(src2: IterableIterator<T>): Iterator<T>;
|
||||
join(sep?: string): string;
|
||||
count(): number;
|
||||
}
|
||||
|
||||
export function chain<T>(src: IterableIterator<T>): Iterator<T> {
|
||||
@ -26,6 +27,7 @@ export function chain<T>(src: IterableIterator<T>): Iterator<T> {
|
||||
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<T>(...sources: IterableIterator<T>[]): IterableIterator<
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function count(src: Iterable<unknown>): number {
|
||||
let i = 0;
|
||||
|
||||
for (const x of src) {
|
||||
void x;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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<string>();
|
||||
private readonly uninstallingExtensions = observable.set<string>();
|
||||
private readonly installingExtensions = observable.set<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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<void> => {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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<ExtensionInstallPhaseData> = {
|
||||
id: "set-extension-install-phase",
|
||||
};
|
||||
@ -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<SetExtensionAsInstalling>({
|
||||
id: "set-extension-as-installing-token",
|
||||
});
|
||||
|
||||
export type ClearExtensionAsInstalling = (id: LensExtensionId) => void;
|
||||
|
||||
export const clearExtensionAsInstallingInjectionToken = getInjectionToken<ClearExtensionAsInstalling>({
|
||||
id: "clear-extension-as-installing-token",
|
||||
});
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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<string>(),
|
||||
});
|
||||
|
||||
export default preinstallingPhasesInjectable;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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<LensExtensionId, InstallationState>(),
|
||||
});
|
||||
|
||||
export default extensionInstallationStatesInjectable;
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<LensExtensionId, InstalledExtension>;
|
||||
let installExtensionFromInput: jest.MockedFunction<InstallExtensionFromInput>;
|
||||
let extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
let render: DiRender;
|
||||
let deleteFileMock: jest.MockedFunction<RemovePath>;
|
||||
let downloadBinary: jest.MockedFunction<DownloadBinary>;
|
||||
let isLoaded: IObservableValue<boolean>;
|
||||
let removeExtensionFilesMock: jest.MockedFunction<RemoveExtensionFiles>;
|
||||
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", {
|
||||
|
||||
@ -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({
|
||||
</p>
|
||||
));
|
||||
|
||||
return disposer();
|
||||
return clearPreInstallPhase();
|
||||
}
|
||||
|
||||
version = potentialVersion;
|
||||
@ -137,7 +137,7 @@ const attemptInstallByInfoInjectable = getInjectable({
|
||||
</p>
|
||||
));
|
||||
|
||||
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);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<void>;
|
||||
|
||||
@ -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({
|
||||
</p>
|
||||
));
|
||||
} finally {
|
||||
// Remove install state once finished
|
||||
extensionInstallationStateStore.clearInstalling(id);
|
||||
// Remove install state once finished
|
||||
clearExtensionAsInstalling(id);
|
||||
|
||||
// clean up
|
||||
fse.remove(unpackingTempFolder).catch(noop);
|
||||
|
||||
@ -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<InstalledExtension[]>;
|
||||
@ -47,7 +46,7 @@ interface Dependencies {
|
||||
installExtensionFromInput: InstallExtensionFromInput;
|
||||
installFromSelectFileDialog: () => Promise<void>;
|
||||
installOnDrop: InstallOnDrop;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
extensionsInstallingCount: IComputedValue<number>;
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -64,7 +63,7 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
|
||||
reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => {
|
||||
if (curSize > prevSize) {
|
||||
disposeOnUnmount(this, [
|
||||
when(() => !this.props.extensionInstallationStateStore.anyInstalling, () => this.installPath = ""),
|
||||
when(() => this.props.extensionsInstallingCount.get() === 0, () => this.installPath = ""),
|
||||
]);
|
||||
}
|
||||
}),
|
||||
@ -138,6 +137,6 @@ export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
|
||||
installExtensionFromInput: di.inject(installExtensionFromInputInjectable),
|
||||
installOnDrop: di.inject(installOnDropInjectable),
|
||||
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
extensionsInstallingCount: di.inject(extensionsInstallingCountInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -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<void>;
|
||||
|
||||
@ -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({
|
||||
</p>
|
||||
));
|
||||
} finally {
|
||||
disposer?.();
|
||||
clearPreInstallPhase();
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@ -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<number>;
|
||||
extensionsPreinstallingCount: IComputedValue<number>;
|
||||
}
|
||||
|
||||
const installInputValidator = unionInputValidatorsAsync(
|
||||
@ -38,71 +40,67 @@ const installInputValidator = unionInputValidatorsAsync(
|
||||
InputValidators.isPath,
|
||||
);
|
||||
|
||||
const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
|
||||
const NonInjectedInstall = observer(({
|
||||
installPath,
|
||||
supportedFormats,
|
||||
onChange,
|
||||
installFromInput,
|
||||
installFromSelectFileDialog,
|
||||
extensionInstallationStateStore,
|
||||
}) => (
|
||||
<section>
|
||||
<SubTitle
|
||||
title={`Name or file path or URL to an extension package (${supportedFormats.join(
|
||||
", ",
|
||||
)})`}
|
||||
/>
|
||||
<div className={styles.inputs}>
|
||||
<div>
|
||||
<Input
|
||||
theme="round-black"
|
||||
disabled={extensionInstallationStateStore.anyPreInstallingOrInstalling}
|
||||
placeholder={"Name or file path or URL"}
|
||||
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
||||
validators={installPath ? installInputValidator : undefined}
|
||||
value={installPath}
|
||||
onChange={onChange}
|
||||
onSubmit={installFromInput}
|
||||
iconRight={(
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
smallest
|
||||
material="folder_open"
|
||||
onClick={prevDefault(installFromSelectFileDialog)}
|
||||
tooltip="Browse"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className={styles.button}
|
||||
primary
|
||||
label="Install"
|
||||
disabled={
|
||||
extensionInstallationStateStore.anyPreInstallingOrInstalling
|
||||
}
|
||||
waiting={extensionInstallationStateStore.anyPreInstallingOrInstalling}
|
||||
onClick={installFromInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<small className={styles.proTip}>
|
||||
<b>Pro-Tip</b>
|
||||
: you can drag and drop a tarball file to this area
|
||||
</small>
|
||||
</section>
|
||||
);
|
||||
extensionsInstallingCount,
|
||||
extensionsPreinstallingCount,
|
||||
}: Dependencies & InstallProps) => {
|
||||
const anyPreInstallingOrInstalling = extensionsInstallingCount.get() > 0 || extensionsPreinstallingCount.get() > 0;
|
||||
|
||||
export const Install = withInjectables<Dependencies, InstallProps>(
|
||||
observer(NonInjectedInstall),
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
extensionInstallationStateStore: di.inject(
|
||||
extensionInstallationStateStoreInjectable,
|
||||
),
|
||||
return (
|
||||
<section>
|
||||
<SubTitle
|
||||
title={`Name or file path or URL to an extension package (${supportedFormats.join(", ")})`}
|
||||
/>
|
||||
<div className={styles.inputs}>
|
||||
<div>
|
||||
<Input
|
||||
theme="round-black"
|
||||
disabled={anyPreInstallingOrInstalling}
|
||||
placeholder={"Name or file path or URL"}
|
||||
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
||||
validators={installPath ? installInputValidator : undefined}
|
||||
value={installPath}
|
||||
onChange={onChange}
|
||||
onSubmit={installFromInput}
|
||||
iconRight={(
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
smallest
|
||||
material="folder_open"
|
||||
onClick={prevDefault(installFromSelectFileDialog)}
|
||||
tooltip="Browse"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className={styles.button}
|
||||
primary
|
||||
label="Install"
|
||||
disabled={anyPreInstallingOrInstalling}
|
||||
waiting={anyPreInstallingOrInstalling}
|
||||
onClick={installFromInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<small className={styles.proTip}>
|
||||
<b>Pro-Tip</b>
|
||||
: you can drag and drop a tarball file to this area
|
||||
</small>
|
||||
</section>
|
||||
);
|
||||
});
|
||||
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
export const Install = withInjectables<Dependencies, InstallProps>(NonInjectedInstall, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
extensionsInstallingCount: di.inject(extensionsInstallingCountInjectable),
|
||||
extensionsPreinstallingCount: di.inject(extensionsPreinstallingCountInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -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<boolean>;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
getExtensionInstallationPhase: GetExtensionInstallationPhase;
|
||||
extensionsUninstallingCount: IComputedValue<number>;
|
||||
}
|
||||
|
||||
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(({
|
||||
</MenuActions>
|
||||
),
|
||||
};
|
||||
}), [toJS(extensions), extensionInstallationStateStore.anyUninstalling],
|
||||
}), [toJS(extensions), extensionsUninstallingCount.get() > 0],
|
||||
);
|
||||
|
||||
if (!initialDiscoveryLoadCompleted.get()) {
|
||||
@ -184,7 +182,8 @@ const NonInjectedInstalledExtensions = observer(({
|
||||
export const InstalledExtensions = withInjectables<Dependencies, InstalledExtensionsProps>(NonInjectedInstalledExtensions, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
initialDiscoveryLoadCompleted: di.inject(initialDiscoveryLoadCompletedInjectable).value,
|
||||
getExtensionInstallationPhase: di.inject(getExtensionInstallationPhaseInjectable),
|
||||
extensionsUninstallingCount: di.inject(extensionsUninstallingCountInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -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<boolean> => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user