mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Allow to auto-update extensions
Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
parent
d8dbe51e7a
commit
e5d9de3206
@ -51,6 +51,10 @@ export interface InstalledExtension {
|
|||||||
readonly isBundled: boolean; // defined in project root's package.json
|
readonly isBundled: boolean; // defined in project root's package.json
|
||||||
readonly isCompatible: boolean;
|
readonly isCompatible: boolean;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
|
availableUpdate?: {
|
||||||
|
version: string;
|
||||||
|
input: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logModule = "[EXTENSION-DISCOVERY]";
|
const logModule = "[EXTENSION-DISCOVERY]";
|
||||||
@ -371,6 +375,7 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
isBundled,
|
isBundled,
|
||||||
isEnabled,
|
isEnabled,
|
||||||
isCompatible,
|
isCompatible,
|
||||||
|
availableUpdate: null,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === "ENOTDIR") {
|
if (error.code === "ENOTDIR") {
|
||||||
|
|||||||
@ -21,12 +21,13 @@
|
|||||||
|
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
import _ from "lodash";
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { AppPaths } from "../../common/app-paths";
|
import { AppPaths } from "../../common/app-paths";
|
||||||
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc";
|
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc";
|
||||||
import { Disposer, toJS } from "../../common/utils";
|
import { Disposer, downloadJson, toJS } from "../../common/utils";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import type { KubernetesCluster } from "../common-api/catalog";
|
import type { KubernetesCluster } from "../common-api/catalog";
|
||||||
import type { InstalledExtension } from "../extension-discovery";
|
import type { InstalledExtension } from "../extension-discovery";
|
||||||
@ -34,6 +35,8 @@ import { ExtensionsStore } from "../extensions-store";
|
|||||||
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
|
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
|
||||||
import type { LensRendererExtension } from "../lens-renderer-extension";
|
import type { LensRendererExtension } from "../lens-renderer-extension";
|
||||||
import * as registries from "../registries";
|
import * as registries from "../registries";
|
||||||
|
import { SemVer } from "semver";
|
||||||
|
import URLParse from "url-parse";
|
||||||
|
|
||||||
export function extensionPackagesRoot() {
|
export function extensionPackagesRoot() {
|
||||||
return path.join(AppPaths.get("userData"));
|
return path.join(AppPaths.get("userData"));
|
||||||
@ -219,7 +222,6 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId);
|
const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId);
|
||||||
|
|
||||||
// Remove deleted extensions in renderer side only
|
|
||||||
this.extensions.forEach((_, lensExtensionId) => {
|
this.extensions.forEach((_, lensExtensionId) => {
|
||||||
if (!receivedExtensionIds.includes(lensExtensionId)) {
|
if (!receivedExtensionIds.includes(lensExtensionId)) {
|
||||||
this.removeExtension(lensExtensionId);
|
this.removeExtension(lensExtensionId);
|
||||||
@ -249,6 +251,79 @@ export class ExtensionLoader {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAvailableExtensionUpdates(): Promise<{ name: string; version: string }[]> {
|
||||||
|
const availableUpdates: { name: string; version: string }[] = [];
|
||||||
|
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||||
|
for (const [_, extension] of this.extensions) {
|
||||||
|
console.log(`Check for update: ${extension.manifest.name}`);
|
||||||
|
|
||||||
|
const availableUpdate = await this.getLatestVersionFromNpmJs(extension) || await this.getLatestVersionFromGithub(extension);
|
||||||
|
|
||||||
|
if (availableUpdate) {
|
||||||
|
if (new SemVer(extension.manifest.version, { loose: true, includePrerelease: true }).compare(availableUpdate.version) === -1) {
|
||||||
|
extension.availableUpdate = {
|
||||||
|
version: availableUpdate.version,
|
||||||
|
input: availableUpdate.updateInput,
|
||||||
|
};
|
||||||
|
availableUpdates.push({ name: extension.manifest.name, version: availableUpdate.version });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableUpdates;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getLatestVersionFromNpmJs(extension: InstalledExtension) {
|
||||||
|
const name = extension.manifest.name;
|
||||||
|
const registryUrl = new URLParse("https://registry.npmjs.com").set("pathname", name).toString();
|
||||||
|
const { promise } = downloadJson({ url: registryUrl });
|
||||||
|
const json = await promise.catch(() => {
|
||||||
|
// do nothing
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!json || json.error || typeof json.versions !== "object" || !json.versions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = Object.keys(json.versions)
|
||||||
|
.map(version => new SemVer(version, { loose: true, includePrerelease: true }))
|
||||||
|
// ignore pre-releases for auto picking the version
|
||||||
|
.filter(version => version.prerelease.length === 0);
|
||||||
|
|
||||||
|
const version = _.reduce(versions, (prev, curr) => (
|
||||||
|
prev.compareMain(curr) === -1
|
||||||
|
? curr
|
||||||
|
: prev
|
||||||
|
)).format();
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateInput: name,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getLatestVersionFromGithub(extension: InstalledExtension) {
|
||||||
|
|
||||||
|
const repo = extension.manifest.homepage?.replace("https://github.com/", "");
|
||||||
|
|
||||||
|
const registryUrl = `https://api.github.com/repos/${repo}/releases/latest`;
|
||||||
|
|
||||||
|
const { promise } = downloadJson({ url: registryUrl });
|
||||||
|
const json = await promise.catch(() => {
|
||||||
|
// do nothing
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!json || json.error || json.prerelease || !json.tag_name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateInput: json.assets[0].browser_download_url,
|
||||||
|
version: new SemVer(json.tag_name).version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
loadOnMain() {
|
loadOnMain() {
|
||||||
this.autoInitExtensions(() => Promise.resolve([]));
|
this.autoInitExtensions(() => Promise.resolve([]));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -281,6 +281,7 @@ app.on("ready", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
extensionLoader.initExtensions(extensions);
|
extensionLoader.initExtensions(extensions);
|
||||||
|
extensionLoader.getAvailableExtensionUpdates();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
|
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import type {
|
|||||||
LensExtensionManifest,
|
LensExtensionManifest,
|
||||||
} from "../../../../../extensions/lens-extension";
|
} from "../../../../../extensions/lens-extension";
|
||||||
import type { InstallRequest } from "../install-request";
|
import type { InstallRequest } from "../install-request";
|
||||||
|
import { isCompatibleExtension } from "../../../../../extensions/extension-compatibility";
|
||||||
|
|
||||||
export interface InstallRequestValidated {
|
export interface InstallRequestValidated {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
@ -60,6 +61,11 @@ export async function createTempFilesAndValidate({
|
|||||||
|
|
||||||
await fse.writeFile(tempFile, data);
|
await fse.writeFile(tempFile, data);
|
||||||
const manifest = await validatePackage(tempFile);
|
const manifest = await validatePackage(tempFile);
|
||||||
|
|
||||||
|
if (!isCompatibleExtension(manifest)){
|
||||||
|
throw new Error("Incompatible extension");
|
||||||
|
}
|
||||||
|
|
||||||
const id = path.join(
|
const id = path.join(
|
||||||
ExtensionDiscovery.getInstance().nodeModulesPath,
|
ExtensionDiscovery.getInstance().nodeModulesPath,
|
||||||
manifest.name,
|
manifest.name,
|
||||||
|
|||||||
@ -112,6 +112,7 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
|
|||||||
enable={this.props.enableExtension}
|
enable={this.props.enableExtension}
|
||||||
disable={this.props.disableExtension}
|
disable={this.props.disableExtension}
|
||||||
uninstall={this.props.confirmUninstallExtension}
|
uninstall={this.props.confirmUninstallExtension}
|
||||||
|
upgrade={this.props.installFromInput}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</SettingLayout>
|
</SettingLayout>
|
||||||
|
|||||||
@ -37,6 +37,7 @@ interface Props {
|
|||||||
enable: (id: LensExtensionId) => void;
|
enable: (id: LensExtensionId) => void;
|
||||||
disable: (id: LensExtensionId) => void;
|
disable: (id: LensExtensionId) => void;
|
||||||
uninstall: (extension: InstalledExtension) => void;
|
uninstall: (extension: InstalledExtension) => void;
|
||||||
|
upgrade: (input: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatus(extension: InstalledExtension) {
|
function getStatus(extension: InstalledExtension) {
|
||||||
@ -47,7 +48,7 @@ function getStatus(extension: InstalledExtension) {
|
|||||||
return extension.isEnabled ? "Enabled" : "Disabled";
|
return extension.isEnabled ? "Enabled" : "Disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable }: Props) => {
|
export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable, upgrade }: Props) => {
|
||||||
const filters = [
|
const filters = [
|
||||||
(extension: InstalledExtension) => extension.manifest.name,
|
(extension: InstalledExtension) => extension.manifest.name,
|
||||||
(extension: InstalledExtension) => getStatus(extension),
|
(extension: InstalledExtension) => getStatus(extension),
|
||||||
@ -91,7 +92,7 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
|
|||||||
const data = useMemo(
|
const data = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return extensions.map(extension => {
|
return extensions.map(extension => {
|
||||||
const { id, isEnabled, isCompatible, manifest } = extension;
|
const { id, isEnabled, isCompatible, manifest, availableUpdate } = extension;
|
||||||
const { name, description, version } = manifest;
|
const { name, description, version } = manifest;
|
||||||
const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id);
|
const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id);
|
||||||
|
|
||||||
@ -104,7 +105,15 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
version,
|
version: (
|
||||||
|
<div>
|
||||||
|
{version}
|
||||||
|
{ availableUpdate ?(
|
||||||
|
<Icon small material="autorenew" title="Update available"/>
|
||||||
|
) : ""
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
status: (
|
status: (
|
||||||
<div className={cssNames({ [styles.enabled]: isEnabled, [styles.invalid]: !isCompatible })}>
|
<div className={cssNames({ [styles.enabled]: isEnabled, [styles.invalid]: !isCompatible })}>
|
||||||
{getStatus(extension)}
|
{getStatus(extension)}
|
||||||
@ -134,6 +143,16 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{ availableUpdate && (
|
||||||
|
<MenuItem
|
||||||
|
disabled={isUninstalling}
|
||||||
|
onClick={() => upgrade(availableUpdate.input)}
|
||||||
|
>
|
||||||
|
<Icon material="upgrade"/>
|
||||||
|
<span className="title" aria-disabled={isUninstalling}>Upgrade</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={isUninstalling}
|
disabled={isUninstalling}
|
||||||
onClick={() => uninstall(extension)}
|
onClick={() => uninstall(extension)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user