diff --git a/src/extensions/extension-manager.ts b/src/extensions/extension-manager.ts index 0e51eeb666..d14176bf6f 100644 --- a/src/extensions/extension-manager.ts +++ b/src/extensions/extension-manager.ts @@ -97,6 +97,11 @@ export class ExtensionManager { } } + getNpmPackageTarballUrl(packageName: string) { + const command = [this.npmPath, "view", packageName, "dist.tarball", "--silent"]; + return child_process.execSync(command.join(" "), { encoding: "utf8" }); + } + protected installPackages(): Promise { return new Promise((resolve, reject) => { const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], { diff --git a/src/renderer/components/+extensions/extensions.scss b/src/renderer/components/+extensions/extensions.scss index aa666f6922..e7535fc8e2 100644 --- a/src/renderer/components/+extensions/extensions.scss +++ b/src/renderer/components/+extensions/extensions.scss @@ -24,8 +24,12 @@ } .install-extension { - .Clipboard:hover code { - color: $textColorSecondary; + .Clipboard { + font-size: $font-size-small; + + &:hover { + color: $textColorSecondary; + } } } } diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index d7e632f287..144734b84c 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -1,5 +1,7 @@ import "./extensions.scss"; import { remote, shell } from "electron"; +import path from "path"; +import fse from "fs-extra"; import React from "react"; import { computed, observable } from "mobx"; import { observer } from "mobx-react"; @@ -13,10 +15,12 @@ import { PageLayout } from "../layout/page-layout"; import { Clipboard } from "../clipboard"; import { extensionLoader } from "../../../extensions/extension-loader"; import { extensionManager } from "../../../extensions/extension-manager"; +import { Notifications } from "../notifications"; +import request from "request"; +import logger from "../../../main/logger"; @observer export class Extensions extends React.Component { - @observable.ref input: Input; @observable search = ""; @observable downloadUrl = ""; @@ -41,34 +45,75 @@ export class Extensions extends React.Component { const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), { defaultPath: app.getPath("downloads"), properties: ["openFile", "multiSelections"], - message: _i18n._(t`Select extensions to install (supported: ${supportedFormats.join(", ")}), `), + message: _i18n._(t`Select extensions to install (supported formats: ${supportedFormats.join(", ")}), `), buttonLabel: _i18n._(t`Use configuration`), filters: [ { name: "tarball", extensions: supportedFormats } ] }); if (!canceled && filePaths.length) { - this.installFromLocalPath(filePaths); + this.installFromSelectFileDialog(filePaths); } } - // todo - installFromUrl = () => { - if (!this.downloadUrl) { - this.input?.focus(); + // fixme: doesn't work + // todo: move to common/utils + async downloadFile(url: string, fileName = path.basename(url)): Promise { + return new Promise((resolve, reject) => { + const downloadingReq = request(url, { gzip: true }); + downloadingReq.on("complete", (res, body: Buffer) => { + resolve(new File([body], fileName)); + }); + downloadingReq.on("error", reject); + }) + } + + installFromUrl = async () => { + const { downloadUrl } = this; + if (!downloadUrl) { return; } - console.log('Install from URL', this.downloadUrl); + let tarballUrl: string; + if (InputValidators.isUrl.validate(downloadUrl)) { + tarballUrl = downloadUrl; + } else { + try { + tarballUrl = extensionManager.getNpmPackageTarballUrl(downloadUrl); + } catch (err) { + Notifications.error(`Error: npm package "${downloadUrl}" not found`); + return; + } + } + logger.info('Install from packed extension URL', { tarballUrl }); + if (tarballUrl) { + try { + const file = await this.downloadFile(tarballUrl); + this.installExtensionFromFile([file]); + } catch (err) { + Notifications.error(`Installing extension from ${tarballUrl} has failed: ${String(err)}`); + } + } } - // todo - installFromLocalPath = (filePaths: string[]) => { - console.log('Install select from dialog', filePaths) + installFromSelectFileDialog = async (filePaths: string[]) => { + logger.info('Install from select dialog', { filePaths }); + const files: File[] = await Promise.all( + filePaths.map(filePath => { + const fileName = path.basename(filePath); + return fse.readFile(filePath).then(buffer => new File([buffer], fileName)); + }) + ); + return this.installExtensionFromFile(files); } - // todo installOnDrop = (files: File[]) => { - console.log('Install from D&D', files); + logger.info('Install from D&D', { files }); + return this.installExtensionFromFile(files); + } + + // todo + async installExtensionFromFile(files: File[]) { + console.log(`Install files:`, files); } renderInfo() { @@ -80,31 +125,29 @@ export class Extensions extends React.Component { features of Lens are built as extensions and use the same Extension API.
-

All custom extensions located in:

+

Extensions loaded from:

shell.openPath(this.extensionsPath)}> {this.extensionsPath}
-

Install extensions from local file-system or URL:

+

Install extensions from archive (tarball.tgz):

- 0} - onClick={this.installFromUrl} - /> this.downloadUrl = v} onSubmit={this.installFromUrl} - ref={e => this.input = e} + /> + 0} + onClick={this.installFromUrl} />