1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

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

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-11-19 21:05:03 +02:00
parent 6624287626
commit 7d28a43993
8 changed files with 165 additions and 91 deletions

View File

@ -1,12 +1,4 @@
.AddCluster {
.droppable {
box-shadow: 0 0 0 5px inset $primary;
> * {
pointer-events: none;
}
}
.hint {
margin-top: -$padding;
color: $textColorSecondary;

View File

@ -8,7 +8,7 @@ import { KubeConfig } from "@kubernetes/client-node";
import { _i18n } from "../../i18n";
import { t, Trans } from "@lingui/macro";
import { Select, SelectOption } from "../select";
import { Input } from "../input";
import { DropFileInput, Input } from "../input";
import { AceEditor } from "../ace-editor";
import { Button } from "../button";
import { Icon } from "../icon";
@ -43,7 +43,6 @@ export class AddCluster extends React.Component {
@observable proxyServer = "";
@observable isWaiting = false;
@observable showSettings = false;
@observable dropAreaActive = false;
componentDidMount() {
clusterStore.setActive(null);
@ -119,6 +118,11 @@ export class AddCluster extends React.Component {
}
};
onDropKubeConfig = (files: File[]) => {
this.sourceTab = KubeConfigSourceTab.FILE;
this.setKubeConfig(files[0].path);
}
@action
addClusters = () => {
let newClusters: ClusterModel[] = [];
@ -137,7 +141,7 @@ export class AddCluster extends React.Component {
return true;
} catch (err) {
this.error = String(err.message);
if (err instanceof ExecValidationNotFoundError ) {
if (err instanceof ExecValidationNotFoundError) {
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
return false;
} else {
@ -228,7 +232,7 @@ export class AddCluster extends React.Component {
<Tab
value={KubeConfigSourceTab.FILE}
label={<Trans>Select kubeconfig file</Trans>}
active={this.sourceTab == KubeConfigSourceTab.FILE} />
active={this.sourceTab == KubeConfigSourceTab.FILE}/>
<Tab
value={KubeConfigSourceTab.TEXT}
label={<Trans>Paste as text</Trans>}
@ -342,71 +346,55 @@ export class AddCluster extends React.Component {
return (
<div className={cssNames("kube-context flex gaps align-center", context)}>
<span>{context}</span>
{isNew && <Icon small material="fiber_new" />}
{isSelected && <Icon small material="check" className="box right" />}
{isNew && <Icon small material="fiber_new"/>}
{isSelected && <Icon small material="check" className="box right"/>}
</div>
);
};
render() {
const addDisabled = this.selectedContexts.length === 0;
return (
<WizardLayout
className="AddCluster"
infoPanel={this.renderInfo()}
contentClass={{ droppable: this.dropAreaActive }}
contentProps={{
onDragEnter: event => this.dropAreaActive = true,
onDragLeave: event => this.dropAreaActive = false,
onDragOver: event => {
event.preventDefault(); // enable onDrop()-callback
event.dataTransfer.dropEffect = "move";
},
onDrop: event => {
this.sourceTab = KubeConfigSourceTab.FILE;
this.dropAreaActive = false;
this.setKubeConfig(event.dataTransfer.files[0].path);
}
}}
>
<h2><Trans>Add Cluster</Trans></h2>
{this.renderKubeConfigSource()}
{this.renderContextSelector()}
<div className="cluster-settings">
<a href="#" onClick={() => this.showSettings = !this.showSettings}>
<Trans>Proxy settings</Trans>
</a>
</div>
{this.showSettings && (
<div className="proxy-settings">
<p>HTTP Proxy server. Used for communicating with Kubernetes API.</p>
<Input
autoFocus
value={this.proxyServer}
onChange={value => this.proxyServer = value}
theme="round-black"
/>
<small className="hint">
{'A HTTP proxy server URL (format: http://<address>:<port>).'}
</small>
<DropFileInput onDropFiles={this.onDropKubeConfig}>
<WizardLayout className="AddCluster" infoPanel={this.renderInfo()}>
<h2><Trans>Add Cluster</Trans></h2>
{this.renderKubeConfigSource()}
{this.renderContextSelector()}
<div className="cluster-settings">
<a href="#" onClick={() => this.showSettings = !this.showSettings}>
<Trans>Proxy settings</Trans>
</a>
</div>
)}
{this.error && (
<div className="error">{this.error}</div>
)}
<div className="actions-panel">
<Button
primary
disabled={addDisabled}
label={<Trans>Add cluster(s)</Trans>}
onClick={this.addClusters}
waiting={this.isWaiting}
tooltip={addDisabled ? _i18n._("Select at least one cluster to add.") : undefined}
tooltipOverrideDisabled
/>
</div>
</WizardLayout>
{this.showSettings && (
<div className="proxy-settings">
<p>HTTP Proxy server. Used for communicating with Kubernetes API.</p>
<Input
autoFocus
value={this.proxyServer}
onChange={value => this.proxyServer = value}
theme="round-black"
/>
<small className="hint">
{'A HTTP proxy server URL (format: http://<address>:<port>).'}
</small>
</div>
)}
{this.error && (
<div className="error">{this.error}</div>
)}
<div className="actions-panel">
<Button
primary
disabled={addDisabled}
label={<Trans>Add cluster(s)</Trans>}
onClick={this.addClusters}
waiting={this.isWaiting}
tooltip={addDisabled ? _i18n._("Select at least one cluster to add.") : undefined}
tooltipOverrideDisabled
/>
</div>
</WizardLayout>
</DropFileInput>
);
}
}

View File

@ -7,7 +7,7 @@ import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { Button } from "../button";
import { WizardLayout } from "../layout/wizard-layout";
import { Input, InputValidators } from "../input";
import { DropFileInput, Input, InputValidators } from "../input";
import { Icon } from "../icon";
import { PageLayout } from "../layout/page-layout";
import { CopyToClick } from "../copy-to-click/copy-to-click";
@ -35,7 +35,7 @@ export class Extensions extends React.Component {
return extensionManager.localFolderPath;
}
selectPackedExtensionsDialog = async () => {
selectLocalExtensionsDialog = async () => {
const { dialog, BrowserWindow, app } = remote;
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
defaultPath: app.getPath("downloads"),
@ -51,15 +51,23 @@ export class Extensions extends React.Component {
}
}
// todo
installFromUrl = () => {
if (!this.downloadUrl) {
this.input?.focus();
return;
}
console.log('Install from URL', this.downloadUrl);
}
// todo
installFromLocalPath = (filePaths: string[]) => {
console.log('Install select from dialog', filePaths)
}
// todo
installOnDrop = (files: File[]) => {
console.log('Install from D&D', files);
}
renderInfo() {
@ -104,7 +112,7 @@ export class Extensions extends React.Component {
<Button
primary
label="Select local extensions"
onClick={this.selectPackedExtensionsDialog}
onClick={this.selectLocalExtensionsDialog}
/>
<small className="hint">
<Trans>Pro-Tip 1: you can download extension archive.tgz via npm by</Trans>{" "}
@ -163,19 +171,21 @@ export class Extensions extends React.Component {
render() {
return (
<PageLayout showOnTop className="Extensions" header={<h2>Extensions</h2>}>
<WizardLayout infoPanel={this.renderInfo()}>
<Input
autoFocus
theme="round-black"
className="SearchInput"
placeholder={_i18n._(t`Search extensions`)}
value={this.search}
onChange={(value) => this.search = value}
/>
<div className="extensions-list">
{this.renderExtensions()}
</div>
</WizardLayout>
<DropFileInput onDropFiles={this.installOnDrop}>
<WizardLayout infoPanel={this.renderInfo()}>
<Input
autoFocus
theme="round-black"
className="SearchInput"
placeholder={_i18n._(t`Search extensions`)}
value={this.search}
onChange={(value) => this.search = value}
/>
<div className="extensions-list">
{this.renderExtensions()}
</div>
</WizardLayout>
</DropFileInput>
</PageLayout>
);
}

View File

@ -47,9 +47,8 @@ export class CopyToClick extends React.Component<CopyToClickProps> {
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;

View File

@ -0,0 +1,9 @@
.DropFileInput {
&.droppable {
box-shadow: 0 0 0 5px $primary; // fixme: might not work sometimes
> * {
pointer-events: none;
}
}
}

View File

@ -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: onDragEnter,
onDragLeave: onDragLeave,
onDragOver: onDragOver,
onDrop: onDrop,
};
return React.cloneElement(contentElem, contentElemProps);
}
} catch (err) {
logger.error("Invalid root content-element for DropFileInput", { err: String(err) })
return this.props.children;
}
}
}

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';

View File

@ -11,7 +11,6 @@ export interface WizardLayoutProps extends React.DOMAttributes<any> {
infoPanelClass?: IClassName;
infoPanel?: React.ReactNode;
centered?: boolean; // Centering content horizontally
contentProps?: React.DOMAttributes<HTMLElement>
}
@observer
@ -19,7 +18,7 @@ export class WizardLayout extends React.Component<WizardLayoutProps> {
render() {
const {
className, contentClass, infoPanelClass, infoPanel, header, headerClass, centered,
children, contentProps = {}, ...props
children, ...props
} = this.props;
return (
<div {...props} className={cssNames("WizardLayout", { centered }, className)}>
@ -28,7 +27,7 @@ export class WizardLayout extends React.Component<WizardLayoutProps> {
{header}
</div>
)}
<div {...contentProps} className={cssNames("content-col flex column gaps", contentClass)}>
<div className={cssNames("content-col flex column gaps", contentClass)}>
<div className="flex column gaps">
{children}
</div>