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 { .AddCluster {
.droppable {
box-shadow: 0 0 0 5px inset $primary;
> * {
pointer-events: none;
}
}
.hint { .hint {
margin-top: -$padding; margin-top: -$padding;
color: $textColorSecondary; color: $textColorSecondary;

View File

@ -8,7 +8,7 @@ import { KubeConfig } from "@kubernetes/client-node";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { Input } from "../input"; import { DropFileInput, Input } from "../input";
import { AceEditor } from "../ace-editor"; import { AceEditor } from "../ace-editor";
import { Button } from "../button"; import { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
@ -43,7 +43,6 @@ export class AddCluster extends React.Component {
@observable proxyServer = ""; @observable proxyServer = "";
@observable isWaiting = false; @observable isWaiting = false;
@observable showSettings = false; @observable showSettings = false;
@observable dropAreaActive = false;
componentDidMount() { componentDidMount() {
clusterStore.setActive(null); 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 @action
addClusters = () => { addClusters = () => {
let newClusters: ClusterModel[] = []; let newClusters: ClusterModel[] = [];
@ -137,7 +141,7 @@ export class AddCluster extends React.Component {
return true; return true;
} catch (err) { } catch (err) {
this.error = String(err.message); this.error = String(err.message);
if (err instanceof ExecValidationNotFoundError ) { if (err instanceof ExecValidationNotFoundError) {
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>); Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
return false; return false;
} else { } else {
@ -228,7 +232,7 @@ export class AddCluster extends React.Component {
<Tab <Tab
value={KubeConfigSourceTab.FILE} value={KubeConfigSourceTab.FILE}
label={<Trans>Select kubeconfig file</Trans>} label={<Trans>Select kubeconfig file</Trans>}
active={this.sourceTab == KubeConfigSourceTab.FILE} /> active={this.sourceTab == KubeConfigSourceTab.FILE}/>
<Tab <Tab
value={KubeConfigSourceTab.TEXT} value={KubeConfigSourceTab.TEXT}
label={<Trans>Paste as text</Trans>} label={<Trans>Paste as text</Trans>}
@ -342,71 +346,55 @@ export class AddCluster extends React.Component {
return ( return (
<div className={cssNames("kube-context flex gaps align-center", context)}> <div className={cssNames("kube-context flex gaps align-center", context)}>
<span>{context}</span> <span>{context}</span>
{isNew && <Icon small material="fiber_new" />} {isNew && <Icon small material="fiber_new"/>}
{isSelected && <Icon small material="check" className="box right" />} {isSelected && <Icon small material="check" className="box right"/>}
</div> </div>
); );
}; };
render() { render() {
const addDisabled = this.selectedContexts.length === 0; const addDisabled = this.selectedContexts.length === 0;
return ( return (
<WizardLayout <DropFileInput onDropFiles={this.onDropKubeConfig}>
className="AddCluster" <WizardLayout className="AddCluster" infoPanel={this.renderInfo()}>
infoPanel={this.renderInfo()} <h2><Trans>Add Cluster</Trans></h2>
contentClass={{ droppable: this.dropAreaActive }} {this.renderKubeConfigSource()}
contentProps={{ {this.renderContextSelector()}
onDragEnter: event => this.dropAreaActive = true, <div className="cluster-settings">
onDragLeave: event => this.dropAreaActive = false, <a href="#" onClick={() => this.showSettings = !this.showSettings}>
onDragOver: event => { <Trans>Proxy settings</Trans>
event.preventDefault(); // enable onDrop()-callback </a>
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>
</div> </div>
)} {this.showSettings && (
{this.error && ( <div className="proxy-settings">
<div className="error">{this.error}</div> <p>HTTP Proxy server. Used for communicating with Kubernetes API.</p>
)} <Input
<div className="actions-panel"> autoFocus
<Button value={this.proxyServer}
primary onChange={value => this.proxyServer = value}
disabled={addDisabled} theme="round-black"
label={<Trans>Add cluster(s)</Trans>} />
onClick={this.addClusters} <small className="hint">
waiting={this.isWaiting} {'A HTTP proxy server URL (format: http://<address>:<port>).'}
tooltip={addDisabled ? _i18n._("Select at least one cluster to add.") : undefined} </small>
tooltipOverrideDisabled </div>
/> )}
</div> {this.error && (
</WizardLayout> <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 { _i18n } from "../../i18n";
import { Button } from "../button"; import { Button } from "../button";
import { WizardLayout } from "../layout/wizard-layout"; import { WizardLayout } from "../layout/wizard-layout";
import { Input, InputValidators } from "../input"; import { DropFileInput, Input, InputValidators } from "../input";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { PageLayout } from "../layout/page-layout"; import { PageLayout } from "../layout/page-layout";
import { CopyToClick } from "../copy-to-click/copy-to-click"; import { CopyToClick } from "../copy-to-click/copy-to-click";
@ -35,7 +35,7 @@ export class Extensions extends React.Component {
return extensionManager.localFolderPath; return extensionManager.localFolderPath;
} }
selectPackedExtensionsDialog = async () => { selectLocalExtensionsDialog = async () => {
const { dialog, BrowserWindow, app } = remote; const { dialog, BrowserWindow, app } = remote;
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), { const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
defaultPath: app.getPath("downloads"), defaultPath: app.getPath("downloads"),
@ -51,15 +51,23 @@ export class Extensions extends React.Component {
} }
} }
// todo
installFromUrl = () => { installFromUrl = () => {
if (!this.downloadUrl) { if (!this.downloadUrl) {
this.input?.focus(); this.input?.focus();
return; return;
} }
console.log('Install from URL', this.downloadUrl);
} }
// todo
installFromLocalPath = (filePaths: string[]) => { installFromLocalPath = (filePaths: string[]) => {
console.log('Install select from dialog', filePaths)
}
// todo
installOnDrop = (files: File[]) => {
console.log('Install from D&D', files);
} }
renderInfo() { renderInfo() {
@ -104,7 +112,7 @@ export class Extensions extends React.Component {
<Button <Button
primary primary
label="Select local extensions" label="Select local extensions"
onClick={this.selectPackedExtensionsDialog} onClick={this.selectLocalExtensionsDialog}
/> />
<small className="hint"> <small className="hint">
<Trans>Pro-Tip 1: you can download extension archive.tgz via npm by</Trans>{" "} <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() { render() {
return ( return (
<PageLayout showOnTop className="Extensions" header={<h2>Extensions</h2>}> <PageLayout showOnTop className="Extensions" header={<h2>Extensions</h2>}>
<WizardLayout infoPanel={this.renderInfo()}> <DropFileInput onDropFiles={this.installOnDrop}>
<Input <WizardLayout infoPanel={this.renderInfo()}>
autoFocus <Input
theme="round-black" autoFocus
className="SearchInput" theme="round-black"
placeholder={_i18n._(t`Search extensions`)} className="SearchInput"
value={this.search} placeholder={_i18n._(t`Search extensions`)}
onChange={(value) => this.search = value} value={this.search}
/> onChange={(value) => this.search = value}
<div className="extensions-list"> />
{this.renderExtensions()} <div className="extensions-list">
</div> {this.renderExtensions()}
</WizardLayout> </div>
</WizardLayout>
</DropFileInput>
</PageLayout> </PageLayout>
); );
} }

View File

@ -47,9 +47,8 @@ export class CopyToClick extends React.Component<CopyToClickProps> {
try { try {
const rootElem = this.rootReactElem; const rootElem = this.rootReactElem;
return React.cloneElement(rootElem, { return React.cloneElement(rootElem, {
...(rootElem || {}).props,
onClick: this.onClick, onClick: this.onClick,
}) });
} catch (err) { } catch (err) {
logger.error(`Invalid usage components/CopyToClick usage. Children must contain root html element.`, { err: String(err) }) logger.error(`Invalid usage components/CopyToClick usage. Children must contain root html element.`, { err: String(err) })
return this.rootReactElem; 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';
export * from './search-input-url'; export * from './search-input-url';
export * from './file-input'; export * from './file-input';
export * from './drop-file-input';

View File

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