1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

persist extension installation state for the duration of the window's life (#1602)

* persist extension installation state for the duration of the window's life

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2020-12-04 09:25:37 -05:00 committed by Jari Kolehmainen
parent 4f431c8bf5
commit 73f9f19cc9
3 changed files with 60 additions and 41 deletions

View File

@ -5,6 +5,7 @@ import React from "react";
import { extensionDiscovery } from "../../../../extensions/extension-discovery"; import { extensionDiscovery } from "../../../../extensions/extension-discovery";
import { ConfirmDialog } from "../../confirm-dialog"; import { ConfirmDialog } from "../../confirm-dialog";
import { Notifications } from "../../notifications"; import { Notifications } from "../../notifications";
import { ExtensionStateStore } from "../extension-install.store";
import { Extensions } from "../extensions"; import { Extensions } from "../extensions";
jest.mock("fs-extra"); jest.mock("fs-extra");
@ -51,6 +52,10 @@ jest.mock("../../notifications", () => ({
})); }));
describe("Extensions", () => { describe("Extensions", () => {
beforeEach(() => {
ExtensionStateStore.resetInstance();
});
it("disables uninstall and disable buttons while uninstalling", async () => { it("disables uninstall and disable buttons while uninstalling", async () => {
render(<><Extensions /><ConfirmDialog/></>); render(<><Extensions /><ConfirmDialog/></>);
@ -61,14 +66,14 @@ describe("Extensions", () => {
// Approve confirm dialog // Approve confirm dialog
fireEvent.click(screen.getByText("Yes")); fireEvent.click(screen.getByText("Yes"));
expect(extensionDiscovery.uninstallExtension).toHaveBeenCalledWith("/absolute/path"); expect(extensionDiscovery.uninstallExtension).toHaveBeenCalledWith("/absolute/path");
expect(screen.getByText("Disable").closest("button")).toBeDisabled(); expect(screen.getByText("Disable").closest("button")).toBeDisabled();
expect(screen.getByText("Uninstall").closest("button")).toBeDisabled(); expect(screen.getByText("Uninstall").closest("button")).toBeDisabled();
}); });
it("displays error notification on uninstall error", () => { it("displays error notification on uninstall error", () => {
(extensionDiscovery.uninstallExtension as any).mockImplementationOnce(() => (extensionDiscovery.uninstallExtension as any).mockImplementationOnce(() =>
Promise.reject() Promise.reject()
); );
render(<><Extensions /><ConfirmDialog/></>); render(<><Extensions /><ConfirmDialog/></>);

View File

@ -0,0 +1,13 @@
import { observable } from "mobx";
import { autobind, Singleton } from "../../utils";
interface ExtensionState {
displayName: string;
// Possible states the extension can be
state: "installing" | "uninstalling";
}
@autobind()
export class ExtensionStateStore extends Singleton {
extensionState = observable.map<string, ExtensionState>();
}

View File

@ -22,6 +22,7 @@ import { PageLayout } from "../layout/page-layout";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { TooltipPosition } from "../tooltip"; import { TooltipPosition } from "../tooltip";
import { ExtensionStateStore } from "./extension-install.store";
import "./extensions.scss"; import "./extensions.scss";
interface InstallRequest { interface InstallRequest {
@ -39,25 +40,20 @@ interface InstallRequestValidated extends InstallRequestPreloaded {
tempFile: string; // temp system path to packed extension for unpacking tempFile: string; // temp system path to packed extension for unpacking
} }
interface ExtensionState {
displayName: string;
// Possible states the extension can be
state: "installing" | "uninstalling";
}
@observer @observer
export class Extensions extends React.Component { export class Extensions extends React.Component {
private supportedFormats = ["tar", "tgz"]; private static supportedFormats = ["tar", "tgz"];
private installPathValidator: InputValidator = { private static installPathValidator: InputValidator = {
message: <Trans>Invalid URL or absolute path</Trans>, message: <Trans>Invalid URL or absolute path</Trans>,
validate(value: string) { validate(value: string) {
return InputValidators.isUrl.validate(value) || InputValidators.isPath.validate(value); return InputValidators.isUrl.validate(value) || InputValidators.isPath.validate(value);
} }
}; };
@observable get extensionStateStore() {
extensionState = observable.map<string, ExtensionState>(); return ExtensionStateStore.getInstance<ExtensionStateStore>();
}
@observable search = ""; @observable search = "";
@observable installPath = ""; @observable installPath = "";
@ -69,18 +65,24 @@ export class Extensions extends React.Component {
* Extensions that were removed from extensions but are still in "uninstalling" state * Extensions that were removed from extensions but are still in "uninstalling" state
*/ */
@computed get removedUninstalling() { @computed get removedUninstalling() {
return Array.from(this.extensionState.entries()).filter(([id, extension]) => return Array.from(this.extensionStateStore.extensionState.entries())
extension.state === "uninstalling" && !this.extensions.find(extension => extension.id === id) .filter(([id, extension]) =>
).map(([id, extension]) => ({ ...extension, id })); extension.state === "uninstalling"
&& !this.extensions.find(extension => extension.id === id)
)
.map(([id, extension]) => ({ ...extension, id }));
} }
/** /**
* Extensions that were added to extensions but are still in "installing" state * Extensions that were added to extensions but are still in "installing" state
*/ */
@computed get addedInstalling() { @computed get addedInstalling() {
return Array.from(this.extensionState.entries()).filter(([id, extension]) => return Array.from(this.extensionStateStore.extensionState.entries())
extension.state === "installing" && this.extensions.find(extension => extension.id === id) .filter(([id, extension]) =>
).map(([id, extension]) => ({ ...extension, id })); extension.state === "installing"
&& this.extensions.find(extension => extension.id === id)
)
.map(([id, extension]) => ({ ...extension, id }));
} }
componentDidMount() { componentDidMount() {
@ -90,7 +92,7 @@ export class Extensions extends React.Component {
Notifications.ok( Notifications.ok(
<p>Extension <b>{displayName}</b> successfully uninstalled!</p> <p>Extension <b>{displayName}</b> successfully uninstalled!</p>
); );
this.extensionState.delete(id); this.extensionStateStore.extensionState.delete(id);
}); });
this.addedInstalling.forEach(({ id, displayName }) => { this.addedInstalling.forEach(({ id, displayName }) => {
@ -103,7 +105,7 @@ export class Extensions extends React.Component {
Notifications.ok( Notifications.ok(
<p>Extension <b>{displayName}</b> successfully installed!</p> <p>Extension <b>{displayName}</b> successfully installed!</p>
); );
this.extensionState.delete(id); this.extensionStateStore.extensionState.delete(id);
this.installPath = ""; this.installPath = "";
// Enable installed extensions by default. // Enable installed extensions by default.
@ -116,14 +118,11 @@ export class Extensions extends React.Component {
@computed get extensions() { @computed get extensions() {
const searchText = this.search.toLowerCase(); const searchText = this.search.toLowerCase();
return Array.from(extensionLoader.userExtensions.values()).filter(ext => { return Array.from(extensionLoader.userExtensions.values())
const { name, description } = ext.manifest; .filter(({ manifest: { name, description }}) => (
name.toLowerCase().includes(searchText)
return [ || description?.toLowerCase().includes(searchText)
name.toLowerCase().includes(searchText), ));
description?.toLowerCase().includes(searchText),
].some(value => value);
});
} }
get extensionsPath() { get extensionsPath() {
@ -143,10 +142,10 @@ export class Extensions extends React.Component {
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), { const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
defaultPath: app.getPath("downloads"), defaultPath: app.getPath("downloads"),
properties: ["openFile", "multiSelections"], properties: ["openFile", "multiSelections"],
message: _i18n._(t`Select extensions to install (formats: ${this.supportedFormats.join(", ")}), `), message: _i18n._(t`Select extensions to install (formats: ${Extensions.supportedFormats.join(", ")}), `),
buttonLabel: _i18n._(t`Use configuration`), buttonLabel: _i18n._(t`Use configuration`),
filters: [ filters: [
{ name: "tarball", extensions: this.supportedFormats } { name: "tarball", extensions: Extensions.supportedFormats }
] ]
}); });
@ -346,7 +345,9 @@ export class Extensions extends React.Component {
const displayName = extensionDisplayName(name, version); const displayName = extensionDisplayName(name, version);
const extensionId = path.join(extensionDiscovery.nodeModulesPath, name, "package.json"); const extensionId = path.join(extensionDiscovery.nodeModulesPath, name, "package.json");
this.extensionState.set(extensionId, { logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
this.extensionStateStore.extensionState.set(extensionId, {
state: "installing", state: "installing",
displayName displayName
}); });
@ -381,8 +382,8 @@ export class Extensions extends React.Component {
); );
// Remove install state on install failure // Remove install state on install failure
if (this.extensionState.get(extensionId)?.state === "installing") { if (this.extensionStateStore.extensionState.get(extensionId)?.state === "installing") {
this.extensionState.delete(extensionId); this.extensionStateStore.extensionState.delete(extensionId);
} }
} finally { } finally {
// clean up // clean up
@ -406,7 +407,7 @@ export class Extensions extends React.Component {
const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version); const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
try { try {
this.extensionState.set(extension.id, { this.extensionStateStore.extensionState.set(extension.id, {
state: "uninstalling", state: "uninstalling",
displayName displayName
}); });
@ -418,8 +419,8 @@ export class Extensions extends React.Component {
); );
// Remove uninstall state on uninstall failure // Remove uninstall state on uninstall failure
if (this.extensionState.get(extension.id)?.state === "uninstalling") { if (this.extensionStateStore.extensionState.get(extension.id)?.state === "uninstalling") {
this.extensionState.delete(extension.id); this.extensionStateStore.extensionState.delete(extension.id);
} }
} }
} }
@ -445,7 +446,7 @@ export class Extensions extends React.Component {
return extensions.map(extension => { return extensions.map(extension => {
const { id, isEnabled, manifest } = extension; const { id, isEnabled, manifest } = extension;
const { name, description } = manifest; const { name, description } = manifest;
const isUninstalling = this.extensionState.get(id)?.state === "uninstalling"; const isUninstalling = this.extensionStateStore.extensionState.get(id)?.state === "uninstalling";
return ( return (
<div key={id} className="extension flex gaps align-center"> <div key={id} className="extension flex gaps align-center">
@ -481,7 +482,7 @@ export class Extensions extends React.Component {
* True if at least one extension is in installing state * True if at least one extension is in installing state
*/ */
@computed get isInstalling() { @computed get isInstalling() {
return this.startingInstall || [...this.extensionState.values()].some(extension => extension.state === "installing"); return [...this.extensionStateStore.extensionState.values()].some(extension => extension.state === "installing");
} }
render() { render() {
@ -504,9 +505,9 @@ export class Extensions extends React.Component {
className="box grow" className="box grow"
theme="round-black" theme="round-black"
disabled={this.isInstalling} disabled={this.isInstalling}
placeholder={`Path or URL to an extension package (${this.supportedFormats.join(", ")})`} placeholder={`Path or URL to an extension package (${Extensions.supportedFormats.join(", ")})`}
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }} showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
validators={installPath ? this.installPathValidator : undefined} validators={installPath ? Extensions.installPathValidator : undefined}
value={installPath} value={installPath}
onChange={value => this.installPath = value} onChange={value => this.installPath = value}
onSubmit={this.installFromUrlOrPath} onSubmit={this.installFromUrlOrPath}
@ -524,7 +525,7 @@ export class Extensions extends React.Component {
<Button <Button
primary primary
label="Install" label="Install"
disabled={this.isInstalling || !this.installPathValidator.validate(installPath)} disabled={this.isInstalling || !Extensions.installPathValidator.validate(installPath)}
waiting={this.isInstalling} waiting={this.isInstalling}
onClick={this.installFromUrlOrPath} onClick={this.installFromUrlOrPath}
/> />