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:
parent
8b6616a591
commit
5fc0c639a1
@ -6,18 +6,18 @@ pr:
|
|||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
include:
|
include:
|
||||||
- '*'
|
- '*'
|
||||||
tags:
|
tags:
|
||||||
include:
|
include:
|
||||||
- "*"
|
- "*"
|
||||||
jobs:
|
jobs:
|
||||||
- job: Windows
|
- job: Windows
|
||||||
pool:
|
pool:
|
||||||
vmImage: windows-2019
|
vmImage: windows-2019
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_14.x:
|
node_12.x:
|
||||||
node_version: 14.x
|
node_version: 12.x
|
||||||
steps:
|
steps:
|
||||||
- powershell: |
|
- powershell: |
|
||||||
$CI_BUILD_TAG = git describe --tags
|
$CI_BUILD_TAG = git describe --tags
|
||||||
@ -58,8 +58,8 @@ jobs:
|
|||||||
vmImage: macOS-10.14
|
vmImage: macOS-10.14
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_14.x:
|
node_12.x:
|
||||||
node_version: 14.x
|
node_version: 12.x
|
||||||
steps:
|
steps:
|
||||||
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
- 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
|
displayName: Set the tag name as an environment variable
|
||||||
@ -104,8 +104,8 @@ jobs:
|
|||||||
vmImage: ubuntu-16.04
|
vmImage: ubuntu-16.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_14.x:
|
node_12.x:
|
||||||
node_version: 14.x
|
node_version: 12.x
|
||||||
steps:
|
steps:
|
||||||
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
- 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
|
displayName: Set the tag name as an environment variable
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
"bundledHelmVersion": "3.3.4"
|
"bundledHelmVersion": "3.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12 <=14"
|
"node": ">=12 <13"
|
||||||
},
|
},
|
||||||
"lingui": {
|
"lingui": {
|
||||||
"locales": [
|
"locales": [
|
||||||
|
|||||||
@ -11,24 +11,23 @@ export interface DownloadFileTicket {
|
|||||||
cancel(): void;
|
cancel(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadFile(opts: DownloadFileOptions): DownloadFileTicket {
|
export function downloadFile({ url, gzip = true }: DownloadFileOptions): DownloadFileTicket {
|
||||||
const { url, gzip = true } = opts;
|
|
||||||
const fileChunks: Buffer[] = [];
|
const fileChunks: Buffer[] = [];
|
||||||
const req = request(url, { gzip });
|
const req = request(url, { gzip });
|
||||||
const promise: Promise<Buffer> = new Promise((resolve, reject) => {
|
const promise: Promise<Buffer> = new Promise((resolve, reject) => {
|
||||||
req.on("data", (chunk: Buffer) => {
|
req.on("data", (chunk: Buffer) => {
|
||||||
fileChunks.push(chunk);
|
fileChunks.push(chunk);
|
||||||
});
|
});
|
||||||
req.on("complete", () => {
|
req.once("error", err => {
|
||||||
resolve(Buffer.concat(fileChunks));
|
|
||||||
});
|
|
||||||
req.on("error", err => {
|
|
||||||
reject({ url, err });
|
reject({ url, err });
|
||||||
});
|
});
|
||||||
|
req.once("complete", () => {
|
||||||
|
resolve(Buffer.concat(fileChunks));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
url: url,
|
url,
|
||||||
promise: promise,
|
promise,
|
||||||
cancel() {
|
cancel() {
|
||||||
req.abort();
|
req.abort();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,10 +25,10 @@ export function readFileFromTar(tarFilePath: string, opts: ReadFileFromTarOpts):
|
|||||||
entry.on("data", chunk => {
|
entry.on("data", chunk => {
|
||||||
fileChunks.push(chunk);
|
fileChunks.push(chunk);
|
||||||
});
|
});
|
||||||
entry.on("error", err => {
|
entry.once("error", err => {
|
||||||
reject(`Reading ${entry.path} error: ${err}`);
|
reject(`Reading ${entry.path} error: ${err}`);
|
||||||
});
|
});
|
||||||
entry.on("end", () => {
|
entry.once("end", () => {
|
||||||
resolve(Buffer.concat(fileChunks));
|
resolve(Buffer.concat(fileChunks));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -41,3 +41,5 @@ export const apiKubePrefix = "/api-kube"; // k8s cluster apis
|
|||||||
// Links
|
// Links
|
||||||
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues";
|
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues";
|
||||||
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI";
|
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/";
|
||||||
|
|||||||
@ -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> {
|
protected installPackages(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
|
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { app, BrowserWindow, dialog, ipcMain, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
|
import { app, BrowserWindow, dialog, ipcMain, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
|
||||||
import { autorun } from "mobx";
|
import { autorun } from "mobx";
|
||||||
import { WindowManager } from "./window-manager";
|
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 { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route";
|
||||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||||
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
||||||
@ -24,6 +24,7 @@ export function showAbout(browserWindow: BrowserWindow) {
|
|||||||
`${appName}: ${app.getVersion()}`,
|
`${appName}: ${app.getVersion()}`,
|
||||||
`Electron: ${process.versions.electron}`,
|
`Electron: ${process.versions.electron}`,
|
||||||
`Chrome: ${process.versions.chrome}`,
|
`Chrome: ${process.versions.chrome}`,
|
||||||
|
`Node: ${process.versions.node}`,
|
||||||
`Copyright 2020 Mirantis, Inc.`,
|
`Copyright 2020 Mirantis, Inc.`,
|
||||||
];
|
];
|
||||||
dialog.showMessageBoxSync(browserWindow, {
|
dialog.showMessageBoxSync(browserWindow, {
|
||||||
@ -215,13 +216,13 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{
|
{
|
||||||
label: "Documentation",
|
label: "Documentation",
|
||||||
click: async () => {
|
click: async () => {
|
||||||
shell.openExternal('https://docs.k8slens.dev/');
|
shell.openExternal(docsUrl);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Support",
|
label: "Support",
|
||||||
click: async () => {
|
click: async () => {
|
||||||
shell.openExternal('https://docs.k8slens.dev/latest/support/');
|
shell.openExternal(supportUrl);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...ignoreOnMac([
|
...ignoreOnMac([
|
||||||
|
|||||||
@ -22,16 +22,6 @@
|
|||||||
> .flex.gaps {
|
> .flex.gaps {
|
||||||
--flex-gap: #{$padding};
|
--flex-gap: #{$padding};
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-extension {
|
|
||||||
.Clipboard {
|
|
||||||
font-size: $font-size-small;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $textColorSecondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.extensions-path {
|
.extensions-path {
|
||||||
@ -43,10 +33,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Clipboard {
|
||||||
|
display: inline;
|
||||||
|
vertical-align: baseline;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $textColorSecondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.SearchInput {
|
.SearchInput {
|
||||||
--spacing: #{$padding};
|
--spacing: #{$padding};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.SubTitle {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.WizardLayout {
|
.WizardLayout {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { Button } from "../button";
|
|||||||
import { WizardLayout } from "../layout/wizard-layout";
|
import { WizardLayout } from "../layout/wizard-layout";
|
||||||
import { DropFileInput, Input, InputValidators, SearchInput } from "../input";
|
import { DropFileInput, Input, InputValidators, SearchInput } from "../input";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
|
import { SubTitle } from "../layout/sub-title";
|
||||||
import { PageLayout } from "../layout/page-layout";
|
import { PageLayout } from "../layout/page-layout";
|
||||||
import { Clipboard } from "../clipboard";
|
import { Clipboard } from "../clipboard";
|
||||||
import logger from "../../../main/logger";
|
import logger from "../../../main/logger";
|
||||||
@ -21,6 +22,7 @@ import { LensExtensionManifest, sanitizeExtensionName } from "../../../extension
|
|||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { downloadFile } from "../../../common/utils";
|
import { downloadFile } from "../../../common/utils";
|
||||||
import { extractTar, readFileFromTar } from "../../../common/utils/tar";
|
import { extractTar, readFileFromTar } from "../../../common/utils/tar";
|
||||||
|
import { docsUrl } from "../../../common/vars";
|
||||||
|
|
||||||
interface InstallRequest {
|
interface InstallRequest {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
@ -28,7 +30,11 @@ interface InstallRequest {
|
|||||||
data?: Buffer;
|
data?: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InstallRequestValidated extends InstallRequest {
|
interface InstallRequestPreloaded extends InstallRequest {
|
||||||
|
data: Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstallRequestValidated extends InstallRequestPreloaded {
|
||||||
manifest: LensExtensionManifest;
|
manifest: LensExtensionManifest;
|
||||||
tmpFile: string; // temp file for unpacking
|
tmpFile: string; // temp file for unpacking
|
||||||
}
|
}
|
||||||
@ -54,6 +60,10 @@ export class Extensions extends React.Component {
|
|||||||
return extensionManager.localFolderPath;
|
return extensionManager.localFolderPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExtensionPackageTemp(fileName = "") {
|
||||||
|
return path.join(os.tmpdir(), "lens-extensions", fileName);
|
||||||
|
}
|
||||||
|
|
||||||
getExtensionDestFolder(name: string) {
|
getExtensionDestFolder(name: string) {
|
||||||
return path.join(this.extensionsPath, sanitizeExtensionName(name));
|
return path.join(this.extensionsPath, sanitizeExtensionName(name));
|
||||||
}
|
}
|
||||||
@ -79,24 +89,16 @@ export class Extensions extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
installExtensions = () => {
|
addExtensions = () => {
|
||||||
if (this.downloadUrl) {
|
const { downloadUrl } = this;
|
||||||
this.installFromNpmOrUrl(this.downloadUrl);
|
if (downloadUrl && InputValidators.isUrl.validate(downloadUrl)) {
|
||||||
this.downloadUrl = "";
|
this.installFromUrl(downloadUrl);
|
||||||
} else {
|
} else {
|
||||||
this.installFromSelectFileDialog();
|
this.installFromSelectFileDialog();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
installFromNpmOrUrl = async (url = this.downloadUrl) => {
|
installFromUrl = async (url: string) => {
|
||||||
if (!InputValidators.isUrl.validate(url)) {
|
|
||||||
const npmPackageName = url;
|
|
||||||
url = extensionManager.getNpmPackageTarballUrl(npmPackageName);
|
|
||||||
if (!url) {
|
|
||||||
Notifications.error(`Error: npm package "${npmPackageName}" not found!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const { promise: filePromise } = downloadFile({ url });
|
const { promise: filePromise } = downloadFile({ url });
|
||||||
this.requestInstall([{
|
this.requestInstall([{
|
||||||
@ -105,10 +107,7 @@ export class Extensions extends React.Component {
|
|||||||
}]);
|
}]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Notifications.error(
|
Notifications.error(
|
||||||
<div className="flex column gaps">
|
<p>Installation via URL has failed: <b>{String(err)}</b></p>
|
||||||
<p>Installation from URL has failed: <b>{String(err)}</b></p>
|
|
||||||
<p>URL: <em>{url}</em></p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -123,89 +122,110 @@ export class Extensions extends React.Component {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
async requestInstall(installRequests: InstallRequest[]) {
|
async preloadExtensions(requests: InstallRequest[], { showError = true } = {}) {
|
||||||
const pendingFiles: Promise<any>[] = [];
|
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
|
async validatePackage(filePath: string): Promise<LensExtensionManifest> {
|
||||||
installRequests.forEach(ext => {
|
const packageJson: Buffer = await readFileFromTar(filePath, {
|
||||||
if (ext.data) return;
|
// tarball from npm contains single root folder "package/*"
|
||||||
const promise = fse.readFile(ext.filePath)
|
fileMatcher: (path: string) => !!path.match(/(\w+\/)?package\.json$/),
|
||||||
.then(data => ext.data = data)
|
notFoundMessage: "Invalid extension, package.json not found",
|
||||||
.catch(err => {
|
|
||||||
Notifications.error(`Error while reading "${ext.filePath}": ${String(err)}`);
|
|
||||||
});
|
|
||||||
pendingFiles.push(promise);
|
|
||||||
});
|
});
|
||||||
await Promise.all(pendingFiles);
|
const manifest: LensExtensionManifest = JSON.parse(packageJson.toString("utf8"));
|
||||||
installRequests = installRequests.filter(item => item.data); // remove items with reading errors
|
if (!manifest.lens && !manifest.renderer) {
|
||||||
|
throw `package.json must specify "main" and/or "renderer" fields`;
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
// prepare temp folder
|
async createTempFilesAndValidate(requests: InstallRequestPreloaded[], { showErrors = true } = {}) {
|
||||||
const tempFolder = path.join(os.tmpdir(), "lens-extensions");
|
const validatedRequests: InstallRequestValidated[] = [];
|
||||||
await fse.ensureDir(tempFolder);
|
|
||||||
|
|
||||||
// copy files to temp, get extension info from package.json and do basic validation
|
// copy files to temp
|
||||||
const validatedInstalls: Promise<InstallRequestValidated>[] = installRequests.map(async installReq => {
|
await fse.ensureDir(this.getExtensionPackageTemp());
|
||||||
const { fileName, data } = installReq;
|
requests.forEach(req => {
|
||||||
const tempFile = path.join(tempFolder, fileName);
|
const tempFile = this.getExtensionPackageTemp(req.fileName);
|
||||||
await fse.writeFileSync(tempFile, data); // copy to temp
|
fse.writeFileSync(tempFile, req.data);
|
||||||
try {
|
});
|
||||||
const packageJson: Buffer = await readFileFromTar(tempFile, {
|
|
||||||
// tarball from npm contains single root folder "package/*"
|
// validate packages
|
||||||
fileMatcher: (path: string) => !!path.match(/(\w+\/)?package\.json$/),
|
await Promise.all(
|
||||||
notFoundMessage: "Extension's manifest file (package.json) not found",
|
requests.map(async req => {
|
||||||
});
|
const tempFile = this.getExtensionPackageTemp(req.fileName);
|
||||||
const manifest: LensExtensionManifest = JSON.parse(packageJson.toString("utf8"));
|
try {
|
||||||
if (!manifest.lens && !manifest.renderer) {
|
const manifest = await this.validatePackage(tempFile);
|
||||||
throw `package.json must specify "main" and/or "renderer" fields`;
|
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,
|
return validatedRequests;
|
||||||
tmpFile: tempFile,
|
}
|
||||||
};
|
|
||||||
} catch (err) {
|
async requestInstall(requests: InstallRequest[]) {
|
||||||
fse.unlink(tempFile).catch(() => null); // remove invalid temp file
|
const preloadedRequests = await this.preloadExtensions(requests);
|
||||||
Notifications.error(
|
const validatedRequests = await this.createTempFilesAndValidate(preloadedRequests);
|
||||||
<div className="flex column gaps">
|
|
||||||
<p>Installing <em>{fileName}</em> has failed, skipping.</p>
|
validatedRequests.forEach(install => {
|
||||||
<p>Reason: <em>{String(err)}</em></p>
|
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>
|
</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) {
|
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 unpackingTempFolder = path.join(path.dirname(tmpFile), path.basename(tmpFile) + "-unpacked");
|
||||||
const extensionFolder = this.getExtensionDestFolder(name);
|
const extensionFolder = this.getExtensionDestFolder(name);
|
||||||
try {
|
try {
|
||||||
@ -218,18 +238,18 @@ export class Extensions extends React.Component {
|
|||||||
const unpackedFiles = await fse.readdir(unpackingTempFolder);
|
const unpackedFiles = await fse.readdir(unpackingTempFolder);
|
||||||
let unpackedRootFolder = unpackingTempFolder;
|
let unpackedRootFolder = unpackingTempFolder;
|
||||||
if (unpackedFiles.length === 1) {
|
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
|
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
|
||||||
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
|
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
|
||||||
}
|
}
|
||||||
await fse.ensureDir(extensionFolder);
|
await fse.ensureDir(extensionFolder);
|
||||||
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||||
Notifications.ok(
|
Notifications.ok(
|
||||||
<p>Extension <b>{name}/{version}</b> successfully installed!</p>
|
<p>Extension <b>{extName}</b> successfully installed!</p>
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Notifications.error(
|
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 {
|
} finally {
|
||||||
// clean up
|
// 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.
|
features of Lens are built as extensions and use the same Extension API.
|
||||||
</div>
|
</div>
|
||||||
<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)}>
|
<div className="extensions-path flex inline" onClick={() => shell.openPath(this.extensionsPath)}>
|
||||||
<Icon material="folder" tooltip={{ children: "Open folder", preferredPositions: "bottom" }}/>
|
<Icon material="folder" tooltip={{ children: "Open folder", preferredPositions: "bottom" }}/>
|
||||||
<code>{this.extensionsPath}</code>
|
<code>{this.extensionsPath}</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="install-extension flex column gaps">
|
<div className="install-extension flex column gaps">
|
||||||
<em>
|
<SubTitle title="Install extensions:"/>
|
||||||
Install extensions from tarball ({this.supportedFormats.join(", ")}):
|
|
||||||
</em>
|
|
||||||
<Input
|
<Input
|
||||||
showErrorsAsTooltip={true}
|
showErrorsAsTooltip={true}
|
||||||
className="box grow"
|
className="box grow"
|
||||||
theme="round-black"
|
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}
|
value={this.downloadUrl}
|
||||||
onChange={v => this.downloadUrl = v}
|
onChange={v => this.downloadUrl = v}
|
||||||
onSubmit={this.installExtensions}
|
onSubmit={this.addExtensions}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
label="Add extensions"
|
label="Add extensions"
|
||||||
onClick={this.installExtensions}
|
onClick={this.addExtensions}
|
||||||
/>
|
/>
|
||||||
<p className="hint">
|
<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>
|
<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>
|
</Clipboard>
|
||||||
</p>
|
</p>
|
||||||
<p className="hint">
|
<p className="hint">
|
||||||
@ -284,7 +308,7 @@ export class Extensions extends React.Component {
|
|||||||
<div className="more-info flex inline gaps align-center">
|
<div className="more-info flex inline gaps align-center">
|
||||||
<Icon material="local_fire_department"/>
|
<Icon material="local_fire_department"/>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -88,12 +88,11 @@
|
|||||||
//- Themes
|
//- Themes
|
||||||
|
|
||||||
&.theme {
|
&.theme {
|
||||||
&.invalid {
|
|
||||||
box-shadow: 0 0 0 2px $colorError;
|
|
||||||
border-radius: $radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.round-black {
|
&.round-black {
|
||||||
|
&.invalid label {
|
||||||
|
border-color: $colorSoftError !important;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
background: $mainBackground;
|
background: $mainBackground;
|
||||||
border: 1px solid $borderFaintColor;
|
border: 1px solid $borderFaintColor;
|
||||||
@ -115,6 +114,6 @@
|
|||||||
|
|
||||||
.Tooltip.InputTooltipError {
|
.Tooltip.InputTooltipError {
|
||||||
--bgc: #{$colorError};
|
--bgc: #{$colorError};
|
||||||
--border: 1px solid $colorSoftError;
|
--border: none;
|
||||||
--color: white;
|
--color: white;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user