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:
parent
6624287626
commit
7d28a43993
@ -1,12 +1,4 @@
|
||||
.AddCluster {
|
||||
.droppable {
|
||||
box-shadow: 0 0 0 5px inset $primary;
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: -$padding;
|
||||
color: $textColorSecondary;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
9
src/renderer/components/input/drop-file-input.scss
Normal file
9
src/renderer/components/input/drop-file-input.scss
Normal file
@ -0,0 +1,9 @@
|
||||
.DropFileInput {
|
||||
&.droppable {
|
||||
box-shadow: 0 0 0 5px $primary; // fixme: might not work sometimes
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/renderer/components/input/drop-file-input.tsx
Normal file
76
src/renderer/components/input/drop-file-input.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user