mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Option to install an extension from filesystem/url #1227 -- part 1 (UI)
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
c94c599cdd
commit
6624287626
@ -2,7 +2,7 @@
|
||||
--width: 100%;
|
||||
--max-width: auto;
|
||||
|
||||
.extension-list {
|
||||
.extensions-list {
|
||||
.extension {
|
||||
--flex-gap: $padding / 3;
|
||||
padding: $padding $padding * 2;
|
||||
@ -15,6 +15,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.extensions-info {
|
||||
.flex.gaps {
|
||||
--flex-gap: #{$padding};
|
||||
}
|
||||
.install-extension {
|
||||
code {
|
||||
&:hover {
|
||||
color: $textColorSecondary;
|
||||
}
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.extensions-path {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import "./extensions.scss";
|
||||
import { shell } from "electron";
|
||||
import { remote, shell } from "electron";
|
||||
import React from "react";
|
||||
import { computed, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
@ -7,15 +7,18 @@ import { t, Trans } from "@lingui/macro";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { Button } from "../button";
|
||||
import { WizardLayout } from "../layout/wizard-layout";
|
||||
import { Input } from "../input";
|
||||
import { Input, InputValidators } from "../input";
|
||||
import { Icon } from "../icon";
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
import { CopyToClick } from "../copy-to-click/copy-to-click";
|
||||
import { extensionLoader } from "../../../extensions/extension-loader";
|
||||
import { extensionManager } from "../../../extensions/extension-manager";
|
||||
|
||||
@observer
|
||||
export class Extensions extends React.Component {
|
||||
@observable.ref input: Input;
|
||||
@observable search = "";
|
||||
@observable downloadUrl = "";
|
||||
|
||||
@computed get extensions() {
|
||||
const searchText = this.search.toLowerCase();
|
||||
@ -32,16 +35,43 @@ export class Extensions extends React.Component {
|
||||
return extensionManager.localFolderPath;
|
||||
}
|
||||
|
||||
selectPackedExtensionsDialog = async () => {
|
||||
const { dialog, BrowserWindow, app } = remote;
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
|
||||
defaultPath: app.getPath("downloads"),
|
||||
properties: ["openFile", "multiSelections"],
|
||||
message: _i18n._(t`Select extensions to add to Lens (*.tgz`),
|
||||
buttonLabel: _i18n._(t`Use configuration`),
|
||||
filters: [
|
||||
{ name: "tarball", extensions: [".tgz", ".tar.gz"] }
|
||||
]
|
||||
});
|
||||
if (!canceled && filePaths.length) {
|
||||
this.installFromLocalPath(filePaths);
|
||||
}
|
||||
}
|
||||
|
||||
installFromUrl = () => {
|
||||
if (!this.downloadUrl) {
|
||||
this.input?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
installFromLocalPath = (filePaths: string[]) => {
|
||||
}
|
||||
|
||||
renderInfo() {
|
||||
return (
|
||||
<div className="flex column gaps">
|
||||
<div className="extensions-info flex column gaps">
|
||||
<h2>Lens Extension API</h2>
|
||||
<div>
|
||||
The Extensions API in Lens allows users to customize and enhance the Lens experience by creating their own menus or page content that is extended from the existing pages. Many of the core
|
||||
features of Lens are built as extensions and use the same Extension API.
|
||||
</div>
|
||||
<div>
|
||||
Extensions loaded from:
|
||||
<em>Extensions installed and loaded from</em>
|
||||
<div className="extensions-path flex inline">
|
||||
<code>{this.extensionsPath}</code>
|
||||
<Icon
|
||||
@ -51,8 +81,46 @@ export class Extensions extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Check out documentation to <a href="https://docs.k8slens.dev/" target="_blank">learn more</a>
|
||||
<div className="install-extension flex column gaps">
|
||||
<em>Install extensions from local file-system or URL:</em>
|
||||
<div className="install-extension-by-url flex gaps align-center">
|
||||
<Input
|
||||
showErrorsAsTooltip={true}
|
||||
className="box grow"
|
||||
theme="round-black"
|
||||
placeholder="URL, e.g. https://registry.npmjs.org/%path-to-ext.tgz"
|
||||
validators={InputValidators.isUrl}
|
||||
value={this.downloadUrl} // TODO: in addition we could support npm-package-name (if non-url value)?
|
||||
onChange={v => this.downloadUrl = v}
|
||||
ref={e => this.input = e}
|
||||
/>
|
||||
<Icon
|
||||
material="get_app"
|
||||
tooltip="Download and install"
|
||||
interactive={this.downloadUrl.length > 0}
|
||||
onClick={this.installFromUrl}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
primary
|
||||
label="Select local extensions"
|
||||
onClick={this.selectPackedExtensionsDialog}
|
||||
/>
|
||||
<small className="hint">
|
||||
<Trans>Pro-Tip 1: you can download extension archive.tgz via npm by</Trans>{" "}
|
||||
<CopyToClick showNotification><code>npm pack %name</code></CopyToClick>
|
||||
<CopyToClick showNotification><code>npm view %name dist.tarball</code></CopyToClick>{" "}
|
||||
<em className="text-secondary">(click to copy)</em>
|
||||
</small>
|
||||
<small className="hint">
|
||||
<Trans>Pro-Tip 2: you also can drop archive from file-system to this window to install</Trans>
|
||||
</small>
|
||||
</div>
|
||||
<div className="more-info flex inline gaps">
|
||||
<Icon material="local_fire_department"/>
|
||||
<p>
|
||||
Check out documentation to <a href="https://docs.k8slens.dev/" target="_blank">learn more</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -104,7 +172,7 @@ export class Extensions extends React.Component {
|
||||
value={this.search}
|
||||
onChange={(value) => this.search = value}
|
||||
/>
|
||||
<div className="extension-list">
|
||||
<div className="extensions-list">
|
||||
{this.renderExtensions()}
|
||||
</div>
|
||||
</WizardLayout>
|
||||
|
||||
58
src/renderer/components/copy-to-click/copy-to-click.tsx
Normal file
58
src/renderer/components/copy-to-click/copy-to-click.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React from "react"
|
||||
import { findDOMNode } from "react-dom";
|
||||
import { autobind } from "../../../common/utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { copyToClipboard } from "../../utils/copyToClipboard";
|
||||
import logger from "../../../main/logger";
|
||||
|
||||
export interface CopyToClickProps {
|
||||
resetSelection?: boolean
|
||||
showNotification?: boolean
|
||||
getNotificationMessage?(copiedText: string): React.ReactNode;
|
||||
}
|
||||
|
||||
export const defaultProps: Partial<CopyToClickProps> = {
|
||||
getNotificationMessage(copiedText: string) {
|
||||
return <p>Copied to clipboard: <em className="contrast">{copiedText}</em></p>
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyToClick extends React.Component<CopyToClickProps> {
|
||||
static defaultProps = defaultProps as object;
|
||||
|
||||
get rootElem(): HTMLElement {
|
||||
return findDOMNode(this) as HTMLElement;
|
||||
}
|
||||
|
||||
get rootReactElem(): React.ReactElement<React.DOMAttributes<any>> {
|
||||
return React.Children.only(this.props.children) as React.ReactElement;
|
||||
}
|
||||
|
||||
@autobind()
|
||||
onClick(evt: React.MouseEvent<HTMLElement>) {
|
||||
if (!this.rootElem || !this.rootElem.contains(evt.target as any)) {
|
||||
return;
|
||||
}
|
||||
const { showNotification, resetSelection, getNotificationMessage } = this.props;
|
||||
const { copiedText, copied } = copyToClipboard(this.rootElem, { resetSelection });
|
||||
if (copied && showNotification) {
|
||||
Notifications.ok(getNotificationMessage(copiedText));
|
||||
}
|
||||
if (this.rootReactElem.props.onClick) {
|
||||
this.rootReactElem.props.onClick(evt); // pass event to content element as well when provided
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
try {
|
||||
const rootElem = this.rootReactElem;
|
||||
return React.cloneElement(rootElem, {
|
||||
...(rootElem || {}).props,
|
||||
onClick: this.onClick,
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error(`Invalid usage components/CopyToClick usage. Children must contain root html element.`, { err: String(err) })
|
||||
return this.rootReactElem;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/renderer/components/copy-to-click/index.ts
Normal file
1
src/renderer/components/copy-to-click/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./copy-to-click"
|
||||
@ -32,7 +32,7 @@ export class Icon extends React.PureComponent<IconProps> {
|
||||
|
||||
get isInteractive() {
|
||||
const { interactive, onClick, href, link } = this.props;
|
||||
return interactive || !!(onClick || href || link);
|
||||
return interactive ?? !!(onClick || href || link);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
|
||||
@ -107,3 +107,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Tooltip.InputTooltipError {
|
||||
--bgc: #{$colorError};
|
||||
--color: white;
|
||||
--border: 1px solid currentColor;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import "./input.scss";
|
||||
|
||||
import React, { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react";
|
||||
import { autobind, cssNames, debouncePromise } from "../../utils";
|
||||
import { autobind, cssNames, debouncePromise, getRandId } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import * as Validators from "./input_validators";
|
||||
import { InputValidator } from "./input_validators";
|
||||
@ -9,6 +9,7 @@ import isString from "lodash/isString";
|
||||
import isFunction from "lodash/isFunction";
|
||||
import isBoolean from "lodash/isBoolean";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import { Tooltip } from "../tooltip";
|
||||
|
||||
const { conditionalValidators, ...InputValidators } = Validators;
|
||||
export { InputValidators, InputValidator };
|
||||
@ -25,6 +26,7 @@ export type InputProps<T = string> = Omit<InputElementProps, "onChange" | "onSub
|
||||
maxRows?: number; // when multiLine={true} define max rows size
|
||||
dirty?: boolean; // show validation errors even if the field wasn't touched yet
|
||||
showValidationLine?: boolean; // show animated validation line for async validators
|
||||
showErrorsAsTooltip?: boolean; // show validation errors as a tooltip :hover (instead of block below)
|
||||
iconLeft?: string | React.ReactNode; // material-icon name in case of string-type
|
||||
iconRight?: string | React.ReactNode;
|
||||
contentRight?: string | React.ReactNode; // Any component of string goes after iconRight
|
||||
@ -265,7 +267,7 @@ export class Input extends React.Component<InputProps, State> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
multiLine, showValidationLine, validators, theme, maxRows, children,
|
||||
multiLine, showValidationLine, validators, theme, maxRows, children, showErrorsAsTooltip,
|
||||
maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight,
|
||||
...inputProps
|
||||
} = this.props;
|
||||
@ -292,21 +294,31 @@ export class Input extends React.Component<InputProps, State> {
|
||||
ref: this.bindRef,
|
||||
spellCheck: "false",
|
||||
});
|
||||
|
||||
const tooltipId = showErrorsAsTooltip ? getRandId({ prefix: "input_tooltip_id" }) : undefined;
|
||||
const showErrors = errors.length > 0 && !valid && dirty;
|
||||
const errorsInfo = (
|
||||
<div className="errors box grow">
|
||||
{errors.map((error, i) => <p key={i}>{error}</p>)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={className}>
|
||||
<label className="input-area flex gaps align-center">
|
||||
<div id={tooltipId} className={className}>
|
||||
<label className="input-area flex gaps align-center" id="">
|
||||
{isString(iconLeft) ? <Icon material={iconLeft}/> : iconLeft}
|
||||
{multiLine ? <textarea {...inputProps as any} /> : <input {...inputProps as any} />}
|
||||
{isString(iconRight) ? <Icon material={iconRight} /> : iconRight}
|
||||
{isString(iconRight) ? <Icon material={iconRight}/> : iconRight}
|
||||
{contentRight}
|
||||
</label>
|
||||
<div className="input-info flex gaps">
|
||||
{!valid && dirty && (
|
||||
<div className="errors box grow">
|
||||
{errors.map((error, i) => <p key={i}>{error}</p>)}
|
||||
{showErrorsAsTooltip && showErrors && (
|
||||
<Tooltip targetId={tooltipId} className="InputTooltipError">
|
||||
<div className="flex gaps align-center">
|
||||
<Icon material="error_outline"/>
|
||||
{errorsInfo}
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="input-info flex gaps">
|
||||
{!showErrorsAsTooltip && showErrors && errorsInfo}
|
||||
{this.showMaxLenIndicator && (
|
||||
<div className="maxLengthIndicator box right">
|
||||
{this.getValue().length} / {maxLength}
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
|
||||
.Tooltip {
|
||||
--bgc: #{$mainBackground};
|
||||
--radius: #{$radius};
|
||||
--color: #{$textColorSecondary};
|
||||
--border: 1px solid #{$borderColor};
|
||||
|
||||
// use positioning relative to viewport (window)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/position
|
||||
position: fixed;
|
||||
margin: 0 !important;
|
||||
background: $mainBackground;
|
||||
background: var(--bgc);
|
||||
font-size: small;
|
||||
font-weight: normal;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: $radius;
|
||||
color: $textColorSecondary;
|
||||
border: var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--color);
|
||||
white-space: normal;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
|
||||
@ -1,19 +1,25 @@
|
||||
// Helper for selecting element's text content and copy in clipboard
|
||||
|
||||
export function copyToClipboard(elem: HTMLElement, resetSelection = true): boolean {
|
||||
export function copyToClipboard(elem: HTMLElement, { resetSelection = true } = {}) {
|
||||
let clearSelection: () => void;
|
||||
if (isSelectable(elem)) {
|
||||
elem.select();
|
||||
clearSelection = () => elem.setSelectionRange(0, 0);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
selection.selectAllChildren(elem);
|
||||
clearSelection = () => selection.removeAllRanges();
|
||||
}
|
||||
const copyResult = document.execCommand("copy");
|
||||
if (resetSelection) clearSelection();
|
||||
return copyResult;
|
||||
const selectedText = document.getSelection().toString();
|
||||
const isCopied = document.execCommand("copy");
|
||||
if (resetSelection) {
|
||||
clearSelection();
|
||||
}
|
||||
return {
|
||||
copied: isCopied,
|
||||
copiedText: selectedText,
|
||||
clearSelection,
|
||||
};
|
||||
}
|
||||
|
||||
function isSelectable(elem: HTMLElement): elem is HTMLInputElement {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user