Allow to install packed extensions from URL or local file (#1456)

* Option to install an extension from filesystem/url #1227 -- part 1 (UI)

Signed-off-by: Roman <ixrock@gmail.com>

* DropFileInput: common component to handle droped files (replaced also in add-cluster-page)

Signed-off-by: Roman <ixrock@gmail.com>

* fix: install via url-string on input.submit

Signed-off-by: Roman <ixrock@gmail.com>

* ui tweaks & minor fixes

Signed-off-by: Roman <ixrock@gmail.com>

* more ui/ux tweaks & fixes

Signed-off-by: Roman <ixrock@gmail.com>

* layout fixes

Signed-off-by: Roman <ixrock@gmail.com>

* component renaming: `copy-to-click` => `copy-to-clipboard` => `clipboard`

Signed-off-by: Roman <ixrock@gmail.com>

* reworks -- part 1

Signed-off-by: Roman <ixrock@gmail.com>

* fix downloading file, added common/utils/downloadFile

Signed-off-by: Roman <ixrock@gmail.com>

* confirm before install, unpack tar first steps

Signed-off-by: Roman <ixrock@gmail.com>

* installation flow, extracting .tgz

Signed-off-by: Roman <ixrock@gmail.com>

* clean up, fix lint issues

Signed-off-by: Roman <ixrock@gmail.com>

* update .azure-pipelines.yml

Signed-off-by: Roman <ixrock@gmail.com>

* fixes & refactoring

Signed-off-by: Roman <ixrock@gmail.com>

* fix lint harder :/

Signed-off-by: Roman <ixrock@gmail.com>

* fix validation

Signed-off-by: Roman <ixrock@gmail.com>

* fix validation harder

Signed-off-by: Roman <ixrock@gmail.com>

* responding to comments, fixed package validation

Signed-off-by: Roman <ixrock@gmail.com>

* common/utils/tar.ts: reject with Error-type

Signed-off-by: Roman <ixrock@gmail.com>

* fix: unit-tests

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman authored and GitHub committed 2020-11-25 09:55:28 +02:00
1 parent 2da598b66e
commit 77ae31550a
36 files changed
+799 -193

No files matched your search

@@ -0,0 +1,9 @@
.DropFileInput {
&.droppable {
box-shadow: 0 0 0 5px $primary; // fixme: might not work sometimes
> * {
pointer-events: none;
}
}
}
@@ -0,0 +1,76 @@
import "./drop-file-input.scss";
import React from "react";
import { autobind, cssNames, IClassName } from "../../utils";
import { observable } from "mobx";
import { observer } from "mobx-react";
import logger from "../../../main/logger";
export interface DropFileInputProps extends React.DOMAttributes<any> {
className?: IClassName;
disabled?: boolean;
onDropFiles(files: File[], meta: DropFileMeta): void;
}
export interface DropFileMeta<T extends HTMLElement = any> {
evt: React.DragEvent<T>;
}
@observer
export class DropFileInput<T extends HTMLElement = any> extends React.Component<DropFileInputProps> {
@observable dropAreaActive = false;
@autobind()
onDragEnter() {
this.dropAreaActive = true;
}
@autobind()
onDragLeave() {
this.dropAreaActive = false;
}
@autobind()
onDragOver(evt: React.DragEvent<T>) {
if (this.props.onDragOver) {
this.props.onDragOver(evt);
}
evt.preventDefault(); // enable onDrop()-callback
evt.dataTransfer.dropEffect = "move";
}
@autobind()
onDrop(evt: React.DragEvent<T>) {
if (this.props.onDrop) {
this.props.onDrop(evt);
}
this.dropAreaActive = false;
const files = Array.from(evt.dataTransfer.files);
if (files.length > 0) {
this.props.onDropFiles(files, { evt });
}
}
render() {
const { disabled, className } = this.props;
const { onDragEnter, onDragLeave, onDragOver, onDrop } = this;
try {
const contentElem = React.Children.only(this.props.children) as React.ReactElement<React.HTMLProps<HTMLElement>>;
const isValidContentElem = React.isValidElement(contentElem);
if (!disabled && isValidContentElem) {
const contentElemProps: React.HTMLProps<HTMLElement> = {
className: cssNames("DropFileInput", className, {
droppable: this.dropAreaActive,
}),
onDragEnter,
onDragLeave,
onDragOver,
onDrop,
};
return React.cloneElement(contentElem, contentElemProps);
}
} catch (err) {
logger.error("Invalid root content-element for DropFileInput", { err: String(err) });
return this.props.children;
}
}
}
+1
View File
@@ -2,3 +2,4 @@ export * from './input';
export * from './search-input';
export * from './search-input-url';
export * from './file-input';
export * from './drop-file-input';
+10
View File
@@ -89,6 +89,10 @@
&.theme {
&.round-black {
&.invalid label {
border-color: $colorSoftError !important;
}
label {
background: $mainBackground;
border: 1px solid $borderFaintColor;
@@ -107,3 +111,9 @@
}
}
}
.Tooltip.InputTooltipError {
--bgc: #{$colorError};
--border: none;
--color: white;
}
+23 -11
View File
@@ -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,5 +1,6 @@
.Input.SearchInput {
--compact-focus-width: 190px;
--spacing: 6px 6px 6px 10px;
max-width: 900px;
min-width: 220px;
@@ -10,7 +11,7 @@
border: none;
border-radius: $radius;
box-shadow: 0 0 0 1px $halfGray;
padding: 6px 6px 6px 10px;
padding: var(--spacing);
.Icon {
height: $margin * 2;