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:
parent
4f431c8bf5
commit
73f9f19cc9
@ -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/></>);
|
||||||
|
|
||||||
|
|||||||
@ -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>();
|
||||||
|
}
|
||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user