diff --git a/src/renderer/components/+extensions/extensions.scss b/src/renderer/components/+extensions/extensions.scss
index 63778d37e9..f69470fc73 100644
--- a/src/renderer/components/+extensions/extensions.scss
+++ b/src/renderer/components/+extensions/extensions.scss
@@ -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;
}
diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx
index 875861a8dd..14a22f7061 100644
--- a/src/renderer/components/+extensions/extensions.tsx
+++ b/src/renderer/components/+extensions/extensions.tsx
@@ -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 (
-
+
Lens Extension API
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.
- Extensions loaded from:
+
Extensions installed and loaded from
{this.extensionsPath}
-
- Check out documentation to
learn more
+
+
Install extensions from local file-system or URL:
+
+ this.downloadUrl = v}
+ ref={e => this.input = e}
+ />
+ 0}
+ onClick={this.installFromUrl}
+ />
+
+
+
+ Pro-Tip 1: you can download extension archive.tgz via npm by {" "}
+ npm pack %name
+ npm view %name dist.tarball {" "}
+ (click to copy)
+
+
+ Pro-Tip 2: you also can drop archive from file-system to this window to install
+
+
+
);
@@ -104,7 +172,7 @@ export class Extensions extends React.Component {
value={this.search}
onChange={(value) => this.search = value}
/>
-
+
{this.renderExtensions()}
diff --git a/src/renderer/components/copy-to-click/copy-to-click.tsx b/src/renderer/components/copy-to-click/copy-to-click.tsx
new file mode 100644
index 0000000000..ba11941e9f
--- /dev/null
+++ b/src/renderer/components/copy-to-click/copy-to-click.tsx
@@ -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
= {
+ getNotificationMessage(copiedText: string) {
+ return Copied to clipboard: {copiedText}
+ }
+}
+
+export class CopyToClick extends React.Component {
+ static defaultProps = defaultProps as object;
+
+ get rootElem(): HTMLElement {
+ return findDOMNode(this) as HTMLElement;
+ }
+
+ get rootReactElem(): React.ReactElement> {
+ return React.Children.only(this.props.children) as React.ReactElement;
+ }
+
+ @autobind()
+ onClick(evt: React.MouseEvent) {
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/components/copy-to-click/index.ts b/src/renderer/components/copy-to-click/index.ts
new file mode 100644
index 0000000000..c927dba518
--- /dev/null
+++ b/src/renderer/components/copy-to-click/index.ts
@@ -0,0 +1 @@
+export * from "./copy-to-click"
diff --git a/src/renderer/components/icon/icon.tsx b/src/renderer/components/icon/icon.tsx
index 8e99eb0095..24ff28b7c1 100644
--- a/src/renderer/components/icon/icon.tsx
+++ b/src/renderer/components/icon/icon.tsx
@@ -32,7 +32,7 @@ export class Icon extends React.PureComponent {
get isInteractive() {
const { interactive, onClick, href, link } = this.props;
- return interactive || !!(onClick || href || link);
+ return interactive ?? !!(onClick || href || link);
}
@autobind()
diff --git a/src/renderer/components/input/input.scss b/src/renderer/components/input/input.scss
index 31f3c9c46c..39e38a0e10 100644
--- a/src/renderer/components/input/input.scss
+++ b/src/renderer/components/input/input.scss
@@ -107,3 +107,9 @@
}
}
}
+
+.Tooltip.InputTooltipError {
+ --bgc: #{$colorError};
+ --color: white;
+ --border: 1px solid currentColor;
+}
diff --git a/src/renderer/components/input/input.tsx b/src/renderer/components/input/input.tsx
index e61ecc3020..6b4667af83 100644
--- a/src/renderer/components/input/input.tsx
+++ b/src/renderer/components/input/input.tsx
@@ -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 = Omit {
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 {
ref: this.bindRef,
spellCheck: "false",
});
-
+ const tooltipId = showErrorsAsTooltip ? getRandId({ prefix: "input_tooltip_id" }) : undefined;
+ const showErrors = errors.length > 0 && !valid && dirty;
+ const errorsInfo = (
+
+ {errors.map((error, i) =>
{error}
)}
+
+ );
return (
-