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

fixes & refactoring

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-11-24 14:41:39 +02:00
parent 8b6616a591
commit 5fc0c639a1
10 changed files with 169 additions and 149 deletions

View File

@ -6,18 +6,18 @@ pr:
trigger:
branches:
include:
- '*'
- '*'
tags:
include:
- "*"
- "*"
jobs:
- job: Windows
pool:
vmImage: windows-2019
strategy:
matrix:
node_14.x:
node_version: 14.x
node_12.x:
node_version: 12.x
steps:
- powershell: |
$CI_BUILD_TAG = git describe --tags
@ -58,8 +58,8 @@ jobs:
vmImage: macOS-10.14
strategy:
matrix:
node_14.x:
node_version: 14.x
node_12.x:
node_version: 12.x
steps:
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
displayName: Set the tag name as an environment variable
@ -104,8 +104,8 @@ jobs:
vmImage: ubuntu-16.04
strategy:
matrix:
node_14.x:
node_version: 14.x
node_12.x:
node_version: 12.x
steps:
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
displayName: Set the tag name as an environment variable

View File

@ -47,7 +47,7 @@
"bundledHelmVersion": "3.3.4"
},
"engines": {
"node": ">=12 <=14"
"node": ">=12 <13"
},
"lingui": {
"locales": [

View File

@ -11,24 +11,23 @@ export interface DownloadFileTicket {
cancel(): void;
}
export function downloadFile(opts: DownloadFileOptions): DownloadFileTicket {
const { url, gzip = true } = opts;
export function downloadFile({ url, gzip = true }: DownloadFileOptions): DownloadFileTicket {
const fileChunks: Buffer[] = [];
const req = request(url, { gzip });
const promise: Promise<Buffer> = new Promise((resolve, reject) => {
req.on("data", (chunk: Buffer) => {
fileChunks.push(chunk);
});
req.on("complete", () => {
resolve(Buffer.concat(fileChunks));
});
req.on("error", err => {
req.once("error", err => {
reject({ url, err });
});
req.once("complete", () => {
resolve(Buffer.concat(fileChunks));
});
});
return {
url: url,
promise: promise,
url,
promise,
cancel() {
req.abort();
}

View File

@ -25,10 +25,10 @@ export function readFileFromTar(tarFilePath: string, opts: ReadFileFromTarOpts):
entry.on("data", chunk => {
fileChunks.push(chunk);
});
entry.on("error", err => {
entry.once("error", err => {
reject(`Reading ${entry.path} error: ${err}`);
});
entry.on("end", () => {
entry.once("end", () => {
resolve(Buffer.concat(fileChunks));
});
},

View File

@ -41,3 +41,5 @@ export const apiKubePrefix = "/api-kube"; // k8s cluster apis
// Links
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues";
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI";
export const docsUrl = "https://docs.k8slens.dev/";
export const supportUrl = "https://docs.k8slens.dev/latest/support/";

View File

@ -97,15 +97,6 @@ export class ExtensionManager {
}
}
getNpmPackageTarballUrl(packageName: string) {
try {
const command = [this.npmPath, "view", packageName, "dist.tarball", "--silent"];
return child_process.execSync(command.join(" "), { encoding: "utf8" }).trim();
} catch (err) {
return null;
}
}
protected installPackages(): Promise<void> {
return new Promise((resolve, reject) => {
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {

View File

@ -1,7 +1,7 @@
import { app, BrowserWindow, dialog, ipcMain, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
import { autorun } from "mobx";
import { WindowManager } from "./window-manager";
import { appName, isMac, isWindows, isTestEnv } from "../common/vars";
import { appName, isMac, isWindows, isTestEnv, docsUrl, supportUrl } from "../common/vars";
import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route";
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
@ -24,6 +24,7 @@ export function showAbout(browserWindow: BrowserWindow) {
`${appName}: ${app.getVersion()}`,
`Electron: ${process.versions.electron}`,
`Chrome: ${process.versions.chrome}`,
`Node: ${process.versions.node}`,
`Copyright 2020 Mirantis, Inc.`,
];
dialog.showMessageBoxSync(browserWindow, {
@ -215,13 +216,13 @@ export function buildMenu(windowManager: WindowManager) {
{
label: "Documentation",
click: async () => {
shell.openExternal('https://docs.k8slens.dev/');
shell.openExternal(docsUrl);
},
},
{
label: "Support",
click: async () => {
shell.openExternal('https://docs.k8slens.dev/latest/support/');
shell.openExternal(supportUrl);
},
},
...ignoreOnMac([

View File

@ -22,16 +22,6 @@
> .flex.gaps {
--flex-gap: #{$padding};
}
.install-extension {
.Clipboard {
font-size: $font-size-small;
&:hover {
color: $textColorSecondary;
}
}
}
}
.extensions-path {
@ -43,10 +33,24 @@
}
}
.Clipboard {
display: inline;
vertical-align: baseline;
font-size: $font-size-small;
&:hover {
color: $textColorSecondary;
}
}
.SearchInput {
--spacing: #{$padding};
}
.SubTitle {
font-style: italic;
}
.WizardLayout {
padding: 0;

View File

@ -12,6 +12,7 @@ import { Button } from "../button";
import { WizardLayout } from "../layout/wizard-layout";
import { DropFileInput, Input, InputValidators, SearchInput } from "../input";
import { Icon } from "../icon";
import { SubTitle } from "../layout/sub-title";
import { PageLayout } from "../layout/page-layout";
import { Clipboard } from "../clipboard";
import logger from "../../../main/logger";
@ -21,6 +22,7 @@ import { LensExtensionManifest, sanitizeExtensionName } from "../../../extension
import { Notifications } from "../notifications";
import { downloadFile } from "../../../common/utils";
import { extractTar, readFileFromTar } from "../../../common/utils/tar";
import { docsUrl } from "../../../common/vars";
interface InstallRequest {
fileName: string;
@ -28,7 +30,11 @@ interface InstallRequest {
data?: Buffer;
}
interface InstallRequestValidated extends InstallRequest {
interface InstallRequestPreloaded extends InstallRequest {
data: Buffer;
}
interface InstallRequestValidated extends InstallRequestPreloaded {
manifest: LensExtensionManifest;
tmpFile: string; // temp file for unpacking
}
@ -54,6 +60,10 @@ export class Extensions extends React.Component {
return extensionManager.localFolderPath;
}
getExtensionPackageTemp(fileName = "") {
return path.join(os.tmpdir(), "lens-extensions", fileName);
}
getExtensionDestFolder(name: string) {
return path.join(this.extensionsPath, sanitizeExtensionName(name));
}
@ -79,24 +89,16 @@ export class Extensions extends React.Component {
}
};
installExtensions = () => {
if (this.downloadUrl) {
this.installFromNpmOrUrl(this.downloadUrl);
this.downloadUrl = "";
addExtensions = () => {
const { downloadUrl } = this;
if (downloadUrl && InputValidators.isUrl.validate(downloadUrl)) {
this.installFromUrl(downloadUrl);
} else {
this.installFromSelectFileDialog();
}
};
installFromNpmOrUrl = async (url = this.downloadUrl) => {
if (!InputValidators.isUrl.validate(url)) {
const npmPackageName = url;
url = extensionManager.getNpmPackageTarballUrl(npmPackageName);
if (!url) {
Notifications.error(`Error: npm package "${npmPackageName}" not found!`);
return;
}
}
installFromUrl = async (url: string) => {
try {
const { promise: filePromise } = downloadFile({ url });
this.requestInstall([{
@ -105,10 +107,7 @@ export class Extensions extends React.Component {
}]);
} catch (err) {
Notifications.error(
<div className="flex column gaps">
<p>Installation from URL has failed: <b>{String(err)}</b></p>
<p>URL: <em>{url}</em></p>
</div>
<p>Installation via URL has failed: <b>{String(err)}</b></p>
);
}
};
@ -123,89 +122,110 @@ export class Extensions extends React.Component {
);
};
async requestInstall(installRequests: InstallRequest[]) {
const pendingFiles: Promise<any>[] = [];
async preloadExtensions(requests: InstallRequest[], { showError = true } = {}) {
const preloadedRequests = requests.filter(req => req.data);
await Promise.all(
requests
.filter(req => !req.data && req.filePath)
.map(req => {
return fse.readFile(req.filePath).then(data => {
req.data = data;
preloadedRequests.push(req);
}).catch(err => {
if (showError) {
Notifications.error(`Error while reading "${req.filePath}": ${String(err)}`);
}
});
})
);
return preloadedRequests as InstallRequestPreloaded[];
}
// read extensions with provided system path if any
installRequests.forEach(ext => {
if (ext.data) return;
const promise = fse.readFile(ext.filePath)
.then(data => ext.data = data)
.catch(err => {
Notifications.error(`Error while reading "${ext.filePath}": ${String(err)}`);
});
pendingFiles.push(promise);
async validatePackage(filePath: string): Promise<LensExtensionManifest> {
const packageJson: Buffer = await readFileFromTar(filePath, {
// tarball from npm contains single root folder "package/*"
fileMatcher: (path: string) => !!path.match(/(\w+\/)?package\.json$/),
notFoundMessage: "Invalid extension, package.json not found",
});
await Promise.all(pendingFiles);
installRequests = installRequests.filter(item => item.data); // remove items with reading errors
const manifest: LensExtensionManifest = JSON.parse(packageJson.toString("utf8"));
if (!manifest.lens && !manifest.renderer) {
throw `package.json must specify "main" and/or "renderer" fields`;
}
return manifest;
}
// prepare temp folder
const tempFolder = path.join(os.tmpdir(), "lens-extensions");
await fse.ensureDir(tempFolder);
async createTempFilesAndValidate(requests: InstallRequestPreloaded[], { showErrors = true } = {}) {
const validatedRequests: InstallRequestValidated[] = [];
// copy files to temp, get extension info from package.json and do basic validation
const validatedInstalls: Promise<InstallRequestValidated>[] = installRequests.map(async installReq => {
const { fileName, data } = installReq;
const tempFile = path.join(tempFolder, fileName);
await fse.writeFileSync(tempFile, data); // copy to temp
try {
const packageJson: Buffer = await readFileFromTar(tempFile, {
// tarball from npm contains single root folder "package/*"
fileMatcher: (path: string) => !!path.match(/(\w+\/)?package\.json$/),
notFoundMessage: "Extension's manifest file (package.json) not found",
});
const manifest: LensExtensionManifest = JSON.parse(packageJson.toString("utf8"));
if (!manifest.lens && !manifest.renderer) {
throw `package.json must specify "main" and/or "renderer" fields`;
// copy files to temp
await fse.ensureDir(this.getExtensionPackageTemp());
requests.forEach(req => {
const tempFile = this.getExtensionPackageTemp(req.fileName);
fse.writeFileSync(tempFile, req.data);
});
// validate packages
await Promise.all(
requests.map(async req => {
const tempFile = this.getExtensionPackageTemp(req.fileName);
try {
const manifest = await this.validatePackage(tempFile);
validatedRequests.push({
...req,
manifest: manifest,
tmpFile: tempFile,
});
} catch (err) {
fse.unlink(tempFile).catch(() => null); // remove invalid temp package
if (showErrors) {
Notifications.error(
<div className="flex column gaps">
<p>Installing <em>{req.fileName}</em> has failed, skipping.</p>
<p>Reason: <em>{String(err)}</em></p>
</div>
);
}
}
return {
...installReq,
manifest: manifest,
tmpFile: tempFile,
};
} catch (err) {
fse.unlink(tempFile).catch(() => null); // remove invalid temp file
Notifications.error(
<div className="flex column gaps">
<p>Installing <em>{fileName}</em> has failed, skipping.</p>
<p>Reason: <em>{String(err)}</em></p>
})
);
return validatedRequests;
}
async requestInstall(requests: InstallRequest[]) {
const preloadedRequests = await this.preloadExtensions(requests);
const validatedRequests = await this.createTempFilesAndValidate(preloadedRequests);
validatedRequests.forEach(install => {
const { name, version, description } = install.manifest;
const extensionFolder = this.getExtensionDestFolder(name);
const folderExists = fse.existsSync(extensionFolder);
if (!folderExists) {
// auto-install extension if not yet exists
this.unpackExtension(install);
} else {
// otherwise confirmation required (re-install / update)
const removeNotification = Notifications.info(
<div className="InstallingExtensionNotification flex gaps align-center">
<div className="flex column gaps">
<p>Install extension <b>{name}@{version}</b>?</p>
<p>Description: <em>{description}</em></p>
<div className="remove-folder-warning" onClick={() => shell.openPath(extensionFolder)}>
<b>Warning:</b> <code>{extensionFolder}</code> will be removed before installation.
</div>
</div>
<Button autoFocus label="Install" onClick={() => {
removeNotification();
this.unpackExtension(install);
}}/>
</div>
);
}
});
// final step, provide UI with extension info for reviewing and confirming installation
const extensions = await Promise.all(validatedInstalls);
extensions.forEach(install => {
if (!install) {
return; // skip validating errors if any
}
const { fileName, manifest } = install;
const { name, version, description } = manifest;
const extensionFolder = this.getExtensionDestFolder(name);
const folderExists = fse.existsSync(extensionFolder);
const removeNotification = Notifications.info(
<div className="InstallingExtensionNotification flex gaps align-center">
<div className="flex column gaps">
<p>Install extension <b title={fileName}>{name}@{version}</b>?</p>
<p>Description: <em>{description}</em></p>
{folderExists && (
<div className="remove-folder-warning" onClick={() => shell.openPath(extensionFolder)}>
<b>Warning:</b> <code>{extensionFolder}</code> will be removed before installation.
</div>
)}
</div>
<Button autoFocus label="Install" onClick={() => {
removeNotification();
this.unpackExtension(install);
}}/>
</div>
);
});
}
async unpackExtension({ fileName, tmpFile, manifest: { name, version } }: InstallRequestValidated) {
logger.info(`Unpacking extension ${name} from ${fileName}`);
const extName = `${name}@${version}`;
logger.info(`Unpacking extension ${extName}`, { fileName, tmpFile });
const unpackingTempFolder = path.join(path.dirname(tmpFile), path.basename(tmpFile) + "-unpacked");
const extensionFolder = this.getExtensionDestFolder(name);
try {
@ -218,18 +238,18 @@ export class Extensions extends React.Component {
const unpackedFiles = await fse.readdir(unpackingTempFolder);
let unpackedRootFolder = unpackingTempFolder;
if (unpackedFiles.length === 1) {
// handle case when extension.tgz packed with top root folder,
// check if %extension.tgz was packed with single top folder,
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
}
await fse.ensureDir(extensionFolder);
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
Notifications.ok(
<p>Extension <b>{name}/{version}</b> successfully installed!</p>
<p>Extension <b>{extName}</b> successfully installed!</p>
);
} catch (err) {
Notifications.error(
<p>Installing extension <b>{name}</b> has failed: <em>{err}</em></p>
<p>Installing extension <b>{extName}</b> has failed: <em>{err}</em></p>
);
} finally {
// clean up
@ -247,34 +267,38 @@ export class Extensions extends React.Component {
features of Lens are built as extensions and use the same Extension API.
</div>
<div>
<p><em>Extensions loaded from:</em></p>
<SubTitle title="Extensions loaded from:"/>
<div className="extensions-path flex inline" onClick={() => shell.openPath(this.extensionsPath)}>
<Icon material="folder" tooltip={{ children: "Open folder", preferredPositions: "bottom" }}/>
<code>{this.extensionsPath}</code>
</div>
</div>
<div className="install-extension flex column gaps">
<em>
Install extensions from tarball ({this.supportedFormats.join(", ")}):
</em>
<SubTitle title="Install extensions:"/>
<Input
showErrorsAsTooltip={true}
className="box grow"
theme="round-black"
placeholder="URL or npm-package-name"
iconLeft="link"
placeholder={`URL to packed extension (${this.supportedFormats.join(", ")})`}
validators={InputValidators.isUrl}
value={this.downloadUrl}
onChange={v => this.downloadUrl = v}
onSubmit={this.installExtensions}
onSubmit={this.addExtensions}
/>
<Button
primary
label="Add extensions"
onClick={this.installExtensions}
onClick={this.addExtensions}
/>
<p className="hint">
<Trans><b>Pro-Tip 1</b>: you can download packed extension from NPM via</Trans>
<Trans><b>Pro-Tip 1</b>: you can obtain package tarball from NPM via</Trans>{" "}
<Clipboard showNotification>
<code>npm pack %package-name</code>
<code>npm view %package dist.tarball</code>
</Clipboard>
<span> or download package first with </span>
<Clipboard showNotification>
<code>npm pack %package</code>
</Clipboard>
</p>
<p className="hint">
@ -284,7 +308,7 @@ export class Extensions extends React.Component {
<div className="more-info flex inline gaps align-center">
<Icon material="local_fire_department"/>
<p>
Check out documentation to <a href="https://docs.k8slens.dev/" target="_blank">learn more</a>
Check out documentation to <a href={docsUrl} target="_blank">learn more</a>
</p>
</div>
</div>

View File

@ -88,12 +88,11 @@
//- Themes
&.theme {
&.invalid {
box-shadow: 0 0 0 2px $colorError;
border-radius: $radius;
}
&.round-black {
&.invalid label {
border-color: $colorSoftError !important;
}
label {
background: $mainBackground;
border: 1px solid $borderFaintColor;
@ -115,6 +114,6 @@
.Tooltip.InputTooltipError {
--bgc: #{$colorError};
--border: 1px solid $colorSoftError;
--border: none;
--color: white;
}