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 {
|
.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;
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
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';
|
||||||
export * from './search-input-url';
|
export * from './search-input-url';
|
||||||
export * from './file-input';
|
export * from './file-input';
|
||||||
|
export * from './drop-file-input';
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user