mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add extension uninstall (#1524)
Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
parent
95d7ff847e
commit
ccd38b5cbe
@ -18,6 +18,7 @@ jest.mock(
|
|||||||
name: "TestExtension",
|
name: "TestExtension",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
},
|
},
|
||||||
|
absolutePath: "/test/1",
|
||||||
manifestPath,
|
manifestPath,
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
@ -30,6 +31,7 @@ jest.mock(
|
|||||||
name: "TestExtension2",
|
name: "TestExtension2",
|
||||||
version: "2.0.0",
|
version: "2.0.0",
|
||||||
},
|
},
|
||||||
|
absolutePath: "/test/2",
|
||||||
manifestPath: manifestPath2,
|
manifestPath: manifestPath2,
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
@ -52,6 +54,7 @@ jest.mock(
|
|||||||
name: "TestExtension",
|
name: "TestExtension",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
},
|
},
|
||||||
|
absolutePath: "/test/1",
|
||||||
manifestPath,
|
manifestPath,
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
@ -64,7 +67,8 @@ jest.mock(
|
|||||||
name: "TestExtension3",
|
name: "TestExtension3",
|
||||||
version: "3.0.0",
|
version: "3.0.0",
|
||||||
},
|
},
|
||||||
manifestPath3,
|
absolutePath: "/test/3",
|
||||||
|
manifestPath: manifestPath3,
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
},
|
},
|
||||||
@ -94,6 +98,7 @@ describe("ExtensionLoader", () => {
|
|||||||
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`
|
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`
|
||||||
Map {
|
Map {
|
||||||
"manifest/path" => Object {
|
"manifest/path" => Object {
|
||||||
|
"absolutePath": "/test/1",
|
||||||
"isBundled": false,
|
"isBundled": false,
|
||||||
"isEnabled": true,
|
"isEnabled": true,
|
||||||
"manifest": Object {
|
"manifest": Object {
|
||||||
@ -103,13 +108,14 @@ describe("ExtensionLoader", () => {
|
|||||||
"manifestPath": "manifest/path",
|
"manifestPath": "manifest/path",
|
||||||
},
|
},
|
||||||
"manifest/path3" => Object {
|
"manifest/path3" => Object {
|
||||||
|
"absolutePath": "/test/3",
|
||||||
"isBundled": false,
|
"isBundled": false,
|
||||||
"isEnabled": true,
|
"isEnabled": true,
|
||||||
"manifest": Object {
|
"manifest": Object {
|
||||||
"name": "TestExtension3",
|
"name": "TestExtension3",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
},
|
},
|
||||||
"manifestPath3": "manifest/path3",
|
"manifestPath": "manifest/path3",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|||||||
@ -9,6 +9,7 @@ describe("lens extension", () => {
|
|||||||
name: "foo-bar",
|
name: "foo-bar",
|
||||||
version: "0.1.1"
|
version: "0.1.1"
|
||||||
},
|
},
|
||||||
|
absolutePath: "/absolute/fake/",
|
||||||
manifestPath: "/this/is/fake/package.json",
|
manifestPath: "/this/is/fake/package.json",
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true
|
isEnabled: true
|
||||||
|
|||||||
@ -11,6 +11,12 @@ import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
|||||||
|
|
||||||
export interface InstalledExtension {
|
export interface InstalledExtension {
|
||||||
readonly manifest: LensExtensionManifest;
|
readonly manifest: LensExtensionManifest;
|
||||||
|
|
||||||
|
// Absolute path to the non-symlinked source folder,
|
||||||
|
// e.g. "/Users/user/.k8slens/extensions/helloworld"
|
||||||
|
readonly absolutePath: string;
|
||||||
|
|
||||||
|
// Absolute to the symlinked package.json file
|
||||||
readonly manifestPath: string;
|
readonly manifestPath: string;
|
||||||
readonly isBundled: boolean; // defined in project root's package.json
|
readonly isBundled: boolean; // defined in project root's package.json
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
@ -174,6 +180,24 @@ export class ExtensionDiscovery {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstalls extension by path.
|
||||||
|
* The application will detect the folder unlink and remove the extension from the UI automatically.
|
||||||
|
* @param absolutePath Path to the non-symlinked folder of the extension
|
||||||
|
*/
|
||||||
|
async uninstallExtension(absolutePath: string) {
|
||||||
|
logger.info(`${logModule} Uninstalling ${absolutePath}`);
|
||||||
|
|
||||||
|
const exists = await fs.pathExists(absolutePath);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
throw new Error(`Extension path ${absolutePath} doesn't exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fs.remove does nothing if the path doesn't exist anymore
|
||||||
|
await fs.remove(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
||||||
if (this.loadStarted) {
|
if (this.loadStarted) {
|
||||||
// The class is simplified by only supporting .load() to be called once
|
// The class is simplified by only supporting .load() to be called once
|
||||||
@ -230,6 +254,7 @@ export class ExtensionDiscovery {
|
|||||||
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
absolutePath: path.dirname(manifestPath),
|
||||||
manifestPath: installedManifestPath,
|
manifestPath: installedManifestPath,
|
||||||
manifest: manifestJson,
|
manifest: manifestJson,
|
||||||
isBundled,
|
isBundled,
|
||||||
|
|||||||
@ -68,15 +68,18 @@ export class ExtensionLoader {
|
|||||||
logger.info(`${logModule} deleting extension instance ${lensExtensionId}`);
|
logger.info(`${logModule} deleting extension instance ${lensExtensionId}`);
|
||||||
const instance = this.instances.get(lensExtensionId);
|
const instance = this.instances.get(lensExtensionId);
|
||||||
|
|
||||||
if (instance) {
|
if (!instance) {
|
||||||
try {
|
return;
|
||||||
instance.disable();
|
|
||||||
this.events.emit("remove", instance);
|
|
||||||
this.instances.delete(lensExtensionId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
instance.disable();
|
||||||
|
this.events.emit("remove", instance);
|
||||||
|
this.instances.delete(lensExtensionId);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeExtension(lensExtensionId: LensExtensionId) {
|
removeExtension(lensExtensionId: LensExtensionId) {
|
||||||
@ -85,7 +88,6 @@ export class ExtensionLoader {
|
|||||||
if (!this.extensions.delete(lensExtensionId)) {
|
if (!this.extensions.delete(lensExtensionId)) {
|
||||||
throw new Error(`Can't remove extension ${lensExtensionId}, doesn't exist.`);
|
throw new Error(`Can't remove extension ${lensExtensionId}, doesn't exist.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async initMain() {
|
protected async initMain() {
|
||||||
|
|||||||
@ -114,3 +114,7 @@ export class LensExtension {
|
|||||||
export function sanitizeExtensionName(name: string) {
|
export function sanitizeExtensionName(name: string) {
|
||||||
return name.replace("@", "").replace("/", "--");
|
return name.replace("@", "").replace("/", "--");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extensionDisplayName(name: string, version: string) {
|
||||||
|
return `${name}@${version}`;
|
||||||
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ describe("getPageUrl", () => {
|
|||||||
name: "foo-bar",
|
name: "foo-bar",
|
||||||
version: "0.1.1"
|
version: "0.1.1"
|
||||||
},
|
},
|
||||||
|
absolutePath: "/absolute/fake/",
|
||||||
manifestPath: "/this/is/fake/package.json",
|
manifestPath: "/this/is/fake/package.json",
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true
|
isEnabled: true
|
||||||
@ -41,6 +42,7 @@ describe("globalPageRegistry", () => {
|
|||||||
name: "@acme/foo-bar",
|
name: "@acme/foo-bar",
|
||||||
version: "0.1.1"
|
version: "0.1.1"
|
||||||
},
|
},
|
||||||
|
absolutePath: "/absolute/fake/",
|
||||||
manifestPath: "/this/is/fake/package.json",
|
manifestPath: "/this/is/fake/package.json",
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true
|
isEnabled: true
|
||||||
|
|||||||
@ -26,6 +26,10 @@
|
|||||||
padding: $padding $spacing;
|
padding: $padding $spacing;
|
||||||
background: $layoutBackground;
|
background: $layoutBackground;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
|
|
||||||
|
.actions > button:not(:last-child) {
|
||||||
|
margin-right: $spacing / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +1,27 @@
|
|||||||
import "./extensions.scss";
|
import { t, Trans } from "@lingui/macro";
|
||||||
import { remote, shell } from "electron";
|
import { remote, shell } from "electron";
|
||||||
import os from "os";
|
|
||||||
import path from "path";
|
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import React from "react";
|
|
||||||
import { computed, observable } from "mobx";
|
import { computed, observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { t, Trans } from "@lingui/macro";
|
import os from "os";
|
||||||
import { _i18n } from "../../i18n";
|
import path from "path";
|
||||||
import { Button } from "../button";
|
import React from "react";
|
||||||
import { DropFileInput, Input, InputValidator, InputValidators, SearchInput } from "../input";
|
|
||||||
import { Icon } from "../icon";
|
|
||||||
import { SubTitle } from "../layout/sub-title";
|
|
||||||
import { PageLayout } from "../layout/page-layout";
|
|
||||||
import logger from "../../../main/logger";
|
|
||||||
import { extensionLoader } from "../../../extensions/extension-loader";
|
|
||||||
import { extensionDiscovery, manifestFilename } from "../../../extensions/extension-discovery";
|
|
||||||
import { LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension";
|
|
||||||
import { Notifications } from "../notifications";
|
|
||||||
import { downloadFile, extractTar, listTarEntries, readFileFromTar } from "../../../common/utils";
|
import { downloadFile, extractTar, listTarEntries, readFileFromTar } from "../../../common/utils";
|
||||||
import { docsUrl } from "../../../common/vars";
|
import { docsUrl } from "../../../common/vars";
|
||||||
|
import { extensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery";
|
||||||
|
import { extensionLoader } from "../../../extensions/extension-loader";
|
||||||
|
import { extensionDisplayName, LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension";
|
||||||
|
import logger from "../../../main/logger";
|
||||||
|
import { _i18n } from "../../i18n";
|
||||||
import { prevDefault } from "../../utils";
|
import { prevDefault } from "../../utils";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { DropFileInput, Input, InputValidator, InputValidators, SearchInput } from "../input";
|
||||||
|
import { PageLayout } from "../layout/page-layout";
|
||||||
|
import { SubTitle } from "../layout/sub-title";
|
||||||
|
import { Notifications } from "../notifications";
|
||||||
import { TooltipPosition } from "../tooltip";
|
import { TooltipPosition } from "../tooltip";
|
||||||
|
import "./extensions.scss";
|
||||||
|
|
||||||
interface InstallRequest {
|
interface InstallRequest {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
@ -112,9 +112,9 @@ export class Extensions extends React.Component {
|
|||||||
else if (InputValidators.isPath.validate(installPath)) {
|
else if (InputValidators.isPath.validate(installPath)) {
|
||||||
this.requestInstall({ fileName, filePath: installPath });
|
this.requestInstall({ fileName, filePath: installPath });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
Notifications.error(
|
Notifications.error(
|
||||||
<p>Installation has failed: <b>{String(err)}</b></p>
|
<p>Installation has failed: <b>{String(error)}</b></p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -131,6 +131,7 @@ export class Extensions extends React.Component {
|
|||||||
|
|
||||||
async preloadExtensions(requests: InstallRequest[], { showError = true } = {}) {
|
async preloadExtensions(requests: InstallRequest[], { showError = true } = {}) {
|
||||||
const preloadedRequests = requests.filter(req => req.data);
|
const preloadedRequests = requests.filter(req => req.data);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
requests
|
requests
|
||||||
.filter(req => !req.data && req.filePath)
|
.filter(req => !req.data && req.filePath)
|
||||||
@ -138,13 +139,14 @@ export class Extensions extends React.Component {
|
|||||||
return fse.readFile(req.filePath).then(data => {
|
return fse.readFile(req.filePath).then(data => {
|
||||||
req.data = data;
|
req.data = data;
|
||||||
preloadedRequests.push(req);
|
preloadedRequests.push(req);
|
||||||
}).catch(err => {
|
}).catch(error => {
|
||||||
if (showError) {
|
if (showError) {
|
||||||
Notifications.error(`Error while reading "${req.filePath}": ${String(err)}`);
|
Notifications.error(`Error while reading "${req.filePath}": ${String(error)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return preloadedRequests as InstallRequestPreloaded[];
|
return preloadedRequests as InstallRequestPreloaded[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,13 +193,13 @@ export class Extensions extends React.Component {
|
|||||||
manifest,
|
manifest,
|
||||||
tempFile,
|
tempFile,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
fse.unlink(tempFile).catch(() => null); // remove invalid temp package
|
fse.unlink(tempFile).catch(() => null); // remove invalid temp package
|
||||||
if (showErrors) {
|
if (showErrors) {
|
||||||
Notifications.error(
|
Notifications.error(
|
||||||
<div className="flex column gaps">
|
<div className="flex column gaps">
|
||||||
<p>Installing <em>{req.fileName}</em> has failed, skipping.</p>
|
<p>Installing <em>{req.fileName}</em> has failed, skipping.</p>
|
||||||
<p>Reason: <em>{String(err)}</em></p>
|
<p>Reason: <em>{String(error)}</em></p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -241,7 +243,7 @@ export class Extensions extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async unpackExtension({ fileName, tempFile, manifest: { name, version } }: InstallRequestValidated) {
|
async unpackExtension({ fileName, tempFile, manifest: { name, version } }: InstallRequestValidated) {
|
||||||
const extName = `${name}@${version}`;
|
const extName = extensionDisplayName(name, version);
|
||||||
logger.info(`Unpacking extension ${extName}`, { fileName, tempFile });
|
logger.info(`Unpacking extension ${extName}`, { fileName, tempFile });
|
||||||
const unpackingTempFolder = path.join(path.dirname(tempFile), path.basename(tempFile) + "-unpacked");
|
const unpackingTempFolder = path.join(path.dirname(tempFile), path.basename(tempFile) + "-unpacked");
|
||||||
const extensionFolder = this.getExtensionDestFolder(name);
|
const extensionFolder = this.getExtensionDestFolder(name);
|
||||||
@ -264,9 +266,9 @@ export class Extensions extends React.Component {
|
|||||||
Notifications.ok(
|
Notifications.ok(
|
||||||
<p>Extension <b>{extName}</b> successfully installed!</p>
|
<p>Extension <b>{extName}</b> successfully installed!</p>
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
Notifications.error(
|
Notifications.error(
|
||||||
<p>Installing extension <b>{extName}</b> has failed: <em>{err}</em></p>
|
<p>Installing extension <b>{extName}</b> has failed: <em>{error}</em></p>
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
// clean up
|
// clean up
|
||||||
@ -275,6 +277,18 @@ export class Extensions extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uninstallExtension(extension: InstalledExtension) {
|
||||||
|
const extensionName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await extensionDiscovery.uninstallExtension(extension.absolutePath);
|
||||||
|
} catch (error) {
|
||||||
|
Notifications.error(
|
||||||
|
<p>Uninstalling extension <b>{extensionName}</b> has failed: <em>{error?.message ?? ""}</em></p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderExtensions() {
|
renderExtensions() {
|
||||||
const { extensions, extensionsPath, search } = this;
|
const { extensions, extensionsPath, search } = this;
|
||||||
|
|
||||||
@ -293,6 +307,7 @@ export class Extensions extends React.Component {
|
|||||||
return extensions.map(ext => {
|
return extensions.map(ext => {
|
||||||
const { manifestPath: extId, isEnabled, manifest } = ext;
|
const { manifestPath: extId, isEnabled, manifest } = ext;
|
||||||
const { name, description } = manifest;
|
const { name, description } = manifest;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={extId} className="extension flex gaps align-center">
|
<div key={extId} className="extension flex gaps align-center">
|
||||||
<div className="box grow">
|
<div className="box grow">
|
||||||
@ -303,12 +318,17 @@ export class Extensions extends React.Component {
|
|||||||
Description: <span className="text-secondary">{description}</span>
|
Description: <span className="text-secondary">{description}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isEnabled && (
|
<div className="actions">
|
||||||
<Button plain active onClick={() => ext.isEnabled = true}>Enable</Button>
|
{!isEnabled && (
|
||||||
)}
|
<Button plain active onClick={() => ext.isEnabled = true}>Enable</Button>
|
||||||
{isEnabled && (
|
)}
|
||||||
<Button accent onClick={() => ext.isEnabled = false}>Disable</Button>
|
{isEnabled && (
|
||||||
)}
|
<Button accent onClick={() => ext.isEnabled = false}>Disable</Button>
|
||||||
|
)}
|
||||||
|
<Button plain active onClick={() => {
|
||||||
|
this.uninstallExtension(ext);
|
||||||
|
}}>Uninstall</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user